[ Index ]

PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008]

title

Body

[close]

/mnet/ -> lib.php (source)

   1  <?php // $Id: lib.php,v 1.16.2.8 2008/08/20 06:00:08 peterbulmer Exp $
   2  /**
   3   * Library functions for mnet
   4   *
   5   * @author  Donal McMullan  donal@catalyst.net.nz
   6   * @version 0.0.1
   7   * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
   8   * @package mnet
   9   */
  10  require_once $CFG->dirroot.'/mnet/xmlrpc/xmlparser.php';
  11  require_once $CFG->dirroot.'/mnet/peer.php';
  12  require_once $CFG->dirroot.'/mnet/environment.php';
  13  
  14  /// CONSTANTS ///////////////////////////////////////////////////////////
  15  
  16  define('RPC_OK',                0);
  17  define('RPC_NOSUCHFILE',        1);
  18  define('RPC_NOSUCHCLASS',       2);
  19  define('RPC_NOSUCHFUNCTION',    3);
  20  define('RPC_FORBIDDENFUNCTION', 4);
  21  define('RPC_NOSUCHMETHOD',      5);
  22  define('RPC_FORBIDDENMETHOD',   6);
  23  
  24  $MNET = new mnet_environment();
  25  $MNET->init();
  26  
  27  /**
  28   * Strip extraneous detail from a URL or URI and return the hostname
  29   *
  30   * @param  string  $uri  The URI of a file on the remote computer, optionally
  31   *                       including its http:// prefix like
  32   *                       http://www.example.com/index.html
  33   * @return string        Just the hostname
  34   */
  35  function mnet_get_hostname_from_uri($uri = null) {
  36      $count = preg_match("@^(?:http[s]?://)?([A-Z0-9\-\.]+).*@i", $uri, $matches);
  37      if ($count > 0) return $matches[1];
  38      return false;
  39  }
  40  
  41  /**
  42   * Get the remote machine's SSL Cert
  43   *
  44   * @param  string  $uri     The URI of a file on the remote computer, including
  45   *                          its http:// or https:// prefix
  46   * @return string           A PEM formatted SSL Certificate.
  47   */
  48  function mnet_get_public_key($uri, $application=null) {
  49      global $CFG, $MNET;
  50      // The key may be cached in the mnet_set_public_key function...
  51      // check this first
  52      $key = mnet_set_public_key($uri);
  53      if ($key != false) {
  54          return $key;
  55      }
  56  
  57      if (empty($application)) {
  58          $application = get_record('mnet_application', 'name', 'moodle');
  59      }
  60  
  61      $rq = xmlrpc_encode_request('system/keyswap', array($CFG->wwwroot, $MNET->public_key, $application->name), array("encoding" => "utf-8"));
  62      $ch = curl_init($uri . $application->xmlrpc_server_url);
  63  
  64      curl_setopt($ch, CURLOPT_TIMEOUT, 60);
  65      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  66      curl_setopt($ch, CURLOPT_POST, true);
  67      curl_setopt($ch, CURLOPT_USERAGENT, 'Moodle');
  68      curl_setopt($ch, CURLOPT_POSTFIELDS, $rq);
  69      curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: text/xml charset=UTF-8"));
  70      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  71      curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
  72  
  73      $res = xmlrpc_decode(curl_exec($ch));
  74  
  75      // check for curl errors
  76      $curlerrno = curl_errno($ch);
  77      if ($curlerrno!=0) {
  78          debugging("Request for $uri failed with curl error $curlerrno");
  79      } 
  80  
  81      // check HTTP error code
  82      $info =  curl_getinfo($ch);
  83      if (!empty($info['http_code']) and ($info['http_code'] != 200)) {
  84          debugging("Request for $uri failed with HTTP code ".$info['http_code']);
  85      }
  86  
  87      curl_close($ch);
  88  
  89      if (!is_array($res)) { // ! error
  90          $public_certificate = $res;
  91          $credentials=array();
  92          if (strlen(trim($public_certificate))) {
  93              $credentials = openssl_x509_parse($public_certificate);
  94              $host = $credentials['subject']['CN'];
  95              if (strpos($uri, $host) !== false) {
  96                  mnet_set_public_key($uri, $public_certificate);
  97                  return $public_certificate;
  98              }
  99              else {
 100                  debugging("Request for $uri returned public key for different URI - $host");
 101              }
 102          }
 103          else {
 104              debugging("Request for $uri returned empty response");
 105          }
 106      }
 107      else {
 108          debugging( "Request for $uri returned unexpected result");
 109      }
 110      return false;
 111  }
 112  
 113  /**
 114   * Store a URI's public key in a static variable, or retrieve the key for a URI
 115   *
 116   * @param  string  $uri  The URI of a file on the remote computer, including its
 117   *                       https:// prefix
 118   * @param  mixed   $key  A public key to store in the array OR null. If the key
 119   *                       is null, the function will return the previously stored
 120   *                       key for the supplied URI, should it exist.
 121   * @return mixed         A public key OR true/false.
 122   */
 123  function mnet_set_public_key($uri, $key = null) {
 124      static $keyarray = array();
 125      if (isset($keyarray[$uri]) && empty($key)) {
 126          return $keyarray[$uri];
 127      } elseif (!empty($key)) {
 128          $keyarray[$uri] = $key;
 129          return true;
 130      }
 131      return false;
 132  }
 133  
 134  /**
 135   * Sign a message and return it in an XML-Signature document
 136   *
 137   * This function can sign any content, but it was written to provide a system of
 138   * signing XML-RPC request and response messages. The message will be base64
 139   * encoded, so it does not need to be text.
 140   *
 141   * We compute the SHA1 digest of the message.
 142   * We compute a signature on that digest with our private key.
 143   * We link to the public key that can be used to verify our signature.
 144   * We base64 the message data.
 145   * We identify our wwwroot - this must match our certificate's CN
 146   *
 147   * The XML-RPC document will be parceled inside an XML-SIG document, which holds
 148   * the base64_encoded XML as an object, the SHA1 digest of that document, and a
 149   * signature of that document using the local private key. This signature will
 150   * uniquely identify the RPC document as having come from this server.
 151   *
 152   * See the {@Link http://www.w3.org/TR/xmldsig-core/ XML-DSig spec} at the W3c
 153   * site
 154   *
 155   * @param  string   $message              The data you want to sign
 156   * @param  resource $privatekey           The private key to sign the response with
 157   * @return string                         An XML-DSig document
 158   */
 159  function mnet_sign_message($message, $privatekey = null) {
 160      global $CFG, $MNET;
 161      $digest = sha1($message);
 162  
 163      // If the user hasn't supplied a private key (for example, one of our older,
 164      //  expired private keys, we get the current default private key and use that.
 165      if ($privatekey == null) {
 166          $privatekey = $MNET->get_private_key();
 167      }
 168  
 169      // The '$sig' value below is returned by reference.
 170      // We initialize it first to stop my IDE from complaining.
 171      $sig  = '';
 172      $bool = openssl_sign($message, $sig, $privatekey); // TODO: On failure?
 173  
 174      $message = '<?xml version="1.0" encoding="iso-8859-1"?>
 175      <signedMessage>
 176          <Signature Id="MoodleSignature" xmlns="http://www.w3.org/2000/09/xmldsig#">
 177              <SignedInfo>
 178                  <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
 179                  <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
 180                  <Reference URI="#XMLRPC-MSG">
 181                      <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
 182                      <DigestValue>'.$digest.'</DigestValue>
 183                  </Reference>
 184              </SignedInfo>
 185              <SignatureValue>'.base64_encode($sig).'</SignatureValue>
 186              <KeyInfo>
 187                  <RetrievalMethod URI="'.$CFG->wwwroot.'/mnet/publickey.php"/>
 188              </KeyInfo>
 189          </Signature>
 190          <object ID="XMLRPC-MSG">'.base64_encode($message).'</object>
 191          <wwwroot>'.$MNET->wwwroot.'</wwwroot>
 192          <timestamp>'.time().'</timestamp>
 193      </signedMessage>';
 194      return $message;
 195  }
 196  
 197  /**
 198   * Encrypt a message and return it in an XML-Encrypted document
 199   *
 200   * This function can encrypt any content, but it was written to provide a system
 201   * of encrypting XML-RPC request and response messages. The message will be
 202   * base64 encoded, so it does not need to be text - binary data should work.
 203   *
 204   * We compute the SHA1 digest of the message.
 205   * We compute a signature on that digest with our private key.
 206   * We link to the public key that can be used to verify our signature.
 207   * We base64 the message data.
 208   * We identify our wwwroot - this must match our certificate's CN
 209   *
 210   * The XML-RPC document will be parceled inside an XML-SIG document, which holds
 211   * the base64_encoded XML as an object, the SHA1 digest of that document, and a
 212   * signature of that document using the local private key. This signature will
 213   * uniquely identify the RPC document as having come from this server.
 214   *
 215   * See the {@Link http://www.w3.org/TR/xmlenc-core/ XML-ENC spec} at the W3c
 216   * site
 217   *
 218   * @param  string   $message              The data you want to sign
 219   * @param  string   $remote_certificate   Peer's certificate in PEM format
 220   * @return string                         An XML-ENC document
 221   */
 222  function mnet_encrypt_message($message, $remote_certificate) {
 223      global $MNET;
 224  
 225      // Generate a key resource from the remote_certificate text string
 226      $publickey = openssl_get_publickey($remote_certificate);
 227  
 228      if ( gettype($publickey) != 'resource' ) {
 229          // Remote certificate is faulty.
 230          return false;
 231      }
 232  
 233      // Initialize vars
 234      $encryptedstring = '';
 235      $symmetric_keys = array();
 236  
 237      //        passed by ref ->     &$encryptedstring &$symmetric_keys
 238      $bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));
 239      $message = $encryptedstring;
 240      $symmetrickey = array_pop($symmetric_keys);
 241  
 242      $message = '<?xml version="1.0" encoding="iso-8859-1"?>
 243      <encryptedMessage>
 244          <EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#">
 245              <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/>
 246              <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
 247                  <ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/>
 248                  <ds:KeyName>XMLENC</ds:KeyName>
 249              </ds:KeyInfo>
 250              <CipherData>
 251                  <CipherValue>'.base64_encode($message).'</CipherValue>
 252              </CipherData>
 253          </EncryptedData>
 254          <EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#">
 255              <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
 256              <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
 257                  <ds:KeyName>SSLKEY</ds:KeyName>
 258              </ds:KeyInfo>
 259              <CipherData>
 260                  <CipherValue>'.base64_encode($symmetrickey).'</CipherValue>
 261              </CipherData>
 262              <ReferenceList>
 263                  <DataReference URI="#ED"/>
 264              </ReferenceList>
 265              <CarriedKeyName>XMLENC</CarriedKeyName>
 266          </EncryptedKey>
 267          <wwwroot>'.$MNET->wwwroot.'</wwwroot>
 268      </encryptedMessage>';
 269      return $message;
 270  }
 271  
 272  /**
 273   * Get your SSL keys from the database, or create them (if they don't exist yet)
 274   *
 275   * Get your SSL keys from the database, or (if they don't exist yet) call
 276   * mnet_generate_keypair to create them
 277   *
 278   * @param   string  $string     The text you want to sign
 279   * @return  string              The signature over that text
 280   */
 281  function mnet_get_keypair() {
 282      global $CFG;
 283      static $keypair = null;
 284      if (!is_null($keypair)) return $keypair;
 285      if ($result = get_field('config_plugins', 'value', 'plugin', 'mnet', 'name', 'openssl')) {
 286          list($keypair['certificate'], $keypair['keypair_PEM']) = explode('@@@@@@@@', $result);
 287          $keypair['privatekey'] = openssl_pkey_get_private($keypair['keypair_PEM']);
 288          $keypair['publickey']  = openssl_pkey_get_public($keypair['certificate']);
 289          return $keypair;
 290      } else {
 291          $keypair = mnet_generate_keypair();
 292          return $keypair;
 293      }
 294  }
 295  
 296  /**
 297   * Generate public/private keys and store in the config table
 298   *
 299   * Use the distinguished name provided to create a CSR, and then sign that CSR
 300   * with the same credentials. Store the keypair you create in the config table.
 301   * If a distinguished name is not provided, create one using the fullname of
 302   * 'the course with ID 1' as your organization name, and your hostname (as
 303   * detailed in $CFG->wwwroot).
 304   *
 305   * @param   array  $dn  The distinguished name of the server
 306   * @return  string      The signature over that text
 307   */
 308  function mnet_generate_keypair($dn = null, $days=28) {
 309      global $CFG, $USER;
 310  
 311      // check if lifetime has been overriden
 312      if (!empty($CFG->mnetkeylifetime)) {
 313          $days = $CFG->mnetkeylifetime;
 314      }
 315  
 316      $host = strtolower($CFG->wwwroot);
 317      $host = ereg_replace("^http(s)?://",'',$host);
 318      $break = strpos($host.'/' , '/');
 319      $host   = substr($host, 0, $break);
 320  
 321      if ($result = get_record_select('course'," id ='".SITEID."' ")) {
 322          $organization = $result->fullname;
 323      } else {
 324          $organization = 'None';
 325      }
 326  
 327      $keypair = array();
 328  
 329      $country  = 'NZ';
 330      $province = 'Wellington';
 331      $locality = 'Wellington';
 332      $email    = $CFG->noreplyaddress;
 333  
 334      if(!empty($USER->country)) {
 335          $country  = $USER->country;
 336      }
 337      if(!empty($USER->city)) {
 338          $province = $USER->city;
 339          $locality = $USER->city;
 340      }
 341      if(!empty($USER->email)) {
 342          $email    = $USER->email;
 343      }
 344  
 345      if (is_null($dn)) {
 346          $dn = array(
 347             "countryName" => $country,
 348             "stateOrProvinceName" => $province,
 349             "localityName" => $locality,
 350             "organizationName" => $organization,
 351             "organizationalUnitName" => 'Moodle',
 352             "commonName" => $CFG->wwwroot,
 353             "emailAddress" => $email
 354          );
 355      }
 356  
 357      // ensure we remove trailing slashes
 358      $dn["commonName"] = preg_replace(':/$:', '', $dn["commonName"]);
 359  
 360      $new_key = openssl_pkey_new();
 361      $csr_rsc = openssl_csr_new($dn, $new_key, array('private_key_bits',2048));
 362      $selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days);
 363      unset($csr_rsc); // Free up the resource
 364  
 365      // We export our self-signed certificate to a string.
 366      openssl_x509_export($selfSignedCert, $keypair['certificate']);
 367      openssl_x509_free($selfSignedCert);
 368  
 369      // Export your public/private key pair as a PEM encoded string. You
 370      // can protect it with an optional passphrase if you wish.
 371      $export = openssl_pkey_export($new_key, $keypair['keypair_PEM'] /* , $passphrase */);
 372      openssl_pkey_free($new_key);
 373      unset($new_key); // Free up the resource
 374  
 375      return $keypair;
 376  }
 377  
 378  /**
 379   * Check that an IP address falls within the given network/mask
 380   * ok for export
 381   *
 382   * @param  string   $address        Dotted quad
 383   * @param  string   $network        Dotted quad
 384   * @param  string   $mask           A number, e.g. 16, 24, 32
 385   * @return bool
 386   */
 387  function ip_in_range($address, $network, $mask) {
 388     $lnetwork  = ip2long($network);
 389     $laddress  = ip2long($address);
 390  
 391     $binnet    = str_pad( decbin($lnetwork),32,"0","STR_PAD_LEFT" );
 392     $firstpart = substr($binnet,0,$mask);
 393  
 394     $binip     = str_pad( decbin($laddress),32,"0","STR_PAD_LEFT" );
 395     $firstip   = substr($binip,0,$mask);
 396     return(strcmp($firstpart,$firstip)==0);
 397  }
 398  
 399  /**
 400   * Check that a given function (or method) in an include file has been designated
 401   * ok for export
 402   *
 403   * @param  string   $includefile    The path to the include file
 404   * @param  string   $functionname   The name of the function (or method) to
 405   *                                  execute
 406   * @param  mixed    $class          A class name, or false if we're just testing
 407   *                                  a function
 408   * @return int                      Zero (RPC_OK) if all ok - appropriate
 409   *                                  constant otherwise
 410   */
 411  function mnet_permit_rpc_call($includefile, $functionname, $class=false) {
 412      global $CFG, $MNET_REMOTE_CLIENT;
 413  
 414      if (file_exists($CFG->dirroot . $includefile)) {
 415          include_once $CFG->dirroot . $includefile;
 416          // $callprefix matches the rpc convention
 417          // of not having a leading slash
 418          $callprefix = preg_replace('!^/!', '', $includefile);
 419      } else {
 420          return RPC_NOSUCHFILE;
 421      }
 422  
 423      if ($functionname != clean_param($functionname, PARAM_PATH)) {
 424          // Under attack?
 425          // Todo: Should really return a much more BROKEN! response
 426          return RPC_FORBIDDENMETHOD;
 427      }
 428  
 429      $id_list = $MNET_REMOTE_CLIENT->id;
 430      if (!empty($CFG->mnet_all_hosts_id)) {
 431          $id_list .= ', '.$CFG->mnet_all_hosts_id;
 432      }
 433  
 434      // TODO: change to left-join so we can disambiguate:
 435      // 1. method doesn't exist
 436      // 2. method exists but is prohibited
 437      $sql = "
 438          SELECT
 439              count(r.id)
 440          FROM
 441              {$CFG->prefix}mnet_host2service h2s,
 442              {$CFG->prefix}mnet_service2rpc s2r,
 443              {$CFG->prefix}mnet_rpc r
 444          WHERE
 445              h2s.serviceid = s2r.serviceid AND
 446              s2r.rpcid = r.id AND
 447              r.xmlrpc_path = '$callprefix/$functionname' AND
 448              h2s.hostid in ($id_list) AND
 449              h2s.publish = '1'";
 450  
 451      $permissionobj = record_exists_sql($sql);
 452  
 453      if ($permissionobj === false && 'dangerous' != $CFG->mnet_dispatcher_mode) {
 454          return RPC_FORBIDDENMETHOD;
 455      }
 456  
 457      // WE'RE LOOKING AT A CLASS/METHOD
 458      if (false != $class) {
 459          if (!class_exists($class)) {
 460              // Generate error response - unable to locate class
 461              return RPC_NOSUCHCLASS;
 462          }
 463  
 464          $object = new $class();
 465  
 466          if (!method_exists($object, $functionname)) {
 467              // Generate error response - unable to locate method
 468              return RPC_NOSUCHMETHOD;
 469          }
 470  
 471          if (!method_exists($object, 'mnet_publishes')) {
 472              // Generate error response - the class doesn't publish
 473              // *any* methods, because it doesn't have an mnet_publishes
 474              // method
 475              return RPC_FORBIDDENMETHOD;
 476          }
 477  
 478          // Get the list of published services - initialise method array
 479          $servicelist = $object->mnet_publishes();
 480          $methodapproved = false;
 481  
 482          // If the method is in the list of approved methods, set the
 483          // methodapproved flag to true and break
 484          foreach($servicelist as $service) {
 485              if (in_array($functionname, $service['methods'])) {
 486                  $methodapproved = true;
 487                  break;
 488              }
 489          }
 490  
 491          if (!$methodapproved) {
 492              return RPC_FORBIDDENMETHOD;
 493          }
 494  
 495          // Stash the object so we can call the method on it later
 496          $MNET_REMOTE_CLIENT->object_to_call($object);
 497      // WE'RE LOOKING AT A FUNCTION
 498      } else {
 499          if (!function_exists($functionname)) {
 500              // Generate error response - unable to locate function
 501              return RPC_NOSUCHFUNCTION;
 502          }
 503  
 504      }
 505  
 506      return RPC_OK;
 507  }
 508  
 509  function mnet_update_sso_access_control($username, $mnet_host_id, $accessctrl) {
 510      $mnethost = get_record('mnet_host', 'id', $mnet_host_id);
 511      if ($aclrecord = get_record('mnet_sso_access_control', 'username', $username, 'mnet_host_id', $mnet_host_id)) {
 512          // update
 513          $aclrecord->accessctrl = $accessctrl;
 514          if (update_record('mnet_sso_access_control', $aclrecord)) {
 515              add_to_log(SITEID, 'admin/mnet', 'update', 'admin/mnet/access_control.php',
 516                      "SSO ACL: $accessctrl user '$username' from {$mnethost->name}");
 517          } else {
 518              print_error('failedaclwrite', 'mnet', '', $username);
 519              return false;
 520          }
 521      } else {
 522          // insert
 523          $aclrecord->username = $username;
 524          $aclrecord->accessctrl = $accessctrl;
 525          $aclrecord->mnet_host_id = $mnet_host_id;
 526          if ($id = insert_record('mnet_sso_access_control', $aclrecord)) {
 527              add_to_log(SITEID, 'admin/mnet', 'add', 'admin/mnet/access_control.php',
 528                      "SSO ACL: $accessctrl user '$username' from {$mnethost->name}");
 529          } else {
 530              print_error('failedaclwrite', 'mnet', '', $username);
 531              return false;
 532          }
 533      }
 534      return true;
 535  }
 536  
 537  function mnet_get_peer_host ($mnethostid) {
 538      static $hosts;
 539      if (!isset($hosts[$mnethostid])) {
 540          $host = get_record('mnet_host', 'id', $mnethostid);
 541          $hosts[$mnethostid] = $host;
 542      }
 543      return $hosts[$mnethostid];
 544  }
 545  
 546  /**
 547   * Inline function to modify a url string so that mnet users are requested to
 548   * log in at their mnet identity provider (if they are not already logged in)
 549   * before ultimately being directed to the original url.
 550   *
 551   * uses global MNETIDPJUMPURL the url which user should initially be directed to
 552   *     MNETIDPJUMPURL is a URL associated with a moodle networking peer when it
 553   *     is fulfiling a role as an identity provider (IDP). Different urls for
 554   *     different peers, the jumpurl is formed partly from the IDP's webroot, and
 555   *     partly from a predefined local path within that webwroot.
 556   *     The result of the user hitting MNETIDPJUMPURL is that they will be asked
 557   *     to login (at their identity provider (if they aren't already)), mnet
 558   *     will prepare the necessary authentication information, then redirect
 559   *     them back to somewhere at the content provider(CP) moodle (this moodle)
 560   * @param array $url array with 2 elements
 561   *     0 - context the url was taken from, possibly just the url, possibly href="url"
 562   *     1 - the destination url
 563   * @return string the url the remote user should be supplied with.
 564   */
 565  function mnet_sso_apply_indirection ($url) {
 566      global $MNETIDPJUMPURL;
 567  
 568      $localpart='';
 569      $urlparts = parse_url($url[1]);
 570      if($urlparts) {
 571          if (isset($urlparts['path'])) {
 572              $localpart .= $urlparts['path'];
 573          }
 574          if (isset($urlparts['query'])) {
 575              $localpart .= '?'.$urlparts['query'];
 576          }
 577          if (isset($urlparts['fragment'])) {
 578              $localpart .= '#'.$urlparts['fragment'];
 579          }
 580      }
 581      $indirecturl = $MNETIDPJUMPURL . urlencode($localpart);
 582      //If we matched on more than just a url (ie an html link), return the url to an href format
 583      if ($url[0] != $url[1]) {
 584          $indirecturl = 'href="'.$indirecturl.'"';
 585      }
 586      return $indirecturl;
 587  }
 588  
 589  ?>


Generated: Wed Jan 14 11:33:29 2009 Cross-referenced by PHPXref 0.7