Author: chabotc
Date: Thu May  1 17:14:48 2008
New Revision: 652719

URL: http://svn.apache.org/viewvc?rev=652719&view=rev
Log:
Adds basic crypto security token logic. The crypto part is 
courtesy of Bruno Rovagnati.

The crypto should be compatible with the java one (haven't actually
tested yet but it's the same logic).

It introduces 4 new config flags:
st_max_age - max age of the token to be accepted
token_cipher_key & token_hmac_key - keys to use
allow_plaintext_token - allow plain text security token
aka in (john.doe:jane.doe:etc:format)

This last setting should _never_ be true in a real situation but
is required to make the basic code work with the javascript
examples and sample container (which doesn't have crypto tokens).
This might be removed later when i have a good solution for this.


Added:
    incubator/shindig/trunk/php/src/common/Crypto.php
Modified:
    incubator/shindig/trunk/php/config.php
    incubator/shindig/trunk/php/src/common/BlobCrypter.php
    incubator/shindig/trunk/php/src/gadgets/ProxyHandler.php
    incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicBlobCrypter.php
    incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicGadgetToken.php
    
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicGadgetTokenDecoder.php
    
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicRemoteContentFetcher.php
    incubator/shindig/trunk/php/src/socialdata/http/GadgetDataServlet.php
    
incubator/shindig/trunk/php/src/socialdata/samplecontainer/BasicDataService.php
    
incubator/shindig/trunk/php/src/socialdata/samplecontainer/XmlStateFileFetcher.php

Modified: incubator/shindig/trunk/php/config.php
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/php/config.php?rev=652719&r1=652718&r2=652719&view=diff
==============================================================================
--- incubator/shindig/trunk/php/config.php (original)
+++ incubator/shindig/trunk/php/config.php Thu May  1 17:14:48 2008
@@ -6,12 +6,23 @@
  */
 $config = array(
        // Show debug backtrace? Set this to false on anything that resembles a 
production env
-       'debug' => true,
+       'debug' => false,
 
        // The base prefix under which the our url's live, if its the root set 
this to ''
        // don't forget to update your .htaccess to reflect this, as well as 
your container 
        // javascript like: gadget.setServerBase('/someBaseUrl/');
        'web_prefix' => '',
+
+       // Max age of a security token, defaults to one hour
+       'st_max_age' => 60 * 60,
+
+       // Security token keys
+       'token_cipher_key' => 'INSECURE_DEFAULT_KEY',
+       'token_hmac_key' => 'INSECURE_DEFAULT_KEY',
+
+       // The html / javascript samples use a plain text demo token,
+       // set this to false on anything resembling a real site
+       'allow_plaintext_token' => true,
        
        // P3P (Platform for Privacy Preferences) header for allowing cross 
domain cookies.
        // Setting this to an empty string: '' means no P3P header will be send
@@ -29,7 +40,7 @@
        // seperated by a , For example:
        // 'handlers' => 'OpenSocialDataHandler,StateFileDataHandler',
        // if the value is empty, the defaults used in the example above will 
be used.
-       'handlers' => '', 
+       'handlers' => '',
 
        'focedJsLibs' => '',
        

Modified: incubator/shindig/trunk/php/src/common/BlobCrypter.php
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/common/BlobCrypter.php?rev=652719&r1=652718&r2=652719&view=diff
==============================================================================
--- incubator/shindig/trunk/php/src/common/BlobCrypter.php (original)
+++ incubator/shindig/trunk/php/src/common/BlobCrypter.php Thu May  1 17:14:48 
2008
@@ -37,7 +37,7 @@
         * 
         * @throws BlobCrypterException
         */
-       abstract public function wrap($in);
+       abstract public function wrap(Array $in);
        
        /**
         * Unwraps a blob.

Added: incubator/shindig/trunk/php/src/common/Crypto.php
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/common/Crypto.php?rev=652719&view=auto
==============================================================================
--- incubator/shindig/trunk/php/src/common/Crypto.php (added)
+++ incubator/shindig/trunk/php/src/common/Crypto.php Thu May  1 17:14:48 2008
@@ -0,0 +1,116 @@
+<?php
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class GeneralSecurityException extends Exception {
+}
+
+final class Crypto {
+       
+       /**
+        * HMAC algorithm to use
+        */
+       private static $HMAC_TYPE = "HMACSHA1";
+       
+       /** 
+        * minimum safe length for hmac keys (this is good practice, but not 
+        * actually a requirement of the algorithm
+        */
+       private static $MIN_HMAC_KEY_LEN = 8;
+       
+       /**
+        * Encryption algorithm to use
+        */
+       private static $CIPHER_TYPE = "AES/CBC/PKCS5Padding";
+       
+       private static $CIPHER_KEY_TYPE = "AES";
+       
+       /**
+        * Use keys of this length for encryption operations
+        */
+       public static $CIPHER_KEY_LEN = 16;
+       
+       private static $CIPHER_BLOCK_SIZE = 16;
+       
+       /**
+        * Length of HMAC SHA1 output
+        */
+       public static $HMAC_SHA1_LEN = 20;
+       
+       private function __construct()
+       {
+       }
+       
+       public static function hmacSha1Verify($key, $in, $expected)
+       {
+               $hmac = Crypto::hmacSha1($key, $in);
+               if ($hmac != $expected) {
+                       throw new GeneralSecurityException("HMAC verification 
failure");
+               }
+       }
+       
+       public static function aes128cbcEncrypt($key, $text)
+       {
+               /* Open the cipher */
+               $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', 
MCRYPT_MODE_CBC, '');
+               $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), 
MCRYPT_DEV_RANDOM);
+               /* Intialize encryption */
+               mcrypt_generic_init($td, $key, $iv);
+               /* Encrypt data */
+               $encrypted = mcrypt_generic($td, $text);
+               /* Terminate encryption handler */
+               mcrypt_generic_deinit($td);
+               /*
+                *  AES-128-CBC encryption.  The IV is returned as the first 16 
bytes
+                * of the cipher text.
+                */
+               return $iv . $encrypted;
+       }
+       
+       public static function aes128cbcDecrypt($key, $encrypted_text)
+       {
+               /* Open the cipher */
+               $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', 
MCRYPT_MODE_CBC, '');
+               $iv = substr($encrypted_text, 0, Crypto::$CIPHER_BLOCK_SIZE);
+               /* Initialize encryption module for decryption */
+               mcrypt_generic_init($td, $key, $iv);
+               /* Decrypt encrypted string */
+               $encrypted = substr($encrypted_text, 
Crypto::$CIPHER_BLOCK_SIZE);
+               $decrypted = mdecrypt_generic($td, $encrypted);
+               /* Terminate decryption handle and close module */
+               mcrypt_generic_deinit($td);
+               mcrypt_module_close($td);
+               /* Show string */
+               return trim($decrypted);
+       }
+       
+       public static function hmacSha1($key, $data)
+       {
+               $blocksize = 64;
+               $hashfunc = 'sha1';
+               if (strlen($key) > $blocksize) {
+                       $key = pack('H*', $hashfunc($key));
+               }
+               $key = str_pad($key, $blocksize, chr(0x00));
+               $ipad = str_repeat(chr(0x36), $blocksize);
+               $opad = str_repeat(chr(0x5c), $blocksize);
+               $hmac = pack('H*', $hashfunc(($key ^ $opad) . pack('H*', 
$hashfunc(($key ^ $ipad) . $data))));
+               return $hmac;
+       }
+}

Modified: incubator/shindig/trunk/php/src/gadgets/ProxyHandler.php
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/ProxyHandler.php?rev=652719&r1=652718&r2=652719&view=diff
==============================================================================
--- incubator/shindig/trunk/php/src/gadgets/ProxyHandler.php (original)
+++ incubator/shindig/trunk/php/src/gadgets/ProxyHandler.php Thu May  1 
17:14:48 2008
@@ -45,7 +45,12 @@
         */
        public function fetchJson($url, $signer, $method)
        {
-               $token = $this->extractAndValidateToken($signer);
+               try {
+                       $token = $this->extractAndValidateToken($signer);
+               } catch (Exception $e) {
+                       $token = '';
+                       // no token given, safe to ignore
+               }
                $originalUrl = $this->validateUrl($url);
                $signedUrl = $this->signUrl($originalUrl, $token);
                // Fetch the content and convert it into JSON.

Modified: 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicBlobCrypter.php
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicBlobCrypter.php?rev=652719&r1=652718&r2=652719&view=diff
==============================================================================
--- 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicBlobCrypter.php 
(original)
+++ 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicBlobCrypter.php 
Thu May  1 17:14:48 2008
@@ -1,21 +1,59 @@
 <?php
 /**
  * This class provides basic binary blob encryption and decryption, for use 
with the security token
- *
+ * 
  */
 
+class BlobExpiredException extends Exception {
+}
+
 //FIXME make this compatible with the java's blobcrypter
 class BasicBlobCrypter extends BlobCrypter {
        
+       // Labels for key derivation
+       private $CIPHER_KEY_LABEL = 0;
+       private $HMAC_KEY_LABEL = 1;
+       
+       /** Key used for time stamp (in seconds) of data */
+       public $TIMESTAMP_KEY = "t";
+       
+       /** minimum length of master key */
+       public $MASTER_KEY_MIN_LEN = 16;
+       
+       /** allow three minutes for clock skew */
+       private $CLOCK_SKEW_ALLOWANCE = 180;
+       
+       private $UTF8 = "UTF-8";
+       
+       private $cipherKey;
+       private $hmacKey;
+       
+       public function __construct()
+       {
+               $this->cipherKey = Config::get('token_cipher_key');
+               $this->hmacKey = Config::get('token_hmac_key');
+       }
+       
        /**
         * [EMAIL PROTECTED]
         */
-       public function wrap($in)
+       public function wrap(Array $in)
+       {
+               $encoded = $this->serializeAndTimestamp($in);
+               $cipherText = Crypto::aes128cbcEncrypt($this->cipherKey, 
$encoded);
+               $hmac = Crypto::hmacSha1($this->hmacKey, $cipherText);
+               $b64 = base64_encode($cipherText . $hmac);
+               return $b64;
+       }
+       
+       private function serializeAndTimestamp(Array $in)
        {
-               if(is_array($in)) {
-                       $in = implode(":", $in);
+               $encoded = "";
+               foreach ( $in as $key => $val ) {
+                       $encoded .= urlencode($key) . "=" . urlencode($val) . 
"&";
                }
-               return $in;
+               $encoded .= $this->TIMESTAMP_KEY . "=" . time();
+               return $encoded;
        }
        
        /**
@@ -23,14 +61,54 @@
         */
        public function unwrap($in, $maxAgeSec)
        {
-               $data = explode(":", $in);
-               $rta = array();
-               $rta['o'] = $data[0];
-               $rta['a'] = $data[1];
-               $rta['v'] = $data[2];
-               $rta['d'] = $data[3];
-               $rta['u'] = $data[4];
-               $rta['m'] = $data[5];
-               return $rta;
+               //TODO remove this once we have a better way to generate a fake 
token
+               // in the example files
+               if (Config::get('allow_plaintext_token') && count(explode(':', 
$in)) == 6) {
+                       $data = explode(":", $in);
+                       $out = array();
+                       $out['o'] = $data[0];
+                       $out['v'] = $data[1];
+                       $out['a'] = $data[2];
+                       $out['d'] = $data[3];
+                       $out['u'] = $data[4];
+                       $out['m'] = $data[5];
+               } else {
+                       //TODO Exception handling like JAVA
+                       $bin = base64_decode($in);
+                       $cipherText = substr($bin, 0, strlen($bin) - 
Crypto::$HMAC_SHA1_LEN);
+                       $hmac = substr($bin, strlen($cipherText));
+                       Crypto::hmacSha1Verify($this->hmacKey, $cipherText, 
$hmac);
+                       $plain = Crypto::aes128cbcDecrypt($this->cipherKey, 
$cipherText);
+                       $out = $this->deserialize($plain);
+                       $this->checkTimestamp($out, $maxAgeSec);
+               }
+               return $out;
+       }
+       
+       private function deserialize($plain)
+       {
+               $map = array();
+               $items = split("[&=]", $plain);
+               if ((count($items) / 2) != 7) {
+                       // A valid token should decrypt to 14 items, aka 7 
pairs.
+                       // If not, this wasn't valid & untampered data and we 
abort
+                       throw new BlobExpiredException("Invalid security 
token");
+               }
+               for ($i = 0; $i < count($items); ) {
+                       $key = urldecode($items[$i ++]);
+                       $value = urldecode($items[$i ++]);
+                       $map[$key] = $value;
+               }
+               return $map;
+       }
+       
+       private function checkTimestamp(Array $out, $maxAge)
+       {
+               $minTime = (int)$out[$this->TIMESTAMP_KEY] - 
$this->CLOCK_SKEW_ALLOWANCE;
+               $maxTime = (int)$out[$this->TIMESTAMP_KEY] + $maxAge + 
$this->CLOCK_SKEW_ALLOWANCE;
+               $now = time();
+               if (! ($minTime < $now && $now < $maxTime)) {
+                       throw new BlobExpiredException("Security token 
expired");
+               }
        }
 }

Modified: 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicGadgetToken.php
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicGadgetToken.php?rev=652719&r1=652718&r2=652719&view=diff
==============================================================================
--- 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicGadgetToken.php 
(original)
+++ 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicGadgetToken.php 
Thu May  1 17:14:48 2008
@@ -30,8 +30,6 @@
   
   /** tool to use for signing and encrypting the token */
   private $crypter;
-
-  private $INSECURE_KEY = "{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}";
   
   private $OWNER_KEY = "o";
   private $APP_KEY = "a";
@@ -45,7 +43,7 @@
    */
   public function toSerialForm()
   {
-    return $this->token;
+    return urlencode($this->token);
   }
   
   /**
@@ -56,7 +54,7 @@
    */
   static public function createFromToken($token, $maxAge)
   {
-       return new BasicBlobCrypter($token, $maxAge, null, null, null, null, 
null, null);
+       return new BasicGadgetToken($token, $maxAge, null, null, null, null, 
null, null);
   }
   
   /**
@@ -71,7 +69,7 @@
    */
   static public function createFromValues($owner, $viewer, $app, $domain, 
$appUrl, $moduleId)
   {
-       return new BasicBlobCrypter(null, null, $owner, $viewer, $app, $domain, 
$appUrl, $moduleId);
+       return new BasicGadgetToken(null, null, $owner, $viewer, $app, $domain, 
$appUrl, $moduleId);
   }
   
   

Modified: 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicGadgetTokenDecoder.php
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicGadgetTokenDecoder.php?rev=652719&r1=652718&r2=652719&view=diff
==============================================================================
--- 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicGadgetTokenDecoder.php
 (original)
+++ 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicGadgetTokenDecoder.php
 Thu May  1 17:14:48 2008
@@ -18,10 +18,6 @@
  * under the License.
  */
 
-/**
- * A GadgetTokenDecoder implementation that just provides dummy data to satisfy
- * tests and API calls. Do not use this for any security applications.
- */
 class BasicGadgetTokenDecoder extends GadgetTokenDecoder {
        private $OWNER_INDEX = 0;
        private $VIEWER_INDEX = 1;
@@ -38,22 +34,24 @@
        public function createToken($stringToken)
        {
                if (empty($stringToken)) {
-                       //FIXME an empty token should generate an exception 
instead of
-                       // being ignored, however currently a proxy request 
always has
-                       // an empty token (bug presumably), so without this 
hack it doesn't work
-                       // reinstate the throw new 
GadgetException('INVALID_GADGET_TOKEN'); once thats fixed
-                       return;
+                       throw new GadgetException('INVALID_GADGET_TOKEN');
                }
                try {
-                       $tokens = explode(":", $stringToken);
-                       return new BasicGadgetToken(null,null,
-                               urldecode($tokens[$this->OWNER_INDEX]),
-                               urldecode($tokens[$this->VIEWER_INDEX]),
-                               urldecode($tokens[$this->APP_ID_INDEX]),
-                               urldecode($tokens[$this->CONTAINER_INDEX]),
-                               urldecode($tokens[$this->APP_URL_INDEX]),
-                               urldecode($tokens[$this->MODULE_ID_INDEX])
-                       );
+                       //TODO remove this once we have a better way to 
generate a fake token
+                       // in the example files
+                       if (Config::get('allow_plaintext_token') && 
count(explode(':', $stringToken)) == 6) {
+                               $tokens = explode(":", $stringToken);
+                               return new BasicGadgetToken(null,null,
+                                       urldecode($tokens[$this->OWNER_INDEX]),
+                                       urldecode($tokens[$this->VIEWER_INDEX]),
+                                       urldecode($tokens[$this->APP_ID_INDEX]),
+                                       
urldecode($tokens[$this->CONTAINER_INDEX]),
+                                       
urldecode($tokens[$this->APP_URL_INDEX]),
+                                       
urldecode($tokens[$this->MODULE_ID_INDEX])
+                               );
+                       } else {                        
+                               return 
BasicGadgetToken::createFromToken($stringToken, Config::get('st_max_age'));
+                       }
                } catch (Exception $e) {
                        throw new GadgetException('INVALID_GADGET_TOKEN');
                }

Modified: 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicRemoteContentFetcher.php
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicRemoteContentFetcher.php?rev=652719&r1=652718&r2=652719&view=diff
==============================================================================
--- 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicRemoteContentFetcher.php
 (original)
+++ 
incubator/shindig/trunk/php/src/gadgets/samplecontainer/BasicRemoteContentFetcher.php
 Thu May  1 17:14:48 2008
@@ -61,8 +61,7 @@
                // on redirects and such we get multiple headers back from curl 
it seems, we really only want the last one
                while ( substr($content, 0, strlen('HTTP')) == 'HTTP' && 
strpos($content, "\r\n\r\n") !== false ) {
                        $header = substr($content, 0, strpos($content, 
"\r\n\r\n"));
-                       $body = substr($content, strlen($header) + 4);
-                       $content = substr($content, strlen($header) + 4);
+                       $content = $body = substr($content, strlen($header) + 
4);
                }
                $httpCode = curl_getinfo($request->handle, CURLINFO_HTTP_CODE);
                $contentType = curl_getinfo($request->handle, 
CURLINFO_CONTENT_TYPE);

Modified: incubator/shindig/trunk/php/src/socialdata/http/GadgetDataServlet.php
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/socialdata/http/GadgetDataServlet.php?rev=652719&r1=652718&r2=652719&view=diff
==============================================================================
--- incubator/shindig/trunk/php/src/socialdata/http/GadgetDataServlet.php 
(original)
+++ incubator/shindig/trunk/php/src/socialdata/http/GadgetDataServlet.php Thu 
May  1 17:14:48 2008
@@ -11,12 +11,13 @@
        
        public function __construct()
        {
-               global $config;
-               if (empty($config['handlers'])) {
+               parent::__construct();
+               $handlers = Config::get('handlers');
+               if (empty($handlers)) {
                        $this->handlers[] = new OpenSocialDataHandler();
                        $this->handlers[] = new StateFileDataHandler();
                } else {
-                       $handlers = explode(',', $config['handlers']);
+                       $handlers = explode(',', $handlers);
                        foreach ( $handlers as $handler ) {
                                $this->handlers[] = new $handler();
                        }
@@ -27,6 +28,10 @@
        {
                $requestParam = isset($_POST['request']) ? $_POST['request'] : 
'';
                $token = isset($_POST['st']) ? $_POST['st'] : '';
+               // detect if magic quotes are on, and if so strip them from the 
request
+               if (get_magic_quotes_gpc()) {
+                       $requestParam = stripslashes($requestParam);
+               }
                $request = json_decode($requestParam, true);
                if ($request == $requestParam) {
                        // oddly enough if the json_decode function can't parse 
the code,
@@ -73,8 +78,7 @@
        
        public function doGet()
        {
-               echo 
-               header("HTTP/1.0 400 Bad Request", true, 400);
+               echo header("HTTP/1.0 400 Bad Request", true, 400);
                die("<h1>Bad Request</h1>");
        }
 }
\ No newline at end of file

Modified: 
incubator/shindig/trunk/php/src/socialdata/samplecontainer/BasicDataService.php
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/socialdata/samplecontainer/BasicDataService.php?rev=652719&r1=652718&r2=652719&view=diff
==============================================================================
--- 
incubator/shindig/trunk/php/src/socialdata/samplecontainer/BasicDataService.php 
(original)
+++ 
incubator/shindig/trunk/php/src/socialdata/samplecontainer/BasicDataService.php 
Thu May  1 17:14:48 2008
@@ -28,7 +28,7 @@
                                $allPersonData = $allData[$id];
                                $personData = array();
                                foreach ( array_keys($allPersonData) as $key ) {
-                                       if (in_array($key, $keys)) {
+                                       if (in_array($key, $keys) || $keys[0] 
== "*") {
                                                $personData[$key] = 
$allPersonData[$key];
                                        }
                                }

Modified: 
incubator/shindig/trunk/php/src/socialdata/samplecontainer/XmlStateFileFetcher.php
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/socialdata/samplecontainer/XmlStateFileFetcher.php?rev=652719&r1=652718&r2=652719&view=diff
==============================================================================
--- 
incubator/shindig/trunk/php/src/socialdata/samplecontainer/XmlStateFileFetcher.php
 (original)
+++ 
incubator/shindig/trunk/php/src/socialdata/samplecontainer/XmlStateFileFetcher.php
 Thu May  1 17:14:48 2008
@@ -106,7 +106,10 @@
                        $person = (string)$data['person'];
                        $key = (string)$data['field'];
                        $value = (string)$data;
-                       $this->allData[$person] = array($key => $value);
+                       if (!isset($this->allData[$person])) {
+                               $this->allData[$person] = array();
+                       }
+                       $this->allData[$person][$key] = $value;
                }
        }
        


Reply via email to