PageRenderTime 45ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/Auth/OpenID/SQLStore.php

http://github.com/openid/php-openid
PHP | 595 lines | 361 code | 73 blank | 161 comment | 57 complexity | 11cab72d37f732102b75050c99ebbf26 MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /**
  3. * SQL-backed OpenID stores.
  4. *
  5. * PHP versions 4 and 5
  6. *
  7. * LICENSE: See the COPYING file included in this distribution.
  8. *
  9. * @package OpenID
  10. * @author JanRain, Inc. <openid@janrain.com>
  11. * @copyright 2005-2008 Janrain, Inc.
  12. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
  13. */
  14. /**
  15. * @access private
  16. */
  17. require_once 'Auth/OpenID/Interface.php';
  18. require_once 'Auth/OpenID/Nonce.php';
  19. /**
  20. * @access private
  21. */
  22. require_once 'Auth/OpenID.php';
  23. /**
  24. * @access private
  25. */
  26. require_once 'Auth/OpenID/Nonce.php';
  27. /**
  28. * This is the parent class for the SQL stores, which contains the
  29. * logic common to all of the SQL stores.
  30. *
  31. * The table names used are determined by the class variables
  32. * associations_table_name and nonces_table_name. To change the name
  33. * of the tables used, pass new table names into the constructor.
  34. *
  35. * To create the tables with the proper schema, see the createTables
  36. * method.
  37. *
  38. * This class shouldn't be used directly. Use one of its subclasses
  39. * instead, as those contain the code necessary to use a specific
  40. * database. If you're an OpenID integrator and you'd like to create
  41. * an SQL-driven store that wraps an application's database
  42. * abstraction, be sure to create a subclass of
  43. * {@link Auth_OpenID_DatabaseConnection} that calls the application's
  44. * database abstraction calls. Then, pass an instance of your new
  45. * database connection class to your SQLStore subclass constructor.
  46. *
  47. * All methods other than the constructor and createTables should be
  48. * considered implementation details.
  49. *
  50. * @package OpenID
  51. */
  52. class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore {
  53. /** @var string */
  54. protected $associations_table_name = '';
  55. /** @var string */
  56. protected $nonces_table_name = '';
  57. /** @var Auth_OpenID_DatabaseConnection|db_common */
  58. protected $connection;
  59. /** @var int */
  60. protected $max_nonce_age = 0;
  61. /** @var array */
  62. protected $sql = [];
  63. /**
  64. * This creates a new SQLStore instance. It requires an
  65. * established database connection be given to it, and it allows
  66. * overriding the default table names.
  67. *
  68. * @param Auth_OpenID_DatabaseConnection $connection This must be an established
  69. * connection to a database of the correct type for the SQLStore
  70. * subclass you're using. This must either be an PEAR DB
  71. * connection handle or an instance of a subclass of
  72. * Auth_OpenID_DatabaseConnection.
  73. *
  74. * @param associations_table: This is an optional parameter to
  75. * specify the name of the table used for storing associations.
  76. * The default value is 'oid_associations'.
  77. *
  78. * @param nonces_table: This is an optional parameter to specify
  79. * the name of the table used for storing nonces. The default
  80. * value is 'oid_nonces'.
  81. */
  82. function __construct($connection, $associations_table = null, $nonces_table = null)
  83. {
  84. $this->associations_table_name = "oid_associations";
  85. $this->nonces_table_name = "oid_nonces";
  86. // Check the connection object type to be sure it's a PEAR
  87. // database connection.
  88. if (!(is_object($connection) &&
  89. (is_subclass_of($connection, 'db_common') ||
  90. is_subclass_of($connection,
  91. 'auth_openid_databaseconnection')))) {
  92. trigger_error("Auth_OpenID_SQLStore expected PEAR connection " .
  93. "object (got ".get_class($connection).")",
  94. E_USER_ERROR);
  95. return;
  96. }
  97. $this->connection = $connection;
  98. // Be sure to set the fetch mode so the results are keyed on
  99. // column name instead of column index. This is a PEAR
  100. // constant, so only try to use it if PEAR is present. Note
  101. // that Auth_Openid_Databaseconnection instances need not
  102. // implement ::setFetchMode for this reason.
  103. if (is_subclass_of($this->connection, 'db_common')) {
  104. $this->connection->setFetchMode(DB_FETCHMODE_ASSOC);
  105. }
  106. if ($associations_table) {
  107. $this->associations_table_name = $associations_table;
  108. }
  109. if ($nonces_table) {
  110. $this->nonces_table_name = $nonces_table;
  111. }
  112. $this->max_nonce_age = 6 * 60 * 60;
  113. // Be sure to run the database queries with auto-commit mode
  114. // turned OFF, because we want every function to run in a
  115. // transaction, implicitly. As a rule, methods named with a
  116. // leading underscore will NOT control transaction behavior.
  117. // Callers of these methods will worry about transactions.
  118. $this->connection->autoCommit(false);
  119. // Create an empty SQL strings array.
  120. $this->sql = [];
  121. // Call this method (which should be overridden by subclasses)
  122. // to populate the $this->sql array with SQL strings.
  123. $this->setSQL();
  124. // Verify that all required SQL statements have been set, and
  125. // raise an error if any expected SQL strings were either
  126. // absent or empty.
  127. list($missing, $empty) = $this->_verifySQL();
  128. if ($missing) {
  129. trigger_error("Expected keys in SQL query list: " .
  130. implode(", ", $missing),
  131. E_USER_ERROR);
  132. return;
  133. }
  134. if ($empty) {
  135. trigger_error("SQL list keys have no SQL strings: " .
  136. implode(", ", $empty),
  137. E_USER_ERROR);
  138. return;
  139. }
  140. // Add table names to queries.
  141. $this->_fixSQL();
  142. }
  143. function tableExists($table_name)
  144. {
  145. return !$this->isError(
  146. $this->connection->query(
  147. sprintf("SELECT * FROM %s LIMIT 0",
  148. $table_name)));
  149. }
  150. /**
  151. * Returns true if $value constitutes a database error; returns
  152. * false otherwise.
  153. */
  154. function isError($value)
  155. {
  156. return @PEAR::isError($value);
  157. }
  158. /**
  159. * Converts a query result to a boolean. If the result is a
  160. * database error according to $this->isError(), this returns
  161. * false; otherwise, this returns true.
  162. */
  163. function resultToBool($obj)
  164. {
  165. if ($this->isError($obj)) {
  166. return false;
  167. } else {
  168. return true;
  169. }
  170. }
  171. /**
  172. * This method should be overridden by subclasses. This method is
  173. * called by the constructor to set values in $this->sql, which is
  174. * an array keyed on sql name.
  175. */
  176. function setSQL()
  177. {
  178. }
  179. /**
  180. * Resets the store by removing all records from the store's
  181. * tables.
  182. */
  183. function reset()
  184. {
  185. $this->connection->query(sprintf("DELETE FROM %s",
  186. $this->associations_table_name));
  187. $this->connection->query(sprintf("DELETE FROM %s",
  188. $this->nonces_table_name));
  189. }
  190. /**
  191. * @access private
  192. */
  193. function _verifySQL()
  194. {
  195. $missing = [];
  196. $empty = [];
  197. $required_sql_keys = [
  198. 'nonce_table',
  199. 'assoc_table',
  200. 'set_assoc',
  201. 'get_assoc',
  202. 'get_assocs',
  203. 'remove_assoc',
  204. ];
  205. foreach ($required_sql_keys as $key) {
  206. if (!array_key_exists($key, $this->sql)) {
  207. $missing[] = $key;
  208. } else if (!$this->sql[$key]) {
  209. $empty[] = $key;
  210. }
  211. }
  212. return [$missing, $empty];
  213. }
  214. /**
  215. * @access private
  216. */
  217. function _fixSQL()
  218. {
  219. $replacements = [
  220. [
  221. 'value' => $this->nonces_table_name,
  222. 'keys' => [
  223. 'nonce_table',
  224. 'add_nonce',
  225. 'clean_nonce',
  226. ],
  227. ],
  228. [
  229. 'value' => $this->associations_table_name,
  230. 'keys' => [
  231. 'assoc_table',
  232. 'set_assoc',
  233. 'get_assoc',
  234. 'get_assocs',
  235. 'remove_assoc',
  236. 'clean_assoc',
  237. ],
  238. ],
  239. ];
  240. foreach ($replacements as $item) {
  241. $value = $item['value'];
  242. $keys = $item['keys'];
  243. foreach ($keys as $k) {
  244. if (is_array($this->sql[$k])) {
  245. foreach ($this->sql[$k] as $part_key => $part_value) {
  246. $this->sql[$k][$part_key] = sprintf($part_value, $value);
  247. }
  248. } else {
  249. $this->sql[$k] = sprintf($this->sql[$k], $value);
  250. }
  251. }
  252. }
  253. }
  254. function blobDecode($blob)
  255. {
  256. return $blob;
  257. }
  258. function blobEncode($str)
  259. {
  260. return $str;
  261. }
  262. function createTables()
  263. {
  264. $this->connection->autoCommit(true);
  265. $n = $this->create_nonce_table();
  266. $a = $this->create_assoc_table();
  267. $this->connection->autoCommit(false);
  268. if ($n && $a) {
  269. return true;
  270. } else {
  271. return false;
  272. }
  273. }
  274. function create_nonce_table()
  275. {
  276. if (!$this->tableExists($this->nonces_table_name)) {
  277. $r = $this->connection->query($this->sql['nonce_table']);
  278. return $this->resultToBool($r);
  279. }
  280. return true;
  281. }
  282. function create_assoc_table()
  283. {
  284. if (!$this->tableExists($this->associations_table_name)) {
  285. $r = $this->connection->query($this->sql['assoc_table']);
  286. return $this->resultToBool($r);
  287. }
  288. return true;
  289. }
  290. /**
  291. * @access private
  292. * @param string $server_url
  293. * @param int $handle
  294. * @param string $secret
  295. * @param string $issued
  296. * @param int $lifetime
  297. * @param string $assoc_type
  298. * @return mixed
  299. */
  300. function _set_assoc($server_url, $handle, $secret, $issued,
  301. $lifetime, $assoc_type)
  302. {
  303. return $this->connection->query($this->sql['set_assoc'],
  304. [
  305. $server_url,
  306. $handle,
  307. $secret,
  308. $issued,
  309. $lifetime,
  310. $assoc_type,
  311. ]);
  312. }
  313. function storeAssociation($server_url, $association)
  314. {
  315. if ($this->resultToBool($this->_set_assoc(
  316. $server_url,
  317. $association->handle,
  318. $this->blobEncode(
  319. $association->secret),
  320. $association->issued,
  321. $association->lifetime,
  322. $association->assoc_type
  323. ))) {
  324. $this->connection->commit();
  325. } else {
  326. $this->connection->rollback();
  327. }
  328. }
  329. /**
  330. * @access private
  331. * @param string $server_url
  332. * @param int $handle
  333. * @return array|bool|null
  334. */
  335. function _get_assoc($server_url, $handle)
  336. {
  337. $result = $this->connection->getRow($this->sql['get_assoc'],
  338. [$server_url, $handle]);
  339. if ($this->isError($result)) {
  340. return null;
  341. } else {
  342. return $result;
  343. }
  344. }
  345. /**
  346. * @access private
  347. * @param string $server_url
  348. * @return array
  349. */
  350. function _get_assocs($server_url)
  351. {
  352. $result = $this->connection->getAll($this->sql['get_assocs'],
  353. [$server_url]);
  354. if ($this->isError($result)) {
  355. return [];
  356. } else {
  357. return $result;
  358. }
  359. }
  360. function removeAssociation($server_url, $handle)
  361. {
  362. if ($this->_get_assoc($server_url, $handle) == null) {
  363. return false;
  364. }
  365. if ($this->resultToBool($this->connection->query(
  366. $this->sql['remove_assoc'],
  367. [$server_url, $handle]))) {
  368. $this->connection->commit();
  369. } else {
  370. $this->connection->rollback();
  371. }
  372. return true;
  373. }
  374. function getAssociation($server_url, $handle = null)
  375. {
  376. if ($handle !== null) {
  377. $assoc = $this->_get_assoc($server_url, $handle);
  378. $assocs = [];
  379. if ($assoc) {
  380. $assocs[] = $assoc;
  381. }
  382. } else {
  383. $assocs = $this->_get_assocs($server_url);
  384. }
  385. if (!$assocs || (count($assocs) == 0)) {
  386. return null;
  387. } else {
  388. $associations = [];
  389. foreach ($assocs as $assoc_row) {
  390. $assoc = new Auth_OpenID_Association($assoc_row['handle'],
  391. $assoc_row['secret'],
  392. $assoc_row['issued'],
  393. $assoc_row['lifetime'],
  394. $assoc_row['assoc_type']);
  395. $assoc->secret = $this->blobDecode($assoc->secret);
  396. if ($assoc->getExpiresIn() == 0) {
  397. $this->removeAssociation($server_url, $assoc->handle);
  398. } else {
  399. $associations[] = [$assoc->issued, $assoc];
  400. }
  401. }
  402. if ($associations) {
  403. $issued = [];
  404. $assocs = [];
  405. foreach ($associations as $key => $assoc) {
  406. $issued[$key] = $assoc[0];
  407. $assocs[$key] = $assoc[1];
  408. }
  409. array_multisort($issued, SORT_DESC, $assocs, SORT_DESC,
  410. $associations);
  411. // return the most recently issued one.
  412. list(, $assoc) = $associations[0];
  413. return $assoc;
  414. } else {
  415. return null;
  416. }
  417. }
  418. }
  419. /**
  420. * @access private
  421. * @param string $server_url
  422. * @param int $timestamp
  423. * @param string $salt
  424. * @return bool
  425. */
  426. function _add_nonce($server_url, $timestamp, $salt)
  427. {
  428. $sql = $this->sql['add_nonce'];
  429. $result = $this->connection->query($sql, [
  430. $server_url,
  431. $timestamp,
  432. $salt,
  433. ]);
  434. if ($this->isError($result)) {
  435. $this->connection->rollback();
  436. } else {
  437. $this->connection->commit();
  438. }
  439. return $this->resultToBool($result);
  440. }
  441. function useNonce($server_url, $timestamp, $salt)
  442. {
  443. global $Auth_OpenID_SKEW;
  444. if ( abs($timestamp - time()) > $Auth_OpenID_SKEW ) {
  445. return false;
  446. }
  447. return $this->_add_nonce($server_url, $timestamp, $salt);
  448. }
  449. /**
  450. * "Octifies" a binary string by returning a string with escaped
  451. * octal bytes. This is used for preparing binary data for
  452. * PostgreSQL BYTEA fields.
  453. *
  454. * @access private
  455. * @param string $str
  456. * @return string
  457. */
  458. function _octify($str)
  459. {
  460. $result = "";
  461. for ($i = 0; $i < Auth_OpenID::bytes($str); $i++) {
  462. $ch = substr($str, $i, 1);
  463. if ($ch == "\\") {
  464. $result .= "\\\\\\\\";
  465. } else if (ord($ch) == 0) {
  466. $result .= "\\\\000";
  467. } else {
  468. $result .= "\\" . strval(decoct(ord($ch)));
  469. }
  470. }
  471. return $result;
  472. }
  473. /**
  474. * "Unoctifies" octal-escaped data from PostgreSQL and returns the
  475. * resulting ASCII (possibly binary) string.
  476. *
  477. * @access private
  478. * @param string $str
  479. * @return string
  480. */
  481. function _unoctify($str)
  482. {
  483. $result = "";
  484. $i = 0;
  485. while ($i < strlen($str)) {
  486. $char = $str[$i];
  487. if ($char == "\\") {
  488. // Look to see if the next char is a backslash and
  489. // append it.
  490. if ($str[$i + 1] != "\\") {
  491. $octal_digits = substr($str, $i + 1, 3);
  492. $dec = octdec($octal_digits);
  493. $char = chr($dec);
  494. $i += 4;
  495. } else {
  496. $char = "\\";
  497. $i += 2;
  498. }
  499. } else {
  500. $i += 1;
  501. }
  502. $result .= $char;
  503. }
  504. return $result;
  505. }
  506. function cleanupNonces()
  507. {
  508. global $Auth_OpenID_SKEW;
  509. $v = time() - $Auth_OpenID_SKEW;
  510. $this->connection->query($this->sql['clean_nonce'], [$v]);
  511. $num = $this->connection->affectedRows();
  512. $this->connection->commit();
  513. return $num;
  514. }
  515. function cleanupAssociations()
  516. {
  517. $this->connection->query($this->sql['clean_assoc'], [time()]);
  518. $num = $this->connection->affectedRows();
  519. $this->connection->commit();
  520. return $num;
  521. }
  522. }