diff --git a/README.md b/README.md index ff9ab4a..832b4e2 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,27 @@ Sag === -Note: I have not maintained this project for years since I no longer use -CouchDB or PHP. I assume it still works with CouchDB, but haven't been active -in the community for a while and won't be testing or responding to tickets. - -Version %VERSION% - -http://www.saggingcouch.com - Sag is a PHP library for working with CouchDB. It is designed to not force any particular programming method on its users - you just pass PHP objects, and get stdClass objects and Exceptions back. This makes it trivial to incorporate Sag into your application, build different functionality on top of it, and expand Sag to incorporate new CouchDB functionality. -Compatability +This is a *fork* of the original SAG Library that was hosted on saggincouch.com. + +Compatibility ------------- Each Sag release is tested with an automated testing suite against all the combinations of: - - PHP 5.5.x - - - CouchDB 1.6.x + - PHP 7.x - - Cloudant + - CouchDB 3.x Lower versions of CouchDB and PHP will likely work with Sag, but they are not officially supported, so your mileage may vary. -If you are running pre-1.5.1 CouchDB (important security fix) or pre-5.3 PHP, -then you probably want to look into updating your environment. Error Handling -------------- @@ -142,8 +132,7 @@ as a stdClass. Functions --------- -Detailed documentation of the functions and API are available at -http://www.saggingcouch.com/documentation.php. +_to be updated_ License ------- @@ -153,8 +142,3 @@ LICENSE for more information. Copyright information is in the NOTICE file. -More? ------ - -See http://www.saggingcouch.com for more detailed information, bug reporting, -planned features, etc. diff --git a/src/Sag.php b/src/Sag.php index 49a163f..c8c1741 100644 --- a/src/Sag.php +++ b/src/Sag.php @@ -21,10 +21,17 @@ /** * The Sag class provides the core functionality for talking to CouchDB. * - * @version %VERSION% + * @version 0.9.1 * @package Core */ class Sag { + + /** + * @var string Used by login() to use HTTP Basic Authentication. + * @static + */ + public static $VERSION = "0.9.1"; + /** * @var string Used by login() to use HTTP Basic Authentication. * @static @@ -210,6 +217,32 @@ public function login($user, $pass, $type = null) { //should never reach this line throw new SagException("Unknown auth type for login()."); } + + + /** + * Sets the internal (private) variables to use cookie authentication in all subsequent calls to + * $this->procPacket(), which gets used by all worker functions in Sag. + * + * @param string $cookieValue the cookie value $this->authSession from $this->login ($username, $password, Sag::$AUTH_COOKIE); + */ + public function loginByCookie ($cookieValue=null) { + if (is_null($cookieValue)) { + throw new SagException('loginByCookie() expects a string parameter which should be the cookie (the return value) generated by login($username, $password, Sag::$AUTH_COOKIE).'); + } + + $this->authType = Sag::$AUTH_COOKIE; + $this->authSession = $cookieValue; + //echo 'loginByCookie():';var_dump ($this->authType); + } + + public function checkSession () { + $res = $this->procPacket( + 'GET', + '/_session' + ); + + echo 'high level checkSession() results=
'; var_dump ($res); echo '
'.PHP_EOL; + } /** * Get current session information on the server with /_session. @@ -379,6 +412,7 @@ public function put($id, $data) $toSend = (is_string($data)) ? $data : json_encode($data); $id = urlencode($id); + $id = str_replace("%2F", "/", $id); /** Url Encoding a / confuses Couch **/ $url = "/{$this->db}/$id"; $response = $this->procPacket('PUT', $url, $toSend); @@ -448,6 +482,32 @@ public function post($data, $path = null) { return $this->procPacket('POST', "/{$this->db}{$path}", $data); } + + /** + * Makes a request to the _changes feed. + * + * @param array $parameters The feed parameters + * @param mixed $request The request available in the filter function + * + * @return mixed + */ + public function changes($parameters = array(), $request = null) { + if(!$this->db) { + throw new SagException('No database specified'); + } + + if(!is_null($request) && (!is_string($request) && !is_object($request) && !is_array($request))) { + throw new SagException('changes() needs an object for request.'); + } + + if(!is_string($request)) { + $request = json_encode($request); + } + + $queryString = (count($parameters) > 0 ? "?" : "").http_build_query($parameters); + + return $this->procPacket('POST', "/{$this->db}/_changes{$queryString}", $request); + } /** * Bulk pushes documents to the database. @@ -688,6 +748,41 @@ public function generateIDs($num = 10) { } /** + * Uses CouchDB to generate IDs with custom prefix ad separator. + * + * @param int $num The number of IDs to generate (>= 0). Defaults to 10. + * @param string $prefix The prefix for the ID + * @param string $separator the separatore between prefix and ID + * @return array + */ + public function generateIDsCustom($num = 10, $prefix = "", $separator = ":") { + $ids = array(); + + $docs = $this->generateIDs($num); + if ($docs->status == 200) { + if (isset($docs->body->uuids) && is_array($docs->body->uuids)) { + foreach ($docs->body->uuids as $uuid) { + $ids[] = $prefix . $separator . $uuid; + } + } + } + + return $ids; + } + + /** + * Uses CouchDB to generate only one ID with custom prefix ad separator. + * + * @param string $prefix The prefix for the ID + * @param string $separator the separatore between prefix and ID + * @return string + */ + public function generateIDCustom($prefix, $separator = ":") { + $ids = $this->generateIDsCustom(1, $prefix, $separator); + return $ids[0]; + } + + /** * Creates a database with the specified name. * * @param string $name The name of the database you want to create. @@ -732,15 +827,16 @@ public function deleteDatabase($name) { * @param mixed $filterQueryParams An object or associative array of * parameters to be passed to the filter function via query_params. Only used * if $filter is set. + * @param array $doc_ids Array of document IDs to be synchronized * * @return mixed */ - public function replicate($src, $target, $continuous = false, $createTarget = null, $filter = null, $filterQueryParams = null) { - if(empty($src) || !is_string($src)) { + public function replicate($src, $target, $continuous = false, $createTarget = null, $filter = null, $filterQueryParams = null, $doc_ids = null) { + if(empty($src) || (!is_string($src) && !is_object($src))) { throw new SagException('replicate() is missing a source to replicate from.'); } - if(empty($target) || !is_string($target)) { + if(empty($target) || (!is_string($target)) && !is_object($target)) { throw new SagException('replicate() is missing a target to replicate to.'); } @@ -762,6 +858,10 @@ public function replicate($src, $target, $continuous = false, $createTarget = nu } } + if (isset($doc_ids) && !is_array($doc_ids)) { + throw new SagException('Doc IDs needs to be an array.'); + } + $data = new stdClass(); $data->source = $src; $data->target = $target; @@ -786,6 +886,10 @@ public function replicate($src, $target, $continuous = false, $createTarget = nu } } + if ($doc_ids) { + $data->doc_ids = $doc_ids; + } + return $this->procPacket('POST', '/_replicate', json_encode($data)); } @@ -1063,6 +1167,60 @@ public function getPathPrefix() { return $this->pathPrefix; } + /** + * Interface to /db/_index + * + * @param mixed $data (see https://docs.couchdb.org/en/stable/api/database/find.html#db-index) + * $data = array ( + * 'index' => array( + * 'fields' => array ('foo') + * ), + * 'name' => 'foo-index', + * 'type' => 'json' + * ); + * + * + * @return Sag Returns $this->procPacket() results. + */ + public function setIndex($data) { + if (!is_string($data)) $data = json_encode($data); + return $this->procPacket ('POST', '/'.$this->db.'/_index', $data); + } + + /** + * Interface to /db/_find + * + * @param mixed $data + * $data = array ( + * 'selector' => array( + * 'shortened' => $newID + * ), + * 'fields' => array( + * '_id', 'creator', 'destination', 'shortened' + * ) + * ); + * + * + * @return Sag Returns $this->procPacket() results. + */ + public function find($data) { + if (!is_string($data)) $data = json_encode($data); + return $this->procPacket ('POST', '/'.$this->db.'/_find', $data); + } + + /** + * Interface to /db/_security + * + * @param mixed $data + * $data = '{ "admins": { "names": [], "roles": ["guests"] }, "members": { "names": ["Administrator"], "roles": ["guests"] } }'; + * + * @return Sag Returns $this->procPacket() results. + */ + public function setSecurity($data) { + if (!is_string($data)) $data = json_encode($data); + return $this->procPacket ('PUT', '/'.$this->db.'/_security', $data); + } + // The main driver - does all the socket and protocol work. private function procPacket($method, $url, $data = null, $headers = array()) { /* @@ -1093,7 +1251,7 @@ private function procPacket($method, $url, $data = null, $headers = array()) { // Build the request packet. $headers["Host"] = "{$this->host}:{$this->port}"; - $headers["User-Agent"] = "Sag/%VERSION%"; + $headers["User-Agent"] = "Sag/" . self::$VERSION; /* * This prevents some unRESTful requests, such as inline attachments in @@ -1103,6 +1261,8 @@ private function procPacket($method, $url, $data = null, $headers = array()) { $headers['Accept'] = 'application/json'; //usernames and passwords can be blank + //$dbg = [ '$this->authType' => $this->authType, 'Sag::$AUTH_BASIC' => Sag::$AUTH_BASIC, 'Sag::$AUTH_COOKIE' => Sag::$AUTH_COOKIE ]; + //var_dump ($dbg); if($this->authType == Sag::$AUTH_BASIC && (isset($this->user) || isset($this->pass))) { $headers["Authorization"] = 'Basic '.base64_encode("{$this->user}:{$this->pass}"); } @@ -1110,6 +1270,7 @@ private function procPacket($method, $url, $data = null, $headers = array()) { $headers['Cookie'] = array( 'AuthSession' => $this->authSession ); $headers['X-CouchDB-WWW-Authenticate'] = 'Cookie'; } + //var_dump ($headers); if(is_array($this->globalCookies) && sizeof($this->globalCookies)) { //might have been set before by auth handling diff --git a/src/httpAdapters/SagCURLHTTPAdapter.php b/src/httpAdapters/SagCURLHTTPAdapter.php index a195f50..e6996c1 100644 --- a/src/httpAdapters/SagCURLHTTPAdapter.php +++ b/src/httpAdapters/SagCURLHTTPAdapter.php @@ -58,6 +58,10 @@ public function procPacket($method, $url, $data = null, $reqHeaders = array(), $ $opts[CURLOPT_POSTFIELDS] = $data; } + if($method == 'GET') { + $opts[CURLOPT_ENCODING] = ""; + } + // special considerations for HEAD requests if($method == 'HEAD') { $opts[CURLOPT_NOBODY] = true; @@ -92,8 +96,9 @@ public function procPacket($method, $url, $data = null, $reqHeaders = array(), $ curl_reset($this->ch); curl_setopt_array($this->ch, $opts); - + //echo 'curl options = '; var_dump ($opts); echo PHP_EOL; $chResponse = curl_exec($this->ch); + //echo 'curl response = '; var_dump ($chResponse); if($chResponse !== false) { // prepare the response object @@ -122,7 +127,7 @@ public function procPacket($method, $url, $data = null, $reqHeaders = array(), $ else { $line = explode(':', $respHeaders[$i], 2); $line[0] = strtolower($line[0]); - $response->headers->$line[0] = ltrim($line[1]); + $response->headers->{$line[0]} = ltrim($line[1]); if($line[0] == 'set-cookie') { $response->cookies = $this->parseCookieString($line[1]); diff --git a/src/httpAdapters/SagHTTPAdapter.php b/src/httpAdapters/SagHTTPAdapter.php index d717fc5..25ba5e2 100644 --- a/src/httpAdapters/SagHTTPAdapter.php +++ b/src/httpAdapters/SagHTTPAdapter.php @@ -38,6 +38,7 @@ protected function makeResult($response, $method) { if( $method != 'HEAD' && isset($response->headers->{'content-length'}) && + ! isset($response->headers->{'content-encoding'}) && strlen($response->body) != $response->headers->{'content-length'} ) { throw new SagException('Unexpected end of packet.'); @@ -67,7 +68,7 @@ protected function makeResult($response, $method) { ) { $json = json_decode($response->body); - if(isset($json)) { + if(isset($json) && $json !== FALSE) { if(!empty($json->error)) { throw new SagCouchException("{$json->error} ({$json->reason})", $response->headers->_HTTP->status); } diff --git a/src/httpAdapters/SagNativeHTTPAdapter.php b/src/httpAdapters/SagNativeHTTPAdapter.php index c1c54b0..260d3c6 100644 --- a/src/httpAdapters/SagNativeHTTPAdapter.php +++ b/src/httpAdapters/SagNativeHTTPAdapter.php @@ -12,6 +12,7 @@ class SagNativeHTTPAdapter extends SagHTTPAdapter { private $connPool = array(); //Connection pool + private $useSSL = FALSE; /** * Closes any sockets that are left open in the connection pool. @@ -26,14 +27,14 @@ public function __destruct() { * Native sockets does not support SSL. */ public function useSSL($use) { - throw new SagException('Sag::$HTTP_NATIVE_SOCKETS does not support SSL.'); + $this->useSSL = $use; } /** * Native sockets does not support SSL. */ public function setSSLCert($path) { - throw new SagException('Sag::$HTTP_NATIVE_SOCKETS does not support SSL.'); + throw new SagException('Sag::$HTTP_NATIVE_SOCKETS does not yet implement SSL checking.'); } public function procPacket($method, $url, $data = null, $reqHeaders = array(), $specialHost = null, $specialPort = null) { @@ -80,10 +81,18 @@ public function procPacket($method, $url, $data = null, $reqHeaders = array(), $ try { //these calls should throw on error if($this->socketOpenTimeout) { - $sock = fsockopen($host, $port, $sockErrNo, $sockErrStr, $this->socketOpenTimeout); + if ($this->useSSL) { + $sock = fsockopen("ssl://".$host, $port, $sockErrNo, $sockErrStr, $this->socketOpenTimeout); + } else { + $sock = fsockopen($host, $port, $sockErrNo, $sockErrStr, $this->socketOpenTimeout); + } } else { - $sock = fsockopen($host, $port, $sockErrNo, $sockErrStr); + if ($this->useSSL) { + $sock = fsockopen("ssl://".$host, $port, $sockErrNo, $sockErrStr); + } else { + $sock = fsockopen($host, $port, $sockErrNo, $sockErrStr); + } } /* @@ -205,7 +214,7 @@ public function procPacket($method, $url, $data = null, $reqHeaders = array(), $ else { $line = explode(':', $line, 2); $line[0] = strtolower($line[0]); - $response->headers->$line[0] = $line[1] = ltrim($line[1]); + $response->headers->{$line[0]} = $line[1] = ltrim($line[1]); switch($line[0]) { case 'set-cookie': diff --git a/tests/SagTest.php b/tests/SagTest.php index 568a6ef..b5c78a2 100644 --- a/tests/SagTest.php +++ b/tests/SagTest.php @@ -92,6 +92,70 @@ public function test_newDoc() $this->assertTrue($result->body->ok); $this->assertEquals($result->body->id, '1'); } + + public function test_setIndex() + { + $docs = array ( + array ( + 'one' => 'abc', + 'two' => 'def' + ), + array ( + 'one' => 'ghi', + 'two' => 'jkl' + ) + ); + + foreach ($docs as $idx => $doc) { + $this->assertTrue($this->couch->post($doc)->body->ok); + } + + $cmdSetIndex = array ( + 'index' => array ( 'fields' => array ('one') ), + 'name' => 'one-index', + 'ddoc' => 'one-index', + 'type' => 'json' + ); + $call = $this->couch->setIndex ($cmdSetIndex); + $this->assertTrue ($call->headers->_HTTP->status===200); + } + + public function test_find($index) + { + $docs = array ( + array ( + 'one' => 'abc', + 'two' => 'def' + ), + array ( + 'one' => 'ghi', + 'two' => 'jkl' + ) + ); + + foreach ($docs as $idx => $doc) { + $this->assertTrue($this->couch->post($doc)->body->ok); + } + + $findCommand = array ( + 'selector' => array ( 'one' => 'abc' ), + 'fields' => array ( 'one', 'two' ) + ); + $call = $this->couch->find ($findCommand); + $this->assertTrue ($call->headers->_HTTP->status===200); + $this->assertTrue (sizeof($call->body->docs)===1); + } + + public function test_setSecurity() + { + $json = '{ "admins": { "names": [], "roles": ["guests"] }, "members": { "names": ["Administrator"], "roles": ["guests"] } }'; + try { + $call = $this->couch->setSecurity ($json); + } catch (Exception $e) { + $this->assertTrue(false); + } + $this->assertTrue(true); + } public function test_newDocFromArray() { diff --git a/tests/bootstrap.bsh b/tests/bootstrap.bsh old mode 100755 new mode 100644