ATK /relations/class.atksecurerelation.inc

Language PHP Lines 544
MD5 Hash 2ed22cce6fae4acbd8494e3f34084784
Repository https://github.com/ibuildingsnl/ATK.git View Raw File View Project SPDX
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
<?php
  /**
   * This file is part of the Achievo ATK distribution.
   * Detailed copyright and licensing information can be found
   * in the doc/COPYRIGHT and doc/LICENSE files which should be 
   * included in the distribution.
   * 
   * @package atk
   * @subpackage relations
   *
   * @copyright (c)2000-2004 Ibuildings.nl BV
   * @license http://www.achievo.org/atk/licensing ATK Open Source License
   *
   */
  atkimport("atk.security.encryption.atkencryption");
  userelation("atkonetoonerelation");
  
  /**
   * Relationship that can link 2 tables based on a secure link
   * that can not be decrypted when not logged in through an atk
   * application. 
   * This effectively secures the database so that data in two
   * tables can not be correlated by mischievous access to the database.
   *
   * @author Mark Baaijens <mark@ibuildings.nl>
   *
   * @package atk
   * @subpackage relations
   *
   */
  class atkSecureRelation extends atkOneToOneRelation
  {
    var $m_crypt = NULL;
    var $m_linktable;
    var $m_linkfield;
    var $m_linkuserfield = "username";
    var $m_keylength;
    var $m_searching = false;
    var $m_keylookup = array();
    var $m_records = array();
    var $m_searcharray=array();
    var $m_linkpass;
    var $m_linkbackfield;
    var $m_ownersearch;
    var $m_destsearch;
    var $m_cachefield;

    /**
     * Creates an atkSecureRelation, 
     * similar to an atkOneToOne relation only encrypted
     *
     * @param string $name        The unique name of the attribute. In slave 
     *                            mode, this corresponds to the foreign key 
     *                            field in the database table. 
     * @param string $destination The destination node (in module.nodename
     *                            notation)
     * @param string $linktable   The table we link to
     * @param string $linkfield   The field we link to
     * @param string $linkbackfield  
     * @param int $keylength      The length of the encryption key
     * @param string $refKey=""   In master mode, this specifies the foreign 
     *                            key field from the destination node that 
     *                            points to the master record. In slave mode, 
     *                            this parameter should be empty.
     * @param string $encryption  The encryption to use
     * @param int $flags          Attribute flags that influence this 
     *                            attributes' behavior.     
     */
    function atkSecureRelation($name, $destination, $linktable, $linkfield, $linkbackfield, $keylength, $refKey="", $encryption, $flags=0)
    {
      $this->atkOneToOneRelation($name, $destination, $refKey, $flags|AF_ONETOONE_ERROR);
      $this->createDestination();
      $this->m_crypt = &atkEncryption::getEncryption($encryption);
            
      $this->m_linktable = $linktable;
      $this->m_linkfield = $linkfield;
      $this->m_keylength = $keylength;
      $this->m_linkbackfield = $linkbackfield;
    }
    
    /**
     * Set the name of the cache field
     *
     * @param string $fieldname The cache fieldname
     */
    function setCacheField($fieldname="cache")
    {
      $this->m_cachefield = $fieldname;
    }
  
    /**
     * Adds the attribute / field to the list header. This includes the column name and search field.
     *
     * Framework method. It should not be necessary to call this method directly.
     *
     * @param String $action the action that is being performed on the node
     * @param array  $arr reference to the the recordlist array
     * @param String $fieldprefix the fieldprefix
     * @param int    $flags the recordlist flags
     * @param array  $atksearch the current ATK search list (if not empty)
     * @param String $atkorderby the current ATK orderby string (if not empty)
     */
    function addToListArrayHeader($action, &$arr, $fieldprefix, $flags, $atksearch, $atkorderby)
    {
      if ($this->hasFlag(AF_ONETOONE_INTEGRATE))
      {
        // integrated version, don't add ourselves, but add all columns from the destination.
        if ($this->createDestination())
        {
          foreach (array_keys($this->m_destInstance->m_attribList) as $attribname)
          {
            $p_attrib = &$this->m_destInstance->getAttribute($attribname);
            $p_attrib->addFlag(AF_NO_SORT);
          }
        }
      }
      parent::addToListArrayHeader($action, $arr, $fieldprefix, $flags, $atksearch, $atkorderby);
    }
    
    /**
     * Gets the password for the link
     * for more security the administrator gets a random password. You have to capture in your application that
     * the administrator is only able to insert the first record in this relation and make also a useracount with it.
     * @param string $linktable the table where we find the linkpass
     * @param string $linkfield the field where we find the encrypted linkpass
     * @param string $encryption The encryption to use
     * @return string           the password for the link
     */
    function getLinkPassword($linktable,$linkfield,$encryption="")
    {      
      if ($this->m_linkpass) return $this->m_linkpass;
      if (!$linktable) $linktable = $this->m_linktable;
      if (!$linkfield) $linkfield = $this->m_linkfield;
      
      $user = getUser();
      $username = $user['name'];
      $password = $user['PASS'];

      if($encryption)
        $crypt = atkEncryption::getEncryption($encryption);
      else
        $crypt = $this->m_crypt;
  
      if($username == "administrator")
      {
        //if the administrator asks for a  password we generate one
        //because the administrator only makes the first person
        global $linkpass;
        if(!$linkpass)
          $linkpass = $crypt->getRandomKey($password);
      }
      else
      {
        $query = "SELECT ".$linkfield." as pass FROM ".$linktable." WHERE ".atkconfig("auth_userfield")." = '".$username."'";

        $db = &atkGetDb();
        $rec = $db->getrows($query);
        if(count($rec) < 1)
          return $linkpass;
          
        $encryptedpass = array_pop($rec);

        $linkpass = $encryptedpass['pass'];
      }
      $this->m_linkpass = $crypt->decryptKey($linkpass,$password);
      return $this->m_linkpass;
    }
  
    /**
     * This function in the atkOneToOneRelation store the record of the parentnode in the DB
     * with the reference key of the other table. 
     * So we encrypt the reference key before we call the method.
     * For more documentation see the atkOneToOneRelation
     * 
     * @param atkQuery $query The SQL query object
     * @param String $tablename The name of the table of this attribute
     * @param String $fieldaliasprefix Prefix to use in front of the alias
     *                                 in the query.
     * @param Array $rec The record that contains the value of this attribute.
     * @param int $level Recursion level if relations point to eachother, an
     *                   endless loop could occur if they keep loading
     *                   eachothers data. The $level is used to detect this
     *                   loop. If overriden in a derived class, any subcall to
     *                   an addToQuery method should pass the $level+1.
     * @param String $mode Indicates what kind of query is being processing:
     *                     This can be any action performed on a node (edit,
     *                     add, etc) Mind you that "add" and "update" are the
     *                     actions that store something in the database,
     *                     whereas the rest are probably select queries.
     */
    function addToQuery(&$query, $tablename="", $fieldaliasprefix="", $rec="", $level=0, $mode="")
    {
      $records = $this->m_records;
      
      if (count($records)==0 && !$this->m_searching)
      {
        if(is_array($rec))
        {
          $link = $rec[$this->fieldName()][$this->m_destInstance->m_primaryKey[0]];
          $cryptedlink = $this->m_crypt->encrypt($link,$this->getLinkPassword($this->m_linktable, $this->m_linkfield));
          $rec[$this->fieldName()][$this->m_destInstance->m_primaryKey[0]] = addslashes($cryptedlink);
        }
  
        return parent::addToQuery($query, $tablename, $fieldaliasprefix, $rec, $level, $mode);
      }
      else // lookup matching
      {
        $where = array();

        foreach (array_keys($this->m_keylookup) as $decryptedlink)
        {                     
          $where[] = $decryptedlink;
        }
        
        if ($tablename) $tablename .= ".";
        $query->addSearchCondition($tablename.$this->m_ownerInstance->primaryKeyField()." IN ('".implode("','", $where)."')");
      }
    }
  
    /**
     * This function in the atkOneToOneRelation loads the record of the childnode from the DB
     * with the the id from de reference key in childnode. 
     * So we decrypt the reference key before we call the method. 
     * For more documentation see the atkOneToOneRelation
     * 
     * @param atkDb $db The database object
     * @param array $record The record
     * @param string $mode The mode we're in ("add", "edit", "copy")
     * @return array The loaded records
     */
    function load(&$db, $record, $mode)
    {                 
      if ($this->m_searching)
      {
        if($this->m_searcharray !== $this->m_ownerInstance->m_postvars["atksearch"][$this->fieldName()] 
           && is_array($this->m_searcharray))
        {
          $this->m_records = array();
          $this->m_keylookup= array();
          // perform query on destination node to retrieve all records.
          if ($this->createDestination())
          {
            $this->m_searcharray = $this->m_ownerInstance->m_postvars["atksearch"][$this->fieldName()];

            //if al search values are equal, then make it an OR search
            if(count(array_unique(array_values($this->m_searcharray))) == 1)
              $this->m_destInstance->m_postvars['atksearchmethod'] = "OR";

             $oldsearcharray = $this->m_searcharray;
            // check wether mentioned fields are actually in the node
            foreach ($this->m_searcharray as $searchfield => $searchvalue)
            {
              if (!is_object($this->m_destInstance->m_attribList[$searchfield]))
                unset($this->m_searcharray[$searchfield]);
            }            
            $this->m_destInstance->m_postvars["atksearch"] =        $this->m_searcharray;
            $this->m_destInstance->m_postvars["atksearchmode"] =    $this->m_ownerInstance->m_postvars["atksearchmode"];
            $this->m_destInstance->m_postvars["atksearchmethod"] =  $this->m_ownerInstance->m_postvars["atksearchmethod"];
            
            $records = $this->m_destInstance->selectDb();
            $this->m_searcharray = $oldsearcharray;
            $errorconfig = atkconfig("securerelation_decrypterror", null);

            // create lookup table for easy reference.            
            for ($i=0, $_i=count($records); $i<$_i; $i++)
            {              
              $decryptedlink = $this->decrypt($records[$i], $this->m_linkbackfield);
              
              if(!$decryptedlink && $errorconfig) 
              {
                atkdebug("Unable to decrypt link: ".$link."for record: ".var_export($records[$i], true)." with linkbackfield: ".$this->m_linkbackfield);
                $decrypterror=true;
              }
              else
              {
                $this->m_keylookup[$decryptedlink] = $i;
                $this->m_records[] = $records[$i];     
              }
            }
            if ($decrypterror)
            {
              if ($errorconfig==2)      atkerror("There were errors decrypting the secured links, see debuginfo");
              else if ($errorconfig==1) mailreport();
            }
            return $this->m_records;
          }
        }
        else // lookup table present, postload stage
        {
          $this->m_searching = false;
          return $this->m_records[$this->m_keylookup[$record[$this->m_ownerInstance->primaryKeyField()]]];
        }
      }
      else 
      {
        if (!$record[$this->fieldName()] || (!$record[$this->m_cachefield] && $this->m_cachefield))
        {
          $query = "SELECT ".$this->fieldName();
          $query.= ($this->m_cachefield?",{$this->m_cachefield}":"");
          $query.=" FROM ".$this->m_ownerInstance->m_table." WHERE ".$this->m_ownerInstance->m_table.".".$this->m_ownerInstance->primaryKeyField()."='".$record[$this->m_ownerInstance->primaryKeyField()]."'";
          $result = $db->getrows($query);
        }
        else 
        {
          $result[0] = $record;
        }
        $cryptedlink = $this->decrypt($result[0],$this->fieldName());
        $records[0][$this->fieldName()] = $cryptedlink;

        if ($cryptedlink)
        {

          $record[$this->fieldName()] = $cryptedlink;

          //for the use of encrypted id's we don't want to use the refkey,
          //because in that case we have to encrypt the id of the employee
          //and than atk CAN get the destination data, but not the owner data.
          //so we backup de refkey, make in empty and put it back after loading the record.
          $backup_refkey = $this->m_refKey;
          $this->m_refKey = "";
          $load = parent::load($db, $record, $mode);
          $this->m_refKey = $backup_refkey;
          return $load;
        }
        else
        {
          atkdebug("Could not decrypt the link: $link for ".$this->m_ownerInstance->primaryKeyField()."='".$record[$this->m_ownerInstance->primaryKeyField()]);
        }
      }
    }
    
    /**
     * Decrypt the field
     *
     * @param array $record
     * @param array $field
     * @return string The decrypted value
     */
    function decrypt($record, $field)
    { 
      global $g_encryption;
      
      if (!$this->m_linkpass) $this->getLinkPassword($this->m_linktable, $this->m_linkfield, $g_encryption);
      
      if (!$this->m_cachefield || !$record[$this->m_cachefield])
      {
        $cryptedlink = $this->m_crypt->decrypt($record[$field],$this->m_linkpass);
        if ($this->m_ownerInstance)
        {
          if ($this->m_cachefield && is_numeric($cryptedlink) && $cryptedlink && $record[$this->m_ownerInstance->primaryKeyField()] && $this->createDestination())
          {
            if ($this->m_ownerInstance->m_attribList[$field]) $cachetable = $this->m_ownerInstance->m_table;
            else if ($this->m_destInstance->m_attribList[$field]) $cachetable = $this->m_destInstance->m_table;
            
            $db = &atkGetDb();
            $db->query("UPDATE $cachetable
                              SET {$this->m_cachefield}='$cryptedlink'
                              WHERE ".$this->m_ownerInstance->primaryKeyField()." = '".
            $record[$this->m_ownerInstance->primaryKeyField()]."'");
          }
          else if (!$cryptedlink || !is_numeric($cryptedlink))
          {
            atkdebug("decrypt($record, $field) failed! and yielded: $cryptedlink");
            return NULL;
          }
        }
        else
        {
          halt("no ownerinstance found for the secure relation");
        }
      }
      else
      {
        $cryptedlink = $record[$this->m_cachefield];
      }
      return $cryptedlink;
    }

    /**
     * For creating a new user put the linkpassword in the db
     * @param string $id the id of the user to create
     * @param string $pass the password for the user
     */
    function newUser($id,$pass)
    {
      $db = &atkGetDb();
      $linkpass = $this->m_crypt->encryptKey($this->getLinkPassword($this->m_linktable,$this->m_linkfield),$pass);
      $query = "UPDATE $this->m_linktable SET $this->m_linkfield = '".$linkpass."' WHERE id = '$id'";
      $db->query($query);
    }
        
    /**
     * Returns the condition which can be used when calling atkQuery's addJoin() method
     * Joins the relation's owner with the destination
     */
    function _getJoinCondition()
    {
      $db = &atkGetDb();
      
      // decrypt the encrypted keys to get the tables joined
      $temp_query = "SELECT " . $this->fieldName() . " FROM " . $this->m_ownerInstance->m_table;
      $result = $db->getRows($temp_query);
      
      $condition = "";
      foreach($result as $recordArray)
      {
        $record = $recordArray[$this->fieldName()];
        $decrypted_record = $this->decrypt($recordArray,$this->fieldName());
        if ($condition == "")
          $whereOrAnd = "(";
        else
          $whereOrAnd = "OR";
          
        $condition .= $whereOrAnd . " (" . $this->m_destInstance->m_table . "." . $this->m_destInstance->primaryKeyField() . "='" . $decrypted_record . "' ";
        $condition .= "AND " . $this->m_ownerInstance->m_table . "." . $this->fieldName() . "=\"".addslashes($record)."\") ";
      }
      $condition .= ") ";

      return $condition;
    }
    
    /**
     * Determine the load type of this attribute.
     *
     * @param String $mode The type of load (view,admin,edit etc)
     * @param bool $searching Is this a search?
     *
     * @return int Bitmask containing information about load requirements.
     *             POSTLOAD|ADDTOQUERY when AF_ONETOONE_LAZY is set.
     *             ADDTOQUERY when AF_ONETOONE_LAZY is not set.
     */
    function loadType($mode, $searching=false)
    {
      if ($searching) 
      {        
        $this->m_searching = true;
        return PRELOAD|ADDTOQUERY|POSTLOAD;
      }
      else 
      {
        return parent::loadType($mode, $searching);
      }
    }    
    
    /**
     * Creates a search condition for a given search value, and adds it to the
     * query that will be used for performing the actual search.
     *
     * @param atkQuery $query The query to which the condition will be added.
     * @param String $table The name of the table in which this attribute
     *                      is stored
     * @param mixed $value The value the user has entered in the searchbox
     * @param String $searchmode The searchmode to use. This can be any one
     *                           of the supported modes, as returned by this
     *                           attribute's getSearchModes() method.
     * @param string $fieldaliasprefix optional prefix for the fieldalias in the table
     */
    function searchCondition(&$query, $table, $value, $searchmode, $fieldaliasprefix='')
    {
      //dummy implementation, we handle our own search in the destination node.
    }
    
    /**
     * Creates a searchcondition for the field,
     * was once part of searchCondition, however,
     * searchcondition() also immediately adds the search condition.
     *
     * @param atkQuery $query     The query object where the search condition should be placed on
     * @param String $table       The name of the table in which this attribute
     *                              is stored
     * @param mixed $value        The value the user has entered in the searchbox
     * @param String $searchmode  The searchmode to use. This can be any one
     *                              of the supported modes, as returned by this
     *                              attribute's getSearchModes() method.
     * @return String The searchcondition to use.
     */
    function getSearchCondition(&$query, $table, $value, $searchmode)
    {
      // Off course, the secure relation has to have a special search condition
      // because searching on a secure relation has to be broken up in 2 pieces
      // first the destination, then the owner, filtered by the results from
      // the search on the destination

      $searchConditions = array();
      $descfields = $this->m_destInstance->descriptorFields();
      $prevdescfield = "";
      foreach($descfields as $descField)
      {
        if ($descField !== $prevdescfield && $descField !== $this->m_owner)
        {
          $p_attrib = &$this->m_destInstance->getAttribute($descField);
          if (is_object($p_attrib))
          {
            if ($p_attrib->m_destInstance)
            {
              $itsTable = $p_attrib->m_destInstance->m_table;
            }
            else
            {
              $itsTable = $p_attrib->m_ownerInstance->m_table;
            }
            
            if (is_array($searchmode)) $searchmode = $searchmode[$this->fieldName()];
            if (!$searchmode) $searchmode = atkconfig("search_defaultmode");
            // checking for the getSearchCondition
            // for backwards compatibility
            if (method_exists($p_attrib,"getSearchCondition"))
            {
              $searchcondition = $p_attrib->getSearchCondition($query,$itsTable,$value,$searchmode);
              if ($searchcondition)
              {
                $searchConditions[] = $searchcondition;
              }
            }
            else
            {
              $p_attrib->searchCondition($query,$itsTable,$value,$searchmode);
            }
          }
          $prevdescfield = $descField;
        }
      }
      
      if (!$this->m_destsearch[$value] && $this->createDestination()) 
      {
        $this->m_destsearch[$value] = $this->m_destInstance->selectDb(implode(" OR ",$searchConditions));
      }
      
      foreach ($this->m_destsearch[$value] as $result)
      {
        $destresult = $this->decrypt($result,$this->m_linkbackfield);
        if ($destresult) $destresults[] = $destresult;
      }
      
      if ($query->m_joinaliases[$this->m_ownerInstance->m_table."*".$this->m_ownerInstance->primaryKeyField()]) $table = $query->m_joinaliases[$this->m_ownerInstance->m_table."*".$this->m_ownerInstance->primaryKeyField()];
      else if (in_array($this->m_ownerInstance->m_table, $query->m_tables)) $table = $this->m_ownerInstance->m_table;
      else $table=null;
      
      if (!empty($destresults) && $table)
        return $table.".".$this->m_ownerInstance->primaryKeyField()." IN (".implode(",",$destresults).")";
    }
  }
?>
Back to Top