PageRenderTime 64ms CodeModel.GetById 31ms RepoModel.GetById 1ms app.codeStats 0ms

/library/Adapto/Relation/Secure.php

http://github.com/egeniq/adapto
PHP | 513 lines | 340 code | 35 blank | 138 comment | 62 complexity | 99bf3584221a209318a8ac593f1dd00f MD5 | raw file
  1. <?php
  2. /**
  3. * This file is part of the Adapto Toolkit.
  4. * Detailed copyright and licensing information can be found
  5. * in the doc/COPYRIGHT and doc/LICENSE files which should be
  6. * included in the distribution.
  7. *
  8. * @package adapto
  9. * @subpackage relations
  10. *
  11. * @copyright (c)2000-2004 Ibuildings.nl BV
  12. * @license http://www.achievo.org/atk/licensing ATK Open Source License
  13. *
  14. */
  15. userelation("atkonetoonerelation");
  16. /**
  17. * Relationship that can link 2 tables based on a secure link
  18. * that can not be decrypted when not logged in through an atk
  19. * application.
  20. * This effectively secures the database so that data in two
  21. * tables can not be correlated by mischievous access to the database.
  22. *
  23. * @author Mark Baaijens <mark@ibuildings.nl>
  24. *
  25. * @package adapto
  26. * @subpackage relations
  27. *
  28. */
  29. class Adapto_Relation_Secure extends Adapto_OneToOneRelation
  30. {
  31. public $m_crypt = NULL; // defaulted to public
  32. public $m_linktable; // defaulted to public
  33. public $m_linkfield; // defaulted to public
  34. public $m_linkuserfield = "username"; // defaulted to public
  35. public $m_keylength; // defaulted to public
  36. public $m_searching = false; // defaulted to public
  37. public $m_keylookup = array(); // defaulted to public
  38. public $m_records = array(); // defaulted to public
  39. public $m_searcharray = array(); // defaulted to public
  40. public $m_linkpass; // defaulted to public
  41. public $m_linkbackfield; // defaulted to public
  42. public $m_ownersearch; // defaulted to public
  43. public $m_destsearch; // defaulted to public
  44. public $m_cachefield; // defaulted to public
  45. /**
  46. * Creates an Adapto_Relation_Secure,
  47. * similar to an atkOneToOne relation only encrypted
  48. *
  49. * @param string $name The unique name of the attribute. In slave
  50. * mode, this corresponds to the foreign key
  51. * field in the database table.
  52. * @param string $destination The destination entity (in module.entityname
  53. * notation)
  54. * @param string $linktable The table we link to
  55. * @param string $linkfield The field we link to
  56. * @param string $linkbackfield
  57. * @param int $keylength The length of the encryption key
  58. * @param string $refKey="" In master mode, this specifies the foreign
  59. * key field from the destination entity that
  60. * points to the master record. In slave mode,
  61. * this parameter should be empty.
  62. * @param string $encryption The encryption to use
  63. * @param int $flags Attribute flags that influence this
  64. * attributes' behavior.
  65. */
  66. public function __construct($name, $destination, $linktable, $linkfield, $linkbackfield, $keylength, $refKey = "", $encryption, $flags = 0)
  67. {
  68. parent::__construct($name, $destination, $refKey, $flags | AF_ONETOONE_ERROR);
  69. $this->createDestination();
  70. $this->m_crypt = &atkEncryption::getEncryption($encryption);
  71. $this->m_linktable = $linktable;
  72. $this->m_linkfield = $linkfield;
  73. $this->m_keylength = $keylength;
  74. $this->m_linkbackfield = $linkbackfield;
  75. }
  76. /**
  77. * Set the name of the cache field
  78. *
  79. * @param string $fieldname The cache fieldname
  80. */
  81. function setCacheField($fieldname = "cache")
  82. {
  83. $this->m_cachefield = $fieldname;
  84. }
  85. /**
  86. * Adds the attribute / field to the list header. This includes the column name and search field.
  87. *
  88. * Framework method. It should not be necessary to call this method directly.
  89. *
  90. * @param String $action the action that is being performed on the entity
  91. * @param array $arr reference to the the recordlist array
  92. * @param String $fieldprefix the fieldprefix
  93. * @param int $flags the recordlist flags
  94. * @param array $atksearch the current ATK search list (if not empty)
  95. * @param String $atkorderby the current ATK orderby string (if not empty)
  96. */
  97. function addToListArrayHeader($action, &$arr, $fieldprefix, $flags, $atksearch, $atkorderby)
  98. {
  99. if ($this->hasFlag(AF_ONETOONE_INTEGRATE)) {
  100. // integrated version, don't add ourselves, but add all columns from the destination.
  101. if ($this->createDestination()) {
  102. foreach (array_keys($this->m_destInstance->m_attribList) as $attribname) {
  103. $p_attrib = &$this->m_destInstance->getAttribute($attribname);
  104. $p_attrib->addFlag(AF_NO_SORT);
  105. }
  106. }
  107. }
  108. parent::addToListArrayHeader($action, $arr, $fieldprefix, $flags, $atksearch, $atkorderby);
  109. }
  110. /**
  111. * Gets the password for the link
  112. * for more security the administrator gets a random password. You have to capture in your application that
  113. * the administrator is only able to insert the first record in this relation and make also a useracount with it.
  114. * @param string $linktable the table where we find the linkpass
  115. * @param string $linkfield the field where we find the encrypted linkpass
  116. * @param string $encryption The encryption to use
  117. * @return string the password for the link
  118. */
  119. function getLinkPassword($linktable, $linkfield, $encryption = "")
  120. {
  121. if ($this->m_linkpass)
  122. return $this->m_linkpass;
  123. if (!$linktable)
  124. $linktable = $this->m_linktable;
  125. if (!$linkfield)
  126. $linkfield = $this->m_linkfield;
  127. $user = getUser();
  128. $username = $user['name'];
  129. $password = $user['PASS'];
  130. if ($encryption)
  131. $crypt = atkEncryption::getEncryption($encryption);
  132. else
  133. $crypt = $this->m_crypt;
  134. if ($username == "administrator") {
  135. //if the administrator asks for a password we generate one
  136. //because the administrator only makes the first person
  137. global $linkpass;
  138. if (!$linkpass)
  139. $linkpass = $crypt->getRandomKey($password);
  140. } else {
  141. $query = "SELECT " . $linkfield . " as pass FROM " . $linktable . " WHERE " . Adapto_Config::getGlobal("auth_userfield") . " = '" . $username . "'";
  142. $db = &atkGetDb();
  143. $rec = $db->getrows($query);
  144. if (count($rec) < 1)
  145. return $linkpass;
  146. $encryptedpass = array_pop($rec);
  147. $linkpass = $encryptedpass['pass'];
  148. }
  149. $this->m_linkpass = $crypt->decryptKey($linkpass, $password);
  150. return $this->m_linkpass;
  151. }
  152. /**
  153. * This function in the atkOneToOneRelation store the record of the parententity in the DB
  154. * with the reference key of the other table.
  155. * So we encrypt the reference key before we call the method.
  156. * For more documentation see the atkOneToOneRelation
  157. *
  158. * @param atkQuery $query The SQL query object
  159. * @param String $tablename The name of the table of this attribute
  160. * @param String $fieldaliasprefix Prefix to use in front of the alias
  161. * in the query.
  162. * @param Array $rec The record that contains the value of this attribute.
  163. * @param int $level Recursion level if relations point to eachother, an
  164. * endless loop could occur if they keep loading
  165. * eachothers data. The $level is used to detect this
  166. * loop. If overriden in a derived class, any subcall to
  167. * an addToQuery method should pass the $level+1.
  168. * @param String $mode Indicates what kind of query is being processing:
  169. * This can be any action performed on an entity (edit,
  170. * add, etc) Mind you that "add" and "update" are the
  171. * actions that store something in the database,
  172. * whereas the rest are probably select queries.
  173. */
  174. function addToQuery(&$query, $tablename = "", $fieldaliasprefix = "", $rec = "", $level = 0, $mode = "")
  175. {
  176. $records = $this->m_records;
  177. if (count($records) == 0 && !$this->m_searching) {
  178. if (is_array($rec)) {
  179. $link = $rec[$this->fieldName()][$this->m_destInstance->m_primaryKey[0]];
  180. $cryptedlink = $this->m_crypt->encrypt($link, $this->getLinkPassword($this->m_linktable, $this->m_linkfield));
  181. $rec[$this->fieldName()][$this->m_destInstance->m_primaryKey[0]] = addslashes($cryptedlink);
  182. }
  183. return parent::addToQuery($query, $tablename, $fieldaliasprefix, $rec, $level, $mode);
  184. } else // lookup matching
  185. {
  186. $where = array();
  187. foreach (array_keys($this->m_keylookup) as $decryptedlink) {
  188. $where[] = $decryptedlink;
  189. }
  190. if ($tablename)
  191. $tablename .= ".";
  192. $query->addSearchCondition($tablename . $this->m_ownerInstance->primaryKeyField() . " IN ('" . implode("','", $where) . "')");
  193. }
  194. }
  195. /**
  196. * This function in the atkOneToOneRelation loads the record of the childentity from the DB
  197. * with the the id from de reference key in childentity.
  198. * So we decrypt the reference key before we call the method.
  199. * For more documentation see the atkOneToOneRelation
  200. *
  201. * @param atkDb $db The database object
  202. * @param array $record The record
  203. * @param string $mode The mode we're in ("add", "edit", "copy")
  204. * @return array The loaded records
  205. */
  206. function load(&$db, $record, $mode)
  207. {
  208. if ($this->m_searching) {
  209. if ($this->m_searcharray !== $this->m_ownerInstance->m_postvars["atksearch"][$this->fieldName()] && is_array($this->m_searcharray)) {
  210. $this->m_records = array();
  211. $this->m_keylookup = array();
  212. // perform query on destination entity to retrieve all records.
  213. if ($this->createDestination()) {
  214. $this->m_searcharray = $this->m_ownerInstance->m_postvars["atksearch"][$this->fieldName()];
  215. //if al search values are equal, then make it an OR search
  216. if (count(array_unique(array_values($this->m_searcharray))) == 1)
  217. $this->m_destInstance->m_postvars['atksearchmethod'] = "OR";
  218. $oldsearcharray = $this->m_searcharray;
  219. // check wether mentioned fields are actually in the entity
  220. foreach ($this->m_searcharray as $searchfield => $searchvalue) {
  221. if (!is_object($this->m_destInstance->m_attribList[$searchfield]))
  222. unset($this->m_searcharray[$searchfield]);
  223. }
  224. $this->m_destInstance->m_postvars["atksearch"] = $this->m_searcharray;
  225. $this->m_destInstance->m_postvars["atksearchmode"] = $this->m_ownerInstance->m_postvars["atksearchmode"];
  226. $this->m_destInstance->m_postvars["atksearchmethod"] = $this->m_ownerInstance->m_postvars["atksearchmethod"];
  227. $records = $this->m_destInstance->selectDb();
  228. $this->m_searcharray = $oldsearcharray;
  229. $errorconfig = Adapto_Config::getGlobal("securerelation_decrypterror", null);
  230. // create lookup table for easy reference.
  231. for ($i = 0, $_i = count($records); $i < $_i; $i++) {
  232. $decryptedlink = $this->decrypt($records[$i], $this->m_linkbackfield);
  233. if (!$decryptedlink && $errorconfig) {
  234. Adapto_Util_Debugger::debug(
  235. "Unable to decrypt link: " . $link . "for record: " . var_export($records[$i], true) . " with linkbackfield: "
  236. . $this->m_linkbackfield);
  237. $decrypterror = true;
  238. } else {
  239. $this->m_keylookup[$decryptedlink] = $i;
  240. $this->m_records[] = $records[$i];
  241. }
  242. }
  243. if ($decrypterror) {
  244. if ($errorconfig == 2)
  245. throw new Adapto_Exception("There were errors decrypting the secured links, see debuginfo");
  246. else if ($errorconfig == 1)
  247. mailreport();
  248. }
  249. return $this->m_records;
  250. }
  251. } else // lookup table present, postload stage
  252. {
  253. $this->m_searching = false;
  254. return $this->m_records[$this->m_keylookup[$record[$this->m_ownerInstance->primaryKeyField()]]];
  255. }
  256. } else {
  257. if (!$record[$this->fieldName()] || (!$record[$this->m_cachefield] && $this->m_cachefield)) {
  258. $query = "SELECT " . $this->fieldName();
  259. $query .= ($this->m_cachefield ? ",{$this->m_cachefield}" : "");
  260. $query .= " FROM " . $this->m_ownerInstance->m_table . " WHERE " . $this->m_ownerInstance->m_table . "."
  261. . $this->m_ownerInstance->primaryKeyField() . "='" . $record[$this->m_ownerInstance->primaryKeyField()] . "'";
  262. $result = $db->getrows($query);
  263. } else {
  264. $result[0] = $record;
  265. }
  266. $cryptedlink = $this->decrypt($result[0], $this->fieldName());
  267. $records[0][$this->fieldName()] = $cryptedlink;
  268. if ($cryptedlink) {
  269. $record[$this->fieldName()] = $cryptedlink;
  270. //for the use of encrypted id's we don't want to use the refkey,
  271. //because in that case we have to encrypt the id of the employee
  272. //and than atk CAN get the destination data, but not the owner data.
  273. //so we backup de refkey, make in empty and put it back after loading the record.
  274. $backup_refkey = $this->m_refKey;
  275. $this->m_refKey = "";
  276. $load = parent::load($db, $record, $mode);
  277. $this->m_refKey = $backup_refkey;
  278. return $load;
  279. } else {
  280. Adapto_Util_Debugger::debug(
  281. "Could not decrypt the link: $link for " . $this->m_ownerInstance->primaryKeyField() . "='"
  282. . $record[$this->m_ownerInstance->primaryKeyField()]);
  283. }
  284. }
  285. }
  286. /**
  287. * Decrypt the field
  288. *
  289. * @param array $record
  290. * @param array $field
  291. * @return string The decrypted value
  292. */
  293. function decrypt($record, $field)
  294. {
  295. global $g_encryption;
  296. if (!$this->m_linkpass)
  297. $this->getLinkPassword($this->m_linktable, $this->m_linkfield, $g_encryption);
  298. if (!$this->m_cachefield || !$record[$this->m_cachefield]) {
  299. $cryptedlink = $this->m_crypt->decrypt($record[$field], $this->m_linkpass);
  300. if ($this->m_ownerInstance) {
  301. if ($this->m_cachefield && is_numeric($cryptedlink) && $cryptedlink && $record[$this->m_ownerInstance->primaryKeyField()]
  302. && $this->createDestination()) {
  303. if ($this->m_ownerInstance->m_attribList[$field])
  304. $cachetable = $this->m_ownerInstance->m_table;
  305. else if ($this->m_destInstance->m_attribList[$field])
  306. $cachetable = $this->m_destInstance->m_table;
  307. $db = &atkGetDb();
  308. $db
  309. ->query(
  310. "UPDATE $cachetable
  311. SET {$this->m_cachefield}='$cryptedlink'
  312. WHERE " . $this->m_ownerInstance->primaryKeyField() . " = '" . $record[$this->m_ownerInstance->primaryKeyField()] . "'");
  313. } else if (!$cryptedlink || !is_numeric($cryptedlink)) {
  314. Adapto_Util_Debugger::debug("decrypt($record, $field) failed! and yielded: $cryptedlink");
  315. return NULL;
  316. }
  317. } else {
  318. halt("no ownerinstance found for the secure relation");
  319. }
  320. } else {
  321. $cryptedlink = $record[$this->m_cachefield];
  322. }
  323. return $cryptedlink;
  324. }
  325. /**
  326. * For creating a new user put the linkpassword in the db
  327. * @param string $id the id of the user to create
  328. * @param string $pass the password for the user
  329. */
  330. function newUser($id, $pass)
  331. {
  332. $db = &atkGetDb();
  333. $linkpass = $this->m_crypt->encryptKey($this->getLinkPassword($this->m_linktable, $this->m_linkfield), $pass);
  334. $query = "UPDATE $this->m_linktable SET $this->m_linkfield = '" . $linkpass . "' WHERE id = '$id'";
  335. $db->query($query);
  336. }
  337. /**
  338. * Returns the condition which can be used when calling atkQuery's addJoin() method
  339. * Joins the relation's owner with the destination
  340. */
  341. function _getJoinCondition()
  342. {
  343. $db = &atkGetDb();
  344. // decrypt the encrypted keys to get the tables joined
  345. $temp_query = "SELECT " . $this->fieldName() . " FROM " . $this->m_ownerInstance->m_table;
  346. $result = $db->getRows($temp_query);
  347. $condition = "";
  348. foreach ($result as $recordArray) {
  349. $record = $recordArray[$this->fieldName()];
  350. $decrypted_record = $this->decrypt($recordArray, $this->fieldName());
  351. if ($condition == "")
  352. $whereOrAnd = "(";
  353. else
  354. $whereOrAnd = "OR";
  355. $condition .= $whereOrAnd . " (" . $this->m_destInstance->m_table . "." . $this->m_destInstance->primaryKeyField() . "='" . $decrypted_record
  356. . "' ";
  357. $condition .= "AND " . $this->m_ownerInstance->m_table . "." . $this->fieldName() . "=\"" . addslashes($record) . "\") ";
  358. }
  359. $condition .= ") ";
  360. return $condition;
  361. }
  362. /**
  363. * Determine the load type of this attribute.
  364. *
  365. * @param String $mode The type of load (view,admin,edit etc)
  366. * @param bool $searching Is this a search?
  367. *
  368. * @return int Bitmask containing information about load requirements.
  369. * POSTLOAD|ADDTOQUERY when AF_ONETOONE_LAZY is set.
  370. * ADDTOQUERY when AF_ONETOONE_LAZY is not set.
  371. */
  372. function loadType($mode, $searching = false)
  373. {
  374. if ($searching) {
  375. $this->m_searching = true;
  376. return PRELOAD | ADDTOQUERY | POSTLOAD;
  377. } else {
  378. return parent::loadType($mode, $searching);
  379. }
  380. }
  381. /**
  382. * Creates a search condition for a given search value, and adds it to the
  383. * query that will be used for performing the actual search.
  384. *
  385. * @param atkQuery $query The query to which the condition will be added.
  386. * @param String $table The name of the table in which this attribute
  387. * is stored
  388. * @param mixed $value The value the user has entered in the searchbox
  389. * @param String $searchmode The searchmode to use. This can be any one
  390. * of the supported modes, as returned by this
  391. * attribute's getSearchModes() method.
  392. * @param string $fieldaliasprefix optional prefix for the fieldalias in the table
  393. */
  394. function searchCondition(&$query, $table, $value, $searchmode, $fieldaliasprefix = '')
  395. {
  396. //dummy implementation, we handle our own search in the destination entity.
  397. }
  398. /**
  399. * Creates a searchcondition for the field,
  400. * was once part of searchCondition, however,
  401. * searchcondition() also immediately adds the search condition.
  402. *
  403. * @param atkQuery $query The query object where the search condition should be placed on
  404. * @param String $table The name of the table in which this attribute
  405. * is stored
  406. * @param mixed $value The value the user has entered in the searchbox
  407. * @param String $searchmode The searchmode to use. This can be any one
  408. * of the supported modes, as returned by this
  409. * attribute's getSearchModes() method.
  410. * @return String The searchcondition to use.
  411. */
  412. function getSearchCondition(&$query, $table, $value, $searchmode)
  413. {
  414. // Off course, the secure relation has to have a special search condition
  415. // because searching on a secure relation has to be broken up in 2 pieces
  416. // first the destination, then the owner, filtered by the results from
  417. // the search on the destination
  418. $searchConditions = array();
  419. $descfields = $this->m_destInstance->descriptorFields();
  420. $prevdescfield = "";
  421. foreach ($descfields as $descField) {
  422. if ($descField !== $prevdescfield && $descField !== $this->m_owner) {
  423. $p_attrib = &$this->m_destInstance->getAttribute($descField);
  424. if (is_object($p_attrib)) {
  425. if ($p_attrib->m_destInstance) {
  426. $itsTable = $p_attrib->m_destInstance->m_table;
  427. } else {
  428. $itsTable = $p_attrib->m_ownerInstance->m_table;
  429. }
  430. if (is_array($searchmode))
  431. $searchmode = $searchmode[$this->fieldName()];
  432. if (!$searchmode)
  433. $searchmode = Adapto_Config::getGlobal("search_defaultmode");
  434. // checking for the getSearchCondition
  435. // for backwards compatibility
  436. if (method_exists($p_attrib, "getSearchCondition")) {
  437. $searchcondition = $p_attrib->getSearchCondition($query, $itsTable, $value, $searchmode);
  438. if ($searchcondition) {
  439. $searchConditions[] = $searchcondition;
  440. }
  441. } else {
  442. $p_attrib->searchCondition($query, $itsTable, $value, $searchmode);
  443. }
  444. }
  445. $prevdescfield = $descField;
  446. }
  447. }
  448. if (!$this->m_destsearch[$value] && $this->createDestination()) {
  449. $this->m_destsearch[$value] = $this->m_destInstance->selectDb(implode(" OR ", $searchConditions));
  450. }
  451. foreach ($this->m_destsearch[$value] as $result) {
  452. $destresult = $this->decrypt($result, $this->m_linkbackfield);
  453. if ($destresult)
  454. $destresults[] = $destresult;
  455. }
  456. if ($query->m_joinaliases[$this->m_ownerInstance->m_table . "*" . $this->m_ownerInstance->primaryKeyField()])
  457. $table = $query->m_joinaliases[$this->m_ownerInstance->m_table . "*" . $this->m_ownerInstance->primaryKeyField()];
  458. else if (in_array($this->m_ownerInstance->m_table, $query->m_tables))
  459. $table = $this->m_ownerInstance->m_table;
  460. else
  461. $table = null;
  462. if (!empty($destresults) && $table)
  463. return $table . "." . $this->m_ownerInstance->primaryKeyField() . " IN (" . implode(",", $destresults) . ")";
  464. }
  465. }
  466. ?>