PageRenderTime 44ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/framework/php/classes/plants/SystemPlant.php

https://github.com/letolabs/DIY
PHP | 641 lines | 470 code | 28 blank | 143 comment | 81 complexity | dbf55b1613effe94fdc71aa325e240f4 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1
  1. <?php
  2. /**
  3. * SystemPlant deals with any low-level or secure requests that need processing.
  4. * Some things like user logins appear here instead of their more natural homes
  5. * in order to centralize potential security risks.
  6. *
  7. * @package diy.org.cashmusic
  8. * @author CASH Music
  9. * @link http://cashmusic.org/
  10. *
  11. * Copyright (c) 2011, CASH Music
  12. * Licensed under the Affero General Public License version 3.
  13. * See http://www.gnu.org/licenses/agpl-3.0.html
  14. *
  15. **/
  16. class SystemPlant extends PlantBase {
  17. // hard-coded to avoid 0/o, l/1 type confusions on download cards
  18. protected $lock_code_chars = array(
  19. 'all_chars' => array('2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','m','n','p','q','r','s','t','u','v','w','x','y','z'),
  20. 'code_break' => array(2,3,3,4,4,4,5)
  21. );
  22. public function __construct($request_type,$request) {
  23. $this->request_type = 'system';
  24. $this->routing_table = array(
  25. // alphabetical for ease of reading
  26. // first value = target method to call
  27. // second value = allowed request methods (string or array of strings)
  28. 'addlogin' => array('addLogin','direct'),
  29. 'addlockcode' => array('addLockCode','direct'),
  30. 'deletesettings' => array('deleteSettings','direct'),
  31. 'getapicredentials' => array('getAPICredentials','direct'),
  32. 'getlockcodes' => array('getLockCodes','direct'),
  33. 'getsettings' => array('getSettings','direct'),
  34. 'migratedb' => array('doMigrateDB','direct'),
  35. 'redeemlockcode' => array('redeemLockCode',array('direct','get','post')),
  36. 'setapicredentials' => array('setAPICredentials','direct'),
  37. 'setlogincredentials' => array('setLoginCredentials','direct'),
  38. 'setsettings' => array('setSettings','direct'),
  39. 'validateapicredentials' => array('validateAPICredentials','direct'),
  40. 'validatelogin' => array('validateLogin','direct')
  41. );
  42. // get global salt for hashing
  43. $global_settings = parse_ini_file(CASH_PLATFORM_ROOT.'/settings/cashmusic.ini.php');
  44. $this->salt = $global_settings['salt'];
  45. $this->plantPrep($request_type,$request);
  46. }
  47. /**
  48. * Wrapper for CASHData migrateDB call. Currently used for SQLite -> MySQL migrations but any
  49. * from/to should be possible. More tests need to be written for full support.
  50. *
  51. * @return bool
  52. */
  53. protected function doMigrateDB($todriver,$tosettings) {
  54. return $this->db->migrateDB($todriver,$tosettings);
  55. }
  56. /**
  57. * Logins are validated using the email address given with a salted sha256 hash of the given
  58. * password. Blowfish is unavailable to PHP 5.2 (reliably) so we're limited in hashing. The
  59. * system salt is stored in /framework/settings/cashmusic.ini.php outside the database for
  60. * additional security.
  61. *
  62. * In addition to the standard email/pass we also validate against Mozilla's Browser ID standard
  63. * using the browserid_assetion which can be passed in. This works with the CASHSystem Browser ID
  64. * calls to determine a positive login status for the user, get the email address, and compare it
  65. * to the system to return the correct user and login status.
  66. *
  67. * Pass require_admin to only return true for admin-level users. Pass an element_id if you want
  68. * the login analytics to be tied to a specific element.
  69. *
  70. * @return array|false
  71. */protected function validateLogin($address,$password,$require_admin=false,$verified_address=false,$browserid_assertion=false,$element_id=null) {
  72. $login_method = 'internal';
  73. if ($verified_address && !$address) {
  74. // claiming verified without an address? false!
  75. return false;
  76. } else if ((!$address && !$browserid_assertion) && (!$address && !$password)) {
  77. // none of the fancy stuff but you're trying to push through no user/pass? bullshit! false!
  78. return false;
  79. }
  80. if (!$password && !$browserid_assertion) {
  81. return false; // seriously no password? lame.
  82. }
  83. $password_hash = hash_hmac('sha256', $password, $this->salt);
  84. if ($browserid_assertion && !$verified_address) {
  85. $address = CASHSystem::getBrowserIdStatus($browserid_assertion);
  86. if (!$address) {
  87. return false;
  88. } else {
  89. $verified_address = true;
  90. $login_method = 'browserid';
  91. }
  92. }
  93. if ($browserid_assertion && $verified_address) {
  94. $login_method = 'browserid';
  95. }
  96. $result = $this->db->getData(
  97. 'users',
  98. 'id,password,is_admin',
  99. array(
  100. "email_address" => array(
  101. "condition" => "=",
  102. "value" => $address
  103. )
  104. )
  105. );
  106. if ($result && ($password_hash == $result[0]['password'] || $verified_address)) {
  107. if (($require_admin && $result[0]['is_admin']) || !$require_admin) {
  108. $this->recordLoginAnalytics($result[0]['id'],$element_id,$login_method);
  109. return $result[0]['id'];
  110. } else {
  111. return false;
  112. }
  113. } else {
  114. return false;
  115. }
  116. }
  117. /**
  118. * Records the basic login data to the people analytics table
  119. *
  120. * @return boolean
  121. */protected function recordLoginAnalytics($user_id,$element_id=null,$login_method='internal') {
  122. $ip_and_proxy = CASHSystem::getRemoteIP();
  123. $result = $this->db->setData(
  124. 'people_analytics',
  125. array(
  126. 'user_id' => $user_id,
  127. 'element_id' => $element_id,
  128. 'access_time' => time(),
  129. 'client_ip' => $ip_and_proxy['ip'],
  130. 'client_proxy' => $ip_and_proxy['proxy'],
  131. 'login_method' => $login_method
  132. )
  133. );
  134. return $result;
  135. }
  136. /**
  137. * Adds a new user to the system, setting login details
  138. *
  139. * @param {string} $address - the email address in question
  140. * @param {string} $password - the password
  141. * @return array|false
  142. */protected function addLogin($address,$password,$is_admin=0,$display_name='Anonymous',$first_name='',$last_name='',$organization='',$address_country='') {
  143. $password_hash = hash_hmac('sha256', $password, $this->salt);
  144. $result = $this->db->setData(
  145. 'users',
  146. array(
  147. 'email_address' => $address,
  148. 'password' => $password_hash,
  149. 'display_name' => $display_name,
  150. 'first_name' => $first_name,
  151. 'last_name' => $last_name,
  152. 'organization' => $organization,
  153. 'address_country' => $address_country,
  154. 'is_admin' => $is_admin
  155. )
  156. );
  157. if ($result && $is_admin) {
  158. $this->setAPICredentials($result);
  159. }
  160. return $result;
  161. }
  162. /**
  163. * Resets email/password credentials for a user
  164. *
  165. * @param {int} $user_id - the user
  166. * @return array|false
  167. */protected function setLoginCredentials($user_id,$address,$password) {
  168. $password_hash = hash_hmac('sha256', $password, $this->salt);
  169. $credentials = array(
  170. 'email_address' => $address,
  171. 'password' => $password_hash
  172. );
  173. $result = $this->db->setData(
  174. 'users',
  175. $credentials,
  176. array(
  177. "id" => array(
  178. "condition" => "=",
  179. "value" => $user_id
  180. )
  181. )
  182. );
  183. return $result;
  184. }
  185. /**
  186. * Sets or resets API credentials for a user
  187. *
  188. * @param {int} $user_id - the user
  189. * @return array|false
  190. */protected function setAPICredentials($user_id) {
  191. $some_shit = time() . $user_id . rand(976654,1234567267);
  192. $api_key = hash_hmac('md5', $some_shit, $this->salt) . substr((string) time(),6);
  193. $api_secret = hash_hmac('sha256', $some_shit, $this->salt);
  194. $credentials = array(
  195. 'api_key' => $api_key,
  196. 'api_secret' => $api_secret
  197. );
  198. $result = $this->db->setData(
  199. 'users',
  200. $credentials,
  201. array(
  202. "id" => array(
  203. "condition" => "=",
  204. "value" => $user_id
  205. )
  206. )
  207. );
  208. if ($result) {
  209. return $credentials;
  210. } else {
  211. return false;
  212. }
  213. }
  214. /**
  215. * Gets API credentials for a user id
  216. *
  217. * @param {int} $user_id - the user
  218. * @return array|false
  219. */protected function getAPICredentials($user_id) {
  220. $user = $this->db->getData(
  221. 'users',
  222. 'api_key,api_secret',
  223. array(
  224. "id" => array(
  225. "condition" => "=",
  226. "value" => $user_id
  227. )
  228. )
  229. );
  230. if ($user) {
  231. return array(
  232. 'api_key' => $user[0]['api_key'],
  233. 'api_secret' => $user[0]['api_secret']
  234. );
  235. } else {
  236. return false;
  237. }
  238. }
  239. /**
  240. * Verifies API credentials and returns authorization type (api_key || api_fullauth || none) and user_id
  241. *
  242. * @param {int} $user_id - the user
  243. * @return array|false
  244. */protected function validateAPICredentials($api_key,$api_secret=false) {
  245. $user_id = false;
  246. $auth_type = 'none';
  247. if (!$api_secret) {
  248. $auth_type = 'api_key';
  249. $user = $this->db->getData(
  250. 'users',
  251. 'id',
  252. array(
  253. "api_key" => array(
  254. "condition" => "=",
  255. "value" => $api_key
  256. )
  257. )
  258. );
  259. } else {
  260. $auth_type = 'api_fullauth';
  261. $user = $this->db->getData(
  262. 'users',
  263. 'id',
  264. array(
  265. "api_key" => array(
  266. "condition" => "=",
  267. "value" => $api_key
  268. ),
  269. "api_secret" => array(
  270. "condition" => "=",
  271. "value" => $api_secret
  272. ),
  273. )
  274. );
  275. }
  276. if ($user) {
  277. $user_id = $user[0]['id'];
  278. }
  279. if ($user_id) {
  280. return array(
  281. 'auth_type' => $auth_type,
  282. 'user_id' => $user_id
  283. );
  284. } else {
  285. return false;
  286. }
  287. }
  288. /**
  289. * Removes system settings of the given type for a user — be careful with wild cards. (Don't
  290. * use them unless you want to delete all system settings for a user. So, you know, don't.)
  291. *
  292. * @return bool
  293. */
  294. protected function deleteSettings($user_id,$type) {
  295. $result = $this->db->deleteData(
  296. 'settings',
  297. array(
  298. "type" => array(
  299. "condition" => "=",
  300. "value" => $type
  301. ),
  302. "user_id" => array(
  303. "condition" => "=",
  304. "value" => $user_id
  305. )
  306. )
  307. );
  308. return $result;
  309. }
  310. /**
  311. * Gets settings of the given type for a user. Set return_json to true and the system will
  312. * return the stored JSON without decoding.
  313. *
  314. * @return string|array|false
  315. */
  316. protected function getSettings($user_id,$type,$return_json=false) {
  317. $result = $this->db->getData(
  318. 'settings',
  319. '*',
  320. array(
  321. "type" => array(
  322. "condition" => "=",
  323. "value" => $type
  324. ),
  325. "user_id" => array(
  326. "condition" => "=",
  327. "value" => $user_id
  328. )
  329. )
  330. );
  331. if ($result) {
  332. if ($return_json) {
  333. return $result[0];
  334. } else {
  335. return json_decode($result[0]['value'],true);
  336. }
  337. } else {
  338. return false;
  339. }
  340. }
  341. /**
  342. * Sets data for the given type for a user. This is basically a single key/value, so if the type
  343. * already exists this call with overwrite the existing value.
  344. *
  345. * @return bool
  346. */
  347. protected function setSettings($user_id,$type,$value) {
  348. $go = true;
  349. $condition = false;
  350. // first check to see if the user/key combo exists.
  351. // a little inelegant, but necessary for a key/value store
  352. $exists = $this->db->getData(
  353. 'settings',
  354. 'id,value',
  355. array(
  356. "type" => array(
  357. "condition" => "=",
  358. "value" => $type
  359. ),
  360. "user_id" => array(
  361. "condition" => "=",
  362. "value" => $user_id
  363. )
  364. )
  365. );
  366. if ($exists) {
  367. // the key/user exists, so first compare value
  368. if ($exists[0]['value'] == $value) {
  369. // equal to what's there already? do nothing, return true
  370. $go = false;
  371. } else {
  372. // different? set conditions to perform an update
  373. $condition = array(
  374. "id" => array(
  375. "condition" => "=",
  376. "value" => $exists[0]['id']
  377. )
  378. );
  379. }
  380. }
  381. if ($go) {
  382. // insert/update
  383. $result = $this->db->setData(
  384. 'settings',
  385. array(
  386. 'user_id' => $user_id,
  387. 'type' => $type,
  388. 'value' => json_encode($value)
  389. ),
  390. $condition
  391. );
  392. return $result;
  393. } else {
  394. // we're already up to date...do nothing but signal 'okay'
  395. return true;
  396. }
  397. }
  398. /*
  399. *
  400. * Here lie a bunch of lock code functions that need to reference elements
  401. * instead of assets. duh.
  402. *
  403. */
  404. /**
  405. * Retrieves the last known UID or if none are found creates and returns a
  406. * random UID as a starting point
  407. *
  408. * @return string
  409. */protected function getLastLockCode() {
  410. $result = $this->db->getData(
  411. 'lock_codes',
  412. 'uid',
  413. false,
  414. 1,
  415. 'id DESC'
  416. );
  417. if ($result) {
  418. $code = $result[0]['uid'];
  419. } else {
  420. $code = false;
  421. }
  422. return $code;
  423. }
  424. /**
  425. * Creates a new lock/unlock code for and asset
  426. *
  427. * @param {integer} $element_id - the element for which you're adding the lock code
  428. * @return string|false
  429. */protected function addLockCode($scope_table_alias,$scope_table_id,$user_id=0){
  430. $code = $this->generateCode(
  431. $this->lock_code_chars['all_chars'],
  432. $this->lock_code_chars['code_break'],
  433. $this->getLastLockCode()
  434. );
  435. $result = $this->db->setData(
  436. 'lock_codes',
  437. array(
  438. 'uid' => $code,
  439. 'scope_table_alias' => $scope_table_alias,
  440. 'scope_table_id' => $scope_table_id,
  441. 'user_id' => $user_id
  442. )
  443. );
  444. if ($result) {
  445. return $code;
  446. } else {
  447. return false;
  448. }
  449. }
  450. /**
  451. * Attempts to redeem a given lock code, returning all details for the code on success or false
  452. * on failure. The code is tied to a scope_table_alias and scope_table_id pointing to a specific
  453. * asset, element, etc.
  454. *
  455. * Pass a specific scope_table_alias, scope_table_id, or user_id to limit results to only matching
  456. * returns.
  457. *
  458. * This will continue to return true for four hours after initial redemption — in the case of a
  459. * failed download this will give a user a second try without risking any long-term breach.
  460. *
  461. * @return array|false
  462. */
  463. protected function redeemLockCode($code,$scope_table_alias=false,$scope_table_id=false,$user_id=false) {
  464. $code_details = $this->getLockCode($code);
  465. if ($code_details) {
  466. // check against optional arguments — if they're found then make sure they match
  467. // the data stored with the code...if not invalidate the request and return false
  468. $proceed = true;
  469. if ($scope_table_alias && ($scope_table_alias != $code_details['scope_table_alias'])) {
  470. $proceed = false;
  471. }
  472. if ($scope_table_id && ($scope_table_id != $code_details['scope_table_id'])) {
  473. $proceed = false;
  474. }
  475. if ($user_id && ($user_id != $code_details['user_id'])) {
  476. $proceed = false;
  477. }
  478. if ($proceed) {
  479. // details found
  480. if (!$code_details['claim_date']) {
  481. $result = $this->db->setData(
  482. 'lock_codes',
  483. array(
  484. 'claim_date' => time()
  485. ),
  486. array(
  487. "id" => array(
  488. "condition" => "=",
  489. "value" => $code_details['id']
  490. )
  491. )
  492. );
  493. if ($result) {
  494. return $code_details;
  495. } else {
  496. return false;
  497. }
  498. } else {
  499. // allow retries for four hours after claim
  500. if (($code_details['claim_date'] + 14400) > time()) {
  501. return $code_details;
  502. } else {
  503. return false;
  504. }
  505. }
  506. }
  507. } else {
  508. return false;
  509. }
  510. }
  511. /**
  512. * Returns all data for a given code. Look for "scope_table_alias" and "scope_table_id" in the
  513. * returned aray to find the asset / element / etc that was unlocked with the code.
  514. *
  515. * @return array|false
  516. */
  517. protected function getLockCode($code) {
  518. $result = $this->db->getData(
  519. 'lock_codes',
  520. '*',
  521. array(
  522. "uid" => array(
  523. "condition" => "=",
  524. "value" => $code
  525. )
  526. ),
  527. 1
  528. );
  529. if ($result) {
  530. return $result[0];
  531. } else {
  532. return false;
  533. }
  534. }
  535. /**
  536. * Gets all lock codes for a given resource.
  537. *
  538. * @return array|false
  539. */
  540. protected function getLockCodes($scope_table_alias,$scope_table_id) {
  541. $result = $this->db->getData(
  542. 'lock_codes',
  543. '*',
  544. array(
  545. "scope_table_alias" => array(
  546. "condition" => "=",
  547. "value" => $scope_table_alias
  548. ),
  549. "scope_table_id" => array(
  550. "condition" => "=",
  551. "value" => $scope_table_id
  552. )
  553. )
  554. );
  555. return $result;
  556. }
  557. protected function consistentShuffle(&$items, $seed=false) {
  558. // original here: http://www.php.net/manual/en/function.shuffle.php#105931
  559. $original = md5(serialize($items));
  560. mt_srand(crc32(($seed) ? $seed : $items[0]));
  561. for ($i = count($items) - 1; $i > 0; $i--){
  562. $j = @mt_rand(0, $i);
  563. list($items[$i], $items[$j]) = array($items[$j], $items[$i]);
  564. }
  565. if ($original == md5(serialize($items))) {
  566. list($items[count($items) - 1], $items[0]) = array($items[0], $items[count($items) - 1]);
  567. }
  568. }
  569. protected function generateCode($all_chars,$code_break,$last_code=false) {
  570. $seed = CASHSystem::getSystemSalt();
  571. $this->consistentShuffle($all_chars,$seed);
  572. $this->consistentShuffle($code_break,$seed);
  573. if (!$last_code) {
  574. $last_code = '';
  575. for ($i = 1; $i <= 10; $i++) {
  576. $last_code .= $all_chars[rand(0,count($all_chars) - 1)];
  577. }
  578. }
  579. $sequential = substr($last_code,1,$code_break[0])
  580. . substr($last_code,0 - (7 - $code_break[0]));
  581. $sequential = $this->iterateChars($sequential,$all_chars);
  582. $new_code = $all_chars[rand(0,count($all_chars) - 1)]
  583. . substr($sequential,0,$code_break[0])
  584. . $all_chars[rand(0,count($all_chars) - 1)]
  585. . $all_chars[rand(0,count($all_chars) - 1)]
  586. . substr($sequential,0 - (7 - $code_break[0]));
  587. return $new_code;
  588. }
  589. protected function iterateChars($chars,$all_chars) {
  590. $chars = str_split($chars);
  591. // start with the last character of the $chars string
  592. $current_char = count($chars) - 1;
  593. $loop = 1;
  594. do {
  595. $loop--;
  596. $current_key = array_search($chars[$current_char],$all_chars);
  597. if ($current_key == count($all_chars) - 1) {
  598. $loop++;
  599. $chars[$current_char] = $all_chars[0];
  600. if ($current_char == 0) {
  601. $current_char = count($chars) - 1;
  602. } else {
  603. $current_char--;
  604. }
  605. } else {
  606. $chars[$current_char] = $all_chars[$current_key + 1];
  607. }
  608. } while ($loop > 0);
  609. $chars = implode($chars);
  610. return $chars;
  611. }
  612. } // END class
  613. ?>