Skip to content
This repository has been archived by the owner on Jul 5, 2023. It is now read-only.

Commit

Permalink
[php] Support authenticating directly against a db.
Browse files Browse the repository at this point in the history
See #16

Note that this doesn't work if genghis.php is behind Apache unless `AllowEncodedSlashes` is enabled.

See #62
  • Loading branch information
bobthecow committed Nov 24, 2012
1 parent e5e36c6 commit d25facf
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 11 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* Handle crazy characters in collection names (PHP backend) – See #56
* Better heuristic for guessing document creation date from ObjectId — See #55
* Catch more connection auth errors (Ruby backend).
* Support authenticating directly against a DB for non-admin users (Ruby backend) — See #16
* Support authenticating directly against a DB for non-admin users — See #16


## v2.1.4
Expand Down
2 changes: 1 addition & 1 deletion genghis.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Genghis_JsonRegex { public $pattern; public function __construct($pattern)
class Genghis_JsonResponse extends Genghis_Response { public function renderHeaders() { $this->headers['Content-type'] = 'application/json'; $this->headers['Cache-Control'] = 'no-cache, must-revalidate'; $this->headers['Expires'] = 'Wed, 04 Aug 1982 00:00:00 GMT'; parent::renderHeaders(); } public function renderContent() { print(Genghis_Json::encode($this->data)); } }
class Genghis_Models_Collection implements ArrayAccess, Genghis_JsonEncodable { public $database; public $collection; public function __construct(Genghis_Models_Database $database, MongoCollection $collection) { $this->database = $database; $this->collection = $collection; } public function offsetExists($id) { try { $this->findDocument($id); } catch (Genghis_HttpException $e) { if ($e->getStatus() == 404) { return false; } else { throw $e; } } return true; } public function offsetGet($id) { return $this->findDocument($id); } public function offsetSet($id, $doc) { $this->findDocument($id); $query = array('_id' => $this->thunkMongoId($id)); $result = $this->collection->update($query, $doc, array('safe' => true)); if (!(isset($result['ok']) && $result['ok'])) { throw new Genghis_HttpException; } } public function offsetUnset($id) { $this->findDocument($id); $query = array('_id' => $this->thunkMongoId($id)); $result = $this->collection->remove($query, array('safe' => true)); if (!(isset($result['ok']) && $result['ok'])) { throw new Genghis_HttpException; } } public function findDocuments($query = null, $page = 1) { try { $query = Genghis_Json::decode($query); } catch (Genghis_JsonException $e) { throw new Genghis_HttpException(400, 'Malformed document'); } $offset = Genghis_Api::PAGE_LIMIT * ($page - 1); $cursor = $this->collection ->find($query ? $query : array()) ->limit(Genghis_Api::PAGE_LIMIT) ->skip($offset); $count = $cursor->count(); if (is_array($count) && isset($count['errmsg'])) { throw new Genghis_HttpException(400, $count['errmsg']); } $documents = array(); foreach ($cursor as $doc) { $documents[] = $doc; } return array( 'count' => $count, 'page' => $page, 'pages' => max(1, ceil($count / Genghis_Api::PAGE_LIMIT)), 'per_page' => Genghis_Api::PAGE_LIMIT, 'offset' => $offset, 'documents' => $documents, ); } public function insert($data) { $result = $this->collection->insert($data, array('safe' => true)); if (!(isset($result['ok']) && $result['ok'])) { throw new Genghis_HttpException; } return $data; } public function drop() { $this->collection->drop(); } public function asJson() { $name = $this->collection->getName(); $colls = $this->database->database->listCollections(); foreach ($colls as $coll) { if ($coll->getName() == $name) { return array( 'id' => $coll->getName(), 'name' => $coll->getName(), 'count' => $coll->count(), 'indexes' => $coll->getIndexInfo(), ); } } throw new Genghis_HttpException(404, sprintf("Collection '%s' not found in '%s'", $name, $this->database->name)); } private function thunkMongoId($id) { if ($id[0] == '~') { return Genghis_Json::decode(base64_decode(substr($id, 1))); } return preg_match('/^[a-f0-9]{24}$/i', $id) ? new MongoId($id) : $id; } private function findDocument($id) { $doc = $this->collection->findOne(array('_id' => $this->thunkMongoId($id))); if (!$doc) { throw new Genghis_HttpException(404, sprintf("Document '%s' not found in '%s'", $id, $this->collection->getName())); } return $doc; } }
class Genghis_Models_Database implements ArrayAccess, Genghis_JsonEncodable { public $name; public $server; public $database; private $collections = array(); private $mongoCollections; public function __construct(Genghis_Models_Server $server, MongoDB $database) { $this->server = $server; $this->database = $database; $this->name = (string) $database; } public function drop() { $this->database->drop(); } public function offsetExists($name) { return ($this->getMongoCollection($name) !== null); } public function offsetGet($name) { if (!isset($this->collections[$name])) { $coll = $this->getMongoCollection($name); if ($coll === null) { throw new Genghis_HttpException(404, sprintf("Collection '%s' not found in '%s'", $name, $this->name)); } $this->collections[$name] = new Genghis_Models_Collection($this, $coll); } return $this->collections[$name]; } public function offsetSet($name, $value) { throw new Exception; } public function offsetUnset($name) { $this[$name]->drop(); } public function getCollectionNames() { $colls = array(); foreach ($this->getMongoCollections() as $coll) { $colls[] = $coll->getName(); } return $colls; } public function listCollections() { return array_map(array($this, 'offsetGet'), $this->getCollectionNames()); } public function createCollection($name) { if (isset($this[$name])) { throw new Genghis_HttpException(400, sprintf("Collection '%s' already exists in '%s'", $name, $this->name)); } try { $this->database->createCollection($name); } catch (Exception $e) { if (strpos($e->getMessage(), 'invalid name') !== false) { throw new Genghis_HttpException(400, 'Invalid collection name'); } throw $e; } unset($this->mongoCollections); return $this[$name]; } public function asJson() { $dbs = $this->server->getConnection()->listDBs(); foreach ($dbs['databases'] as $db) { if ($db['name'] == $this->name) { $colls = $this->getCollectionNames(); return array( 'id' => $db['name'], 'name' => $db['name'], 'count' => count($colls), 'collections' => $colls, 'size' => $db['sizeOnDisk'], ); } } throw new Genghis_HttpException(404, sprintf("Database '%s' not found on '%s'", $database, $server)); } private function getMongoCollection($name) { foreach ($this->getMongoCollections() as $coll) { if ($coll->getName() === $name) { return $coll; } } } private function getMongoCollections() { if (!isset($this->mongoCollections)) { $this->mongoCollections = $this->database->listCollections(); } return $this->mongoCollections; } }
class Genghis_Models_Server implements ArrayAccess, Genghis_JsonEncodable { public $dsn; public $name; public $options; public $default; private $connection; private $databases = array(); public function __construct($dsn, $default = false) { $this->default = $default; try { $config = self::parseDsn($dsn); $this->name = $config['name']; $this->dsn = $config['dsn']; $this->options = $config['options']; } catch (Genghis_HttpException $e) { $this->name = $dsn; $this->dsn = $dsn; $this->error = $e->getMessage(); } } public function offsetExists($name) { $list = $this->getConnection()->listDBs(); foreach ($list['databases'] as $db) { if ($db['name'] === $name) { return true; } } return false; } public function offsetGet($name) { if (!isset($this[$name])) { throw new Genghis_HttpException(404, sprintf("Database '%s' not found on '%s'", $name, $this->name)); } if (!isset($this->databases[$name])) { $this->databases[$name] = new Genghis_Models_Database($this, $this->getConnection()->selectDB($name)); } return $this->databases[$name]; } public function getConnection() { if (!isset($this->connection)) { $this->connection = new Mongo($this->dsn, array_merge(array('timeout' => 1000), $this->options)); } return $this->connection; } public function createDatabase($name) { if (isset($this[$name])) { throw new Genghis_HttpException(400, sprintf("Database '%s' already exists on '%s'", $name, $this->name)); } try { $db = $this->connection->selectDB($name); } catch (Exception $e) { if (strpos($e->getMessage(), 'invalid name') !== false) { throw new Genghis_HttpException(400, 'Invalid database name'); } throw $e; } $db->selectCollection('__genghis_tmp_collection__')->drop(); return $this[$name]; } public function listDatabases() { $dbs = array(); $list = $this->getConnection()->listDBs(); foreach ($list['databases'] as $db) { $dbs[] = $this[$db['name']]; } return $dbs; } public function getDatabaseNames() { $names = array(); $list = $this->getConnection()->listDBs(); foreach ($list['databases'] as $db) { $names[] = $db['name']; } return $names; } public function offsetSet($name, $value) { throw new Exception; } public function offsetUnset($name) { $this[$name]->drop(); } public function asJson() { $server = array( 'id' => $this->name, 'name' => $this->name, 'editable' => !$this->default, ); if (isset($this->error)) { $server['error'] = $this->error; return $server; } try { $res = $this->getConnection()->listDBs(); if (isset($res['errmsg'])) { $server['error'] = sprintf("Unable to connect to Mongo server at '%s': %s", $this->name, $res['errmsg']); return $server; } $dbs = $this->getDatabaseNames(); return array_merge($server, array( 'size' => $res['totalSize'], 'count' => count($dbs), 'databases' => $dbs, )); } catch (Exception $e) { $server['error'] = sprintf("Unable to connect to Mongo server at '%s'", $this->name); return $server; } } const DSN_PATTERN = "~^(?:mongodb://)?(?:(?P<username>[^:@]+):(?P<password>[^@]+)@)?(?P<host>[^,/@:]+)(?::(?P<port>\d+))?(?:/(?P<database>[^\?]+)?(?:\?(?P<options>.*))?)?$~"; public static function parseDsn($dsn) { $chunks = array(); if (!preg_match(self::DSN_PATTERN, $dsn, $chunks)) { throw new Genghis_HttpException(400, 'Malformed server DSN'); } $options = array(); if (isset($chunks['options'])) { parse_str($chunks['options'], str_replace(';', '&', $options)); foreach ($options as $name => $value) { switch ($name) { case 'replicaSet': $options['replicaSet'] = (string) $value; break; case 'connectTimeoutMS': $options['timeout'] = intval($value); break; case 'slaveOk': case 'safe': case 'w': case 'wtimeoutMS': case 'fsync': case 'journal': case 'socketTimeoutMS': throw new Genghis_HttpException(400, 'Unsupported connection option - ' . $name); default: throw new Genghis_HttpException(400, 'Malformed server DSN: Unknown connection option - ' . $name); } } } $name = $chunks['host']; if (isset($chunks['user'])) { $name = $chunks['user'].'@'.$name; } if (isset($chunks['port'])) { $port = intval($chunks['port']); if ($port !== 27017) { $name .= ':'.$port; } } return compact('name', 'dsn', 'options'); } }
class Genghis_Models_Server implements ArrayAccess, Genghis_JsonEncodable { public $dsn; public $name; public $options; public $default; public $db; private $connection; private $databases = array(); public function __construct($dsn, $default = false) { $this->default = $default; try { $config = self::parseDsn($dsn); $this->name = $config['name']; $this->dsn = $config['dsn']; $this->options = $config['options']; if (isset($config['db'])) { $this->db = $config['db']; } } catch (Genghis_HttpException $e) { $this->name = $dsn; $this->dsn = $dsn; $this->error = $e->getMessage(); } } public function offsetExists($name) { $list = $this->listDBs(); foreach ($list['databases'] as $db) { if ($db['name'] === $name) { return true; } } return false; } public function offsetGet($name) { if (!isset($this[$name])) { throw new Genghis_HttpException(404, sprintf("Database '%s' not found on '%s'", $name, $this->name)); } if (!isset($this->databases[$name])) { $this->databases[$name] = new Genghis_Models_Database($this, $this->getConnection()->selectDB($name)); } return $this->databases[$name]; } public function getConnection() { if (!isset($this->connection)) { $this->connection = new Mongo($this->dsn, array_merge(array('timeout' => 1000), $this->options)); } return $this->connection; } public function createDatabase($name) { if (isset($this[$name])) { throw new Genghis_HttpException(400, sprintf("Database '%s' already exists on '%s'", $name, $this->name)); } try { $db = $this->connection->selectDB($name); } catch (Exception $e) { if (strpos($e->getMessage(), 'invalid name') !== false) { throw new Genghis_HttpException(400, 'Invalid database name'); } throw $e; } $db->selectCollection('__genghis_tmp_collection__')->drop(); return $this[$name]; } public function listDatabases() { $dbs = array(); $list = $this->listDBs(); foreach ($list['databases'] as $db) { $dbs[] = $this[$db['name']]; } return $dbs; } public function getDatabaseNames() { $names = array(); $list = $this->listDBs(); foreach ($list['databases'] as $db) { $names[] = $db['name']; } return $names; } public function offsetSet($name, $value) { throw new Exception; } public function offsetUnset($name) { $this[$name]->drop(); } public function asJson() { $server = array( 'id' => $this->name, 'name' => $this->name, 'editable' => !$this->default, ); if (isset($this->error)) { $server['error'] = $this->error; return $server; } try { $res = $this->listDBs(); if (isset($res['errmsg'])) { $server['error'] = sprintf("Unable to connect to Mongo server at '%s': %s", $this->name, $res['errmsg']); return $server; } $dbs = $this->getDatabaseNames(); return array_merge($server, array( 'size' => $res['totalSize'], 'count' => count($dbs), 'databases' => $dbs, )); } catch (Exception $e) { $server['error'] = sprintf("Unable to connect to Mongo server at '%s'", $this->name); return $server; } } const DSN_PATTERN = "~^(?:mongodb://)?(?:(?P<username>[^:@]+):(?P<password>[^@]+)@)?(?P<host>[^,/@:]+)(?::(?P<port>\d+))?(?:/(?P<database>[^\?]+)?(?:\?(?P<options>.*))?)?$~"; public static function parseDsn($dsn) { $chunks = array(); if (!preg_match(self::DSN_PATTERN, $dsn, $chunks)) { throw new Genghis_HttpException(400, 'Malformed server DSN'); } if (strpos($dsn, 'mongodb://') !== 0) { $dsn = 'mongodb://'.$dsn; } $options = array(); if (isset($chunks['options'])) { parse_str($chunks['options'], str_replace(';', '&', $options)); foreach ($options as $name => $value) { switch ($name) { case 'replicaSet': $options['replicaSet'] = (string) $value; break; case 'connectTimeoutMS': $options['timeout'] = intval($value); break; case 'slaveOk': case 'safe': case 'w': case 'wtimeoutMS': case 'fsync': case 'journal': case 'socketTimeoutMS': throw new Genghis_HttpException(400, 'Unsupported connection option - ' . $name); default: throw new Genghis_HttpException(400, 'Malformed server DSN: Unknown connection option - ' . $name); } } } $name = $chunks['host']; if (isset($chunks['username']) && !empty($chunks['username'])) { $name = $chunks['username'].'@'.$name; } if (isset($chunks['port']) && !empty($chunks['port'])) { $port = intval($chunks['port']); if ($port !== 27017) { $name .= ':'.$port; } } if (isset($chunks['database']) && !empty($chunks['database']) && $chunks['database'] != 'admin') { $db = $chunks['database']; $name .= '/'.$db; } $ret = compact('name', 'dsn', 'options'); if (isset($db)) { $ret['db'] = $db; } return $ret; } private function listDbs() { if (isset($this->db)) { return array( 'totalSize' => 0, 'databases' => array(array('name' => $this->db)), ); } return $this->getConnection()->listDBs(); } }
class Genghis_RedirectResponse extends Genghis_Response { public function __construct($url, $status = 301) { parent::__construct($url, $status); } public function render() { header(sprintf('Location: %s', $this->data), $this->status); } }
class Genghis_Response { protected static $statusCodes = array( 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 204 => 'No Content', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 412 => 'Precondition Failed', 415 => 'Unsupported Media Type', 417 => 'Expectation Failed', 500 => 'Internal Server Error', 501 => 'Not Implemented', ); protected $data = ''; protected $status = 200; protected $headers = array(); public function __construct($data, $status = 200, $headers = array()) { $this->data = $data; $this->status = $status; $this->headers = $headers; } public function render() { $this->renderHeaders(); $this->renderContent(); } public static function getStatusText($status) { if (isset(self::$statusCodes[$status])) { return self::$statusCodes[$status]; } } protected function renderHeaders() { header(sprintf('HTTP/1.0 %s %s', $this->status, self::$statusCodes[$this->status])); foreach ($this->headers as $name => $val) { header(sprintf('%s: %s', $name, $val)); } } protected function renderContent() { print((string) $this->data); } }
class Genghis_ServerCollection implements ArrayAccess, Genghis_JsonEncodable { private $serverDsns; private $servers; private $defaultServerDsns; private $defaultServers; public function __construct(array $servers = null, array $defaultServers = null) { $this->serverDsns = $servers; $this->defaultServerDsns = $defaultServers; } public function offsetExists($name) { $this->initDsns(); return isset($this->serverDsns[$name]) || isset($this->defaultServerDsns[$name]); } public function offsetGet($name) { $this->initServers(); if (!isset($this[$name])) { throw new Genghis_HttpException(404, sprintf("Server '%s' not found", $name)); } if (!isset($this->servers[$name])) { if (isset($this->serverDsns[$name])) { $this->servers[$name] = new Genghis_Models_Server($this->serverDsns[$name]); } elseif (isset($this->defaultServerDsns[$name])) { $this->servers[$name] = new Genghis_Models_Server($this->defaultServerDsns[$name], true); } } return $this->servers[$name]; } public function offsetSet($name, $server) { $this->initServers(); if (!$server instanceof Genghis_Models_Server) { throw new Exception('Invalid Server instance'); } if (isset($this->serverDsns[$server->name])) { throw new Genghis_HttpException(400, sprintf("Server '%s' already exists", $server->name)); } $this->serverDsns[$server->name] = $server->dsn; $this->servers[$server->name] = $server; $this->saveServers(); } public function offsetUnset($name) { $this->initServers(); if (!isset($this->servers[$name])) { throw new Genghis_HttpException(404, sprintf("Server '%s' not found", $name)); } unset($this->servers[$name]); $this->saveServers(); } public function asJson() { $this->initServers(); return array_values($this->servers); } private function initDsns() { if (!isset($this->serverDsns)) { $this->serverDsns = array(); if (isset($_COOKIE['genghis_servers']) && $localDsns = $this->decodeJson($_COOKIE['genghis_servers'])) { foreach (array_map(array('Genghis_Models_Server', 'parseDsn'), $localDsns) as $info) { $this->serverDsns[$info['name']] = $info['dsn']; } } } if (!isset($this->defaultServerDsns)) { $this->defaultServerDsns = array(); $defaultDsns = array_merge( isset($_ENV['GENGHIS_SERVERS']) ? explode(';', $_ENV['GENGHIS_SERVERS']) : array(), isset($_SERVER['GENGHIS_SERVERS']) ? explode(';', $_SERVER['GENGHIS_SERVERS']) : array() ); foreach (array_map(array('Genghis_Models_Server', 'parseDsn'), $defaultDsns) as $info) { $this->defaultServerDsns[$info['name']] = $info['dsn']; } } if (empty($this->serverDsns) && empty($this->defaultServerDsns)) { $this[] = new Genghis_Models_Server('localhost:27017'); } } private function initServers() { if (!isset($this->servers)) { $this->servers = array(); $this->initDsns(); foreach (array_merge(array_keys($this->serverDsns), array_keys($this->defaultServerDsns)) as $name) { $this[$name]; } } } private function decodeJson($data) { $json = json_decode($data, true); if ($json === false && trim($data) != '') { throw new Genghis_HttpException(400, 'Malformed document'); } return $json; } private function saveServers() { $servers = array(); foreach ($this->servers as $server) { if (!$server->default) { $servers[] = $server->dsn; } } setcookie('genghis_servers', json_encode($servers), time()+60*60*24*365, '/'); } }
Expand Down
Loading

0 comments on commit d25facf

Please sign in to comment.