<?php

if(class_exists('phRETS')) {
  return;
}

class phRETS {

  /**
   *  PHRETS - PHP library for RETS
   *  version 1.0rc2 (release candidate 2)  please send error reports and/or feedback to troy.davisson@gmail.com
   *  http://troda.com/projects/phrets/
   *  Copyright (C) 2007-2010 Troy Davisson
   *
   *  All rights reserved.
   *  Permission is hereby granted, free of charge, to use, copy or modify this software.  Use at your own risk.
   *
   *  This library is divided into 2 sections: high level and low level
   *    High level: Helpful functions that take much of the burden out of processing RETS data
   *    Low level: Framework for communicating with a RETS server.  High level functions sit on top of these
   *
   */

  public $err;
  public $capability_url = array();
  private $ch;
  private $server_hostname;
  private $server_port;
  private $server_protocol;
  private $server_version;
  private $server_software;
  private $static_headers = array();
  private $server_information = array();
  private $cookie_file = "";
  private $debug_file = "rets_debug.txt";
  private $debug_mode;
  private $allowed_capabilities = array(
    "Action" => 1,
    "ChangePassword" => 1,
    "GetObject" => 1,
    "Login" => 1,
    "LoginComplete" => 1,
    "Logout" => 1,
    "Search" => 1,
    "GetMetadata" => 1,
    "ServerInformation" => 1,
    "Update" => 1
    );
  private $last_request = array();
  private $auth_support_basic = false;
  private $auth_support_digest = false;
  private $last_response_headers = array();
  private $last_response_headers_raw = "";
  private $compression_enabled = false;
  private $ua_pwd = "";
  private $ua_auth = false;
  private $request_id = "";
  private $disable_follow_location = false;
  private $force_basic_authentication = false;
  private $use_interealty_ua_auth = false;
  private $int_result_pointer = 0;
  public $error_info = array();
  private $last_request_url;
  private $last_server_response;
  private $session_id;
  private $catch_last_response = false;
  private $disable_encoding_fix = false;
  private $offset_support = false;
  private $override_offset_protection = false;



  public function phRETS() { }


  public function GetLastServerResponse() {
    return $this->last_server_response;
  }


  public function FirewallTest() {
    $google = $this->FirewallTestConn("google.com", 80);
    $crt80 = $this->FirewallTestConn("demo.crt.realtors.org", 80);
    $crt6103 = $this->FirewallTestConn("demo.crt.realtors.org", 6103);
    $flexmls80 = $this->FirewallTestConn("retsgw.flexmls.com", 80);
    $flexmls6103 = $this->FirewallTestConn("retsgw.flexmls.com", 6103);

    if (!$google && !$crt80 && !$crt6103 && !$flexmls80 && !$flexmls6103) {
      echo "Firewall Result: All tests failed.  Possible causes:";
      echo "<ol>";
      echo "<li>Firewall is blocking your outbound connections</li>";
      echo "<li>You aren't connected to the internet</li>";
      echo "</ol>";
      return false;
    }

    if (!$crt6103 && !$flexmls6103) {
      echo "Firewall Result: All port 6103 tests failed.  ";
      echo "Likely cause: Firewall is blocking your outbound connections on port 6103.";
      return false;
    }

    if ($google && $crt6103 && $crt80 && $flexmls6103 && $flexmls80) {
      echo "Firewall Result: All tests passed.";
      return true;
    }

    if (($crt6103 && !$flexmls6103) || (!$crt6103 && $flexmls6103)) {
      echo "Firewall Result: At least one port 6103 test passed.  ";
      echo "Likely cause: One of the test servers might be down but connections on port 80 and port 6103 should work.";
      return true;
    }

    if (!$google || !$crt80 || !$flexmls80) {
      echo "Firewall Result: At least one port 80 test failed.  ";
      echo "Likely cause: One of the test servers might be down.";
      return true;
    }

    echo "Firewall Results: Unable to guess the issue.  See individual test results above.";
    return false;

  }


  private function FirewallTestConn($hostname, $port = 6103) {
    $fp = @fsockopen($hostname, $port, $errno, $errstr, 5);

    if (!$fp) {
      echo "Firewall Test: {$hostname}:{$port} FAILED<br>\n";
      return false;
    }
    else {
      @fclose($fp);
      echo "Firewall Test: {$hostname}:{$port} GOOD<br>\n";
      return true;
    }

  }


  public function GetObject($resource, $type, $id, $photo_number = '*', $location = 0) {
    $this->reset_error_info();
    $return_photos = array();

    if (empty($resource)) {
      die("Resource parameter is required for GetObject() request.");
    }
    if (empty($type)) {
      die("Type parameter is required for GetObject() request.");
    }
    if (empty($id)) {
      die("ID parameter is required for GetObject() request.");
    }
    if (empty($this->capability_url['GetObject'])) {
      die("GetObject() called but unable to find GetObject location.  Failed login?\n");
    }

    $send_id = "";
    $send_numb = "";

    // check if $photo_number needs fixing
    if (preg_match('/\,/', $photo_number)) {
      // change the commas to colons for the request
      $photo_number = preg_replace('/\,/', ':', $photo_number);
    }

    if (preg_match('/\:/', $photo_number)) {
      // photo number contains multiple objects
      // chopping and cleaning
      $requested_numbers = explode(":", $photo_number);
      if (is_array($requested_numbers)) {
        foreach ($requested_numbers as $numb) {
          $numb = trim($numb);
          if (!empty($numb) || $numb == "0") {
            $send_numb .= "{$numb}:";
          }
        }
      }
      $send_numb = preg_replace('/\:$/', '', $send_numb);
    }
    else {
      $send_numb = trim($photo_number);
    }

    if (preg_match('/\,/', $id)) {
      // id contains multiple objects.
      // chopping and combining with photo_number
      $requested_ids = explode(",", $id);
      if (is_array($requested_ids)) {
        foreach ($requested_ids as $req_id) {
          $req_id = trim($req_id);
          if (!empty($req_id) && $req_id != "0") {
            $send_id .= "{$req_id}:{$send_numb},";
          }
        }
      }
      $send_id = preg_replace('/\,$/', '', $send_id);
    }
    else {
      $send_id = trim($id).':'.$send_numb;
    }

    // make request
    $result = $this->RETSRequest($this->capability_url['GetObject'], array('Resource' => $resource, 'Type' => $type, 'ID' => $send_id, 'Location' => $location));
    if (!$result) {
      return false;
    }
    list($headers, $body) = $result;

    // fix case issue if exists
    if (isset($this->last_response_headers['Content-type']) && !isset($this->last_response_headers['Content-Type'])) {
      $this->last_response_headers['Content-Type'] = $this->last_response_headers['Content-type'];
    }

    if (!isset($this->last_response_headers['Content-Type'])) {
      $this->last_response_headers['Content-Type'] = "";
    }

    // check what type of response came back
    if (preg_match('/multipart/', $this->last_response_headers['Content-Type'])) {

      // help bad responses be more multipart compliant
      $body = "\r\n{$body}\r\n";

      // multipart
      preg_match('/boundary\=\"(.*?)\"/', $this->last_response_headers['Content-Type'], $matches);
      if (isset($matches[1])) {
        $boundary = $matches[1];
      }
      else {
        preg_match('/boundary\=(.*?)(\s|$|\;)/', $this->last_response_headers['Content-Type'], $matches);
        $boundary = $matches[1];
      }
      // strip quotes off of the boundary
      $boundary = preg_replace('/^\"(.*?)\"$/', '\1', $boundary);

      // clean up the body to remove a reamble and epilogue
      $body = preg_replace('/^(.*?)\r\n--'.$boundary.'\r\n/', "\r\n--{$boundary}\r\n", $body);
      // make the last one look like the rest for easier parsing
      $body = preg_replace('/\r\n--'.$boundary.'--/', "\r\n--{$boundary}\r\n", $body);

      // cut up the message
      $multi_parts = array();
      $multi_parts = explode("\r\n--{$boundary}\r\n", $body);
      // take off anything that happens before the first boundary (the preamble)
      array_shift($multi_parts);
      // take off anything after the last boundary (the epilogue)
      array_pop($multi_parts);

      // go through each part of the multipart message
      foreach ($multi_parts as $part) {
        // default to processing headers
        $on_headers = true;
        $on_body = false;
        $first_body_found = false;
        $this_photo = array();

        // go through the multipart chunk line-by-line
        $body_parts = array();
        $body_parts = explode("\r\n", $part);
        $this_photo['Data'] = "";
        foreach ($body_parts as $line) {
          if (empty($line) && $on_headers == true) {
            // blank line.  switching to processing a body and moving on
            $on_headers = false;
            $on_body = true;
            continue;
          }
          if ($on_headers == true) {
            // non blank line and we're processing headers so save the header
            @list($header,$value) = explode(':',$line,2);
            $header = trim($header);
            $value = trim($value);
            if (!empty($header)) {
              if ($header == "Description") {
                // for servers where the implementors didn't read the next word in the RETS spec.
                // 'Description' is the BNF term. Content-Description is the correct header.
                // fixing for sanity
                $header = "Content-Description";
              }
              // fix case issue if exists
              if ($header == "Content-type") {
                $header = "Content-Type";
              }
              $this_photo[$header] = $value;
            }
          }
          if ($on_body == true) {
            if ($first_body_found == true) {
              // here again because a linebreak in the body section which was cut out in the explode
              // add the CRLF back
              $this_photo['Data'] .= "\r\n";
            }
            // non blank line and we're processing a body so save the line as part of Data
            $first_body_found = true;
            $this_photo['Data'] .= $line;
          }
        }
        // done with parsing out the multipart response
        // check for errors and finish up

        $this_photo['Success'] = true; // assuming for now

        if (preg_match('/xml/', $this_photo['Content-Type'])) {
          // this multipart might include a RETS error
          $xml = $this->ParseXMLResponse($this_photo['Data']);

          if ($xml['ReplyCode'] == 0 || empty($this_photo['Data'])) {
            // success but no body
            $this_photo['Success'] = true;
          }
          else {
            // RETS error in this multipart section
            $this_photo['Success'] = false;
            $this_photo['ReplyCode'] = "{$xml['ReplyCode']}";
            $this_photo['ReplyText'] = "{$xml['ReplyText']}";
          }
        }

        // add information about this multipart to the returned array
        $return_photos[] = $this_photo;
      }
    }
    else {
      // all we know is that the response wasn't a multipart so it's either a single photo or error
      $this_photo = array();

      $this_photo['Success'] = true; // assuming for now
      if (isset($this->last_response_headers['Content-ID'])) {
        $this_photo['Content-ID'] = $this->last_response_headers['Content-ID'];
      }
      if (isset($this->last_response_headers['Object-ID'])) {
        $this_photo['Object-ID'] = $this->last_response_headers['Object-ID'];
      }
      if (isset($this->last_response_headers['Content-Type'])) {
        $this_photo['Content-Type'] = $this->last_response_headers['Content-Type'];
      }
      if (isset($this->last_response_headers['MIME-Version'])) {
        $this_photo['MIME-Version'] = $this->last_response_headers['MIME-Version'];
      }
      if (isset($this->last_response_headers['Location'])) {
        $this_photo['Location'] = $this->last_response_headers['Location'];
      }
      if (isset($this->last_response_headers['Preferred'])) {
        $this_photo['Preferred'] = $this->last_response_headers['Preferred'];
      }

      if (isset($this->last_response_headers['Description'])) {
        if (!empty($this->last_response_headers['Description'])) {
          // for servers where the implementors didn't read the next word in the RETS spec.
          // 'Description' is the BNF term. Content-Description is the correct header.
          // fixing for sanity
          $this_photo['Content-Description'] = $this->last_response_headers['Description'];
        }
      }
      if (isset($this->last_response_headers['Content-Description'])) {
        $this_photo['Content-Description'] = $this->last_response_headers['Content-Description'];
      }

      $this_photo['Length'] = strlen($body);
      $this_photo['Data'] = $body;

      if (isset($this->last_response_headers['Content-Type'])) {
        if (preg_match('/xml/', $this->last_response_headers['Content-Type'])) {
          // RETS error maybe?
          $xml = $this->ParseXMLResponse($body);

          if ($xml['ReplyCode'] == 0 || empty($body)) {
            // false alarm.  we're good
            $this_photo['Success'] = true;
          }
          else {
            // yes, RETS error
            $this->last_request['ReplyCode'] = "{$xml['ReplyCode']}";
            $this->last_request['ReplyText'] = "{$xml['ReplyText']}";
            $this_photo['ReplyCode'] = "{$xml['ReplyCode']}";
            $this_photo['ReplyText'] = "{$xml['ReplyText']}";
            $this_photo['Success'] = false;
          }
        }
      }

      // add information about this photo to the returned array
      $return_photos[] = $this_photo;
    }

    // return everything
    return $return_photos;
  }


  public function IsMaxrowsReached($pointer_id = "") {
    if (empty($pointer_id)) {
      $pointer_id = $this->int_result_pointer;
    }
    return $this->search_data[$pointer_id]['maxrows_reached'];
  }


  public function TotalRecordsFound($pointer_id = "") {
    if (empty($pointer_id)) {
      $pointer_id = $this->int_result_pointer;
    }
    return $this->search_data[$pointer_id]['total_records_found'];
  }


  public function NumRows($pointer_id = "") {
    if (empty($pointer_id)) {
      $pointer_id = $this->int_result_pointer;
    }
    return $this->search_data[$pointer_id]['last_search_returned'];
  }


  public function SearchGetFields($pointer_id) {
    if (!empty($pointer_id)) {
      return $this->search_data[$pointer_id]['column_names'];
    }
    else {
      return false;
    }
  }


  public function FreeResult($pointer_id) {
    if (!empty($pointer_id)) {
      unset($this->search_data[$pointer_id]['data']);
      unset($this->search_data[$pointer_id]['delimiter_character']);
      unset($this->search_data[$pointer_id]['column_names']);
      return true;
    }
    else {
      return false;
    }
  }


  public function FetchRow($pointer_id) {

    $this_row = false;

    if (!empty($pointer_id)) {

      if (isset($this->search_data[$pointer_id]['data'])) {
        $field_data = current($this->search_data[$pointer_id]['data']);
        next($this->search_data[$pointer_id]['data']);
      }

      if (!empty($field_data)) {
        $this_row = array();

        // split up DATA row on delimiter found earlier
        $field_data = preg_replace("/^{$this->search_data[$pointer_id]['delimiter_character']}/", "", $field_data);
        $field_data = preg_replace("/{$this->search_data[$pointer_id]['delimiter_character']}\$/", "", $field_data);
        $field_data = explode($this->search_data[$pointer_id]['delimiter_character'], $field_data);

        foreach ($this->search_data[$pointer_id]['column_names'] as $key => $name) {
          // assign each value to it's name retrieved in the COLUMNS earlier
          $this_row[$name] = $field_data[$key];
        }
      }
    }

    return $this_row;

  }


  public function SearchQuery($resource, $class, $query = "", $optional_params = array()) {
    $this->reset_error_info();

    if (empty($resource)) {
      die("Resource parameter is required in SearchQuery() request.");
    }
    if (empty($class)) {
      die("Class parameter is required in SearchQuery() request.");
    }
    if (empty($this->capability_url['Search'])) {
      die("SearchQuery() called but unable to find Search location.  Failed login?\n");
    }

    $this->int_result_pointer++;
    $this->search_data[$this->int_result_pointer]['last_search_returned'] = 0;
    $this->search_data[$this->int_result_pointer]['total_records_found'] = 0;
    $this->search_data[$this->int_result_pointer]['column_names'] = "";
    $this->search_data[$this->int_result_pointer]['delimiter_character'] = "";
    $this->search_data[$this->int_result_pointer]['search_requests'] = 0;

    // setup request arguments
    $search_arguments = array();

    $search_arguments['SearchType'] = $resource;
    $search_arguments['Class'] = $class;

    // due to a lack of forward-thinking, reversing a previous decision
    // check if the query passed is missing the outer parenthesis
    // if so, add them
    if (empty($query)) {
      // do nothing.  http://retsdoc.onconfluence.com/display/rcpcenter/RCP+80+-+Optional+Query
    }
    elseif ($query == "*" || preg_match('/^\((.*)\)$/', $query)) {
      $search_arguments['Query'] = $query;
    }
    else {
      $search_arguments['Query'] = '('.$query.')';
    }


    if (isset($search_arguments['Query'])) {
      $search_arguments['QueryType'] = "DMQL2";
    }

    // setup additional, optional request arguments
    $search_arguments['Count'] = empty($optional_params['Count']) ? 1 : $optional_params['Count'];
    $search_arguments['Format'] = empty($optional_params['Format']) ? "COMPACT-DECODED" : $optional_params['Format'];
    $search_arguments['Limit'] = empty($optional_params['Limit']) ? 99999999 : $optional_params['Limit'];

    if (!empty($optional_params['Offset'])) {
      $search_arguments['Offset'] = $optional_params['Offset'];
    }
    elseif ($this->offset_support && empty($optional_params['Offset'])) {
      // start auto-offset looping with Offset at 1
      $search_arguments['Offset'] = 1;
    }
    else { }
    
    if (!empty($optional_params['Select'])) {
      $search_arguments['Select'] = $optional_params['Select'];
    }
    if (!empty($optional_params['RestrictedIndicator'])) {
      $search_arguments['RestrictedIndicator'] = $optional_params['RestrictedIndicator'];
    }

    $search_arguments['StandardNames'] = empty($optional_params['StandardNames']) ? 0 : $optional_params['StandardNames'];

    $continue_searching = true; // Keep searching if MAX ROWS is reached and offset_support is true
    while ($continue_searching) {

      $this->search_data[$this->int_result_pointer]['maxrows_reached'] = false;
      $this->search_data[$this->int_result_pointer]['search_requests']++;

      if ($this->search_data[$this->int_result_pointer]['search_requests'] == 300 && !$this->override_offset_protection) {
        // this call for SearchQuery() has resulted in X number of search requests
        // which is considered excessive.  stopping the process in order to prevent
        // abuse against the server.  almost ALWAYS happens when the user thinks Offset
        // is supported by the server when it's actually NOT supported
        $this->set_error_info("phrets", -1, "Last SearchQuery() has resulted in 300+ requests to the server.  Stopping to prevent abuse");
        return false;
      }
      
      // make request
      $result = $this->RETSRequest($this->capability_url['Search'], $search_arguments);
      if (!$result) {
        return false;
      }
      list($headers, $body) = $result;

      $body = $this->fix_encoding($body);

      $xml = $this->ParseXMLResponse($body);
      if (!$xml) {
        return false;
      }

      // log replycode and replytext for reference later
      $this->last_request['ReplyCode'] = "{$xml['ReplyCode']}";
      $this->last_request['ReplyText'] = "{$xml['ReplyText']}";

      if ($xml['ReplyCode'] != 0) {
        $this->set_error_info("rets", "{$xml['ReplyCode']}", "{$xml['ReplyText']}");
        return false;
      }

      if (isset($xml->DELIMITER)) {
        // delimiter found so we have at least a COLUMNS row to parse
        $delimiter_character = chr("{$xml->DELIMITER->attributes()->value}");
        $this->search_data[$this->int_result_pointer]['delimiter_character'] = $delimiter_character;
        $column_names = "{$xml->COLUMNS[0]}";
        $column_names = preg_replace("/^{$delimiter_character}/", "", $column_names);
        $column_names = preg_replace("/{$delimiter_character}\$/", "", $column_names);
        $this->search_data[$this->int_result_pointer]['column_names'] = explode($delimiter_character, $column_names);
      }

      if (isset($xml->DATA)) {
        foreach ($xml->DATA as $key) {
          $field_data = "{$key}";
          // split up DATA row on delimiter found earlier
          $this->search_data[$this->int_result_pointer]['data'][] = $field_data;
          $this->search_data[$this->int_result_pointer]['last_search_returned']++;
        }
      }

      if (isset($xml->MAXROWS)) {
        // MAXROWS tag found.  the RETS server withheld records.
        // if the server supports Offset, more requests can be sent to page through results
        // until this tag isn't found anymore.
        $this->search_data[$this->int_result_pointer]['maxrows_reached'] = true;
      }

      if (isset($xml->COUNT)) {
        // found the record count returned.  save it
        $this->search_data[$this->int_result_pointer]['total_records_found'] = "{$xml->COUNT->attributes()->Records}";
      }

      if (isset($xml)) {
        unset($xml);
      }

      if ($this->IsMaxrowsReached($this->int_result_pointer) && $this->offset_support) {
        $continue_searching = true;
        $search_arguments['Offset'] = $this->NumRows($this->int_result_pointer) + 1;
      }
      else {
        $continue_searching = false;
      }
    }

    return $this->int_result_pointer;
  }


  public function Search($resource, $class, $query = "", $optional_params = array()) {
    $data_table = array();
    
    $int_result_pointer = $this->SearchQuery($resource, $class, $query, $optional_params);
    
    while ($row = $this->FetchRow($int_result_pointer)) {
      $data_table[] = $row;
    }
    
    return $data_table;
  }


  public function GetAllLookupValues($resource) {
    $this->reset_error_info();

    if (empty($resource)) {
      die("Resource parameter is required in GetAllLookupValues() request.");
    }
    if (empty($this->capability_url['GetMetadata'])) {
      die("GetAllLookupValues() called but unable to find GetMetadata location.  Failed login?\n");
    }

    // make request
    $result = $this->RETSRequest($this->capability_url['GetMetadata'], array('Type' => 'METADATA-LOOKUP_TYPE', 'ID' => $resource.':*', 'Format' => 'STANDARD-XML'));
    if (!$result) {
      return false;
    }
    list($headers, $body) = $result;

    $xml = $this->ParseXMLResponse($body);
    if (!$xml) {
      return false;
    }

    if ($xml['ReplyCode'] != 0) {
      $this->set_error_info("rets", "{$xml['ReplyCode']}", "{$xml['ReplyText']}");
      return false;
    }

    $this_table = array();

    // parse XML into a nice array
    if ($xml->METADATA) {

      foreach ($xml->METADATA->{'METADATA-LOOKUP_TYPE'} as $key) {
        if (!empty($key->attributes()->Lookup)) {
          $this_lookup = array();

          $lookup_xml_array = array();
          if ($this->server_version == "RETS/1.7.2") {
            $lookup_xml_array = $key->LookupType;
          }
          else {
            $lookup_xml_array = $key->Lookup;
          }

          foreach ($lookup_xml_array as $look) {
            $metadataentryid = isset($look->MetadataEntryID) ? "{$look->MetadataEntryID}" : "";
            $value = isset($look->Value) ? "{$look->Value}" : "";
            $shortvalue = isset($look->ShortValue) ? "{$look->ShortValue}" : "";
            $longvalue = isset($look->LongValue) ? "{$look->LongValue}" : "";

            $this_lookup[] = array('MetadataEntryID' => $metadataentryid, 'Value' => $value, 'ShortValue' => $shortvalue,
                        'LongValue' => $longvalue);
          }
          $this_table[] = array('Lookup' => "{$key->attributes()->Lookup}", 'Values' => $this_lookup);
        }
      }
    }

    // return the big array
    return $this_table;
  }


  public function GetLookupValues($resource, $lookupname) {
    $this->reset_error_info();

    if (empty($resource)) {
      die("Resource parameter is required in GetLookupValues() request.");
    }
    if (empty($lookupname)) {
      die("Lookup Name parameter is required in GetLookupValues() request.");
    }
    if (empty($this->capability_url['GetMetadata'])) {
      die("GetLookupValues() called but unable to find GetMetadata location.  Failed login?\n");
    }

    // make request
    $result = $this->RETSRequest($this->capability_url['GetMetadata'], array('Type' => 'METADATA-LOOKUP_TYPE', 'ID' => $resource.':'.$lookupname, 'Format' => 'STANDARD-XML'));
    if (!$result) {
      return false;
    }
    list($headers, $body) = $result;

    $xml = $this->ParseXMLResponse($body);
    if (!$xml) {
      return false;
    }

    if ($xml['ReplyCode'] != 0) {
      $this->set_error_info("rets", "{$xml['ReplyCode']}", "{$xml['ReplyText']}");
      return false;
    }

    $this_table = array();

    // parse XML into a nice array
    if ($xml->METADATA) {

      $lookup_xml_array = array();
      if ($this->server_version == "RETS/1.7.2") {
        $lookup_xml_array = $xml->METADATA->{'METADATA-LOOKUP_TYPE'}->LookupType;
      }
      else {
        $lookup_xml_array = $xml->METADATA->{'METADATA-LOOKUP_TYPE'}->Lookup;
      }

      foreach ($lookup_xml_array as $key) {
        if (isset($key->Value)) {

          $metadataentryid = isset($key->MetadataEntryID) ? "{$key->MetadataEntryID}" : "";
          $value = isset($key->Value) ? "{$key->Value}" : "";
          $shortvalue = isset($key->ShortValue) ? "{$key->ShortValue}" : "";
          $longvalue = isset($key->LongValue) ? "{$key->LongValue}" : "";

          $this_table[] = array('MetadataEntryID' => $metadataentryid, 'Value' => $value, 'ShortValue' => $shortvalue,
                      'LongValue' => $longvalue);
        }
      }
    }

    // return the big array
    return $this_table;
  }


  public function GetMetadataResources($id = 0) {
    $this->reset_error_info();

    if (empty($this->capability_url['GetMetadata'])) {
      die("GetMetadataResources() called but unable to find GetMetadata location.  Failed login?\n");
    }

    // make request
    $result = $this->RETSRequest($this->capability_url['GetMetadata'], array('Type' => 'METADATA-RESOURCE', 'ID' => $id, 'Format' => 'STANDARD-XML'));
    if (!$result) {
      return false;
    }
    list($headers, $body) = $result;

    $xml = $this->ParseXMLResponse($body);
    if (!$xml) {
      return false;
    }

    if ($xml['ReplyCode'] != 0) {
      $this->set_error_info("rets", "{$xml['ReplyCode']}", "{$xml['ReplyText']}");
      return false;
    }

    $this_resource = array();

    // parse XML into a nice array
    if ($xml->METADATA) {
      foreach ($xml->METADATA->{'METADATA-RESOURCE'}->Resource as $key => $value) {
        $this_resource["{$value->ResourceID}"] = array('ResourceID' => "{$value->ResourceID}", 'StandardName'=>"{$value->StandardName}", 'VisibleName' => "{$value->VisibleName}",
                              'Description' => "{$value->Description}", 'KeyField' => "{$value->KeyField}", 'ClassCount' => "{$value->ClassCount}",
                              'ClassVersion' => "{$value->ClassVersion}", 'ClassDate' => "{$value->ClassDate}", 'ObjectVersion' => "{$value->ObjectVersion}",
                              'ObjectDate' => "{$value->ObjectDate}", 'SearchHelpVersion' => "{$value->SearchHelpVersion}",
                              'SearchHelpDate' => "{$value->SearchHelpDate}", 'EditMaskVersion' => "{$value->EditMaskVersion}",
                              'EditMaskDate' => "{$value->EditMaskDate}", 'LookupVersion' => "{$value->LookupVersion}", 'LookupDate' => "{$value->LookupDate}",
                              'UpdateHelpVersion' => "{$value->UpdateHelpVersion}", 'UpdateHelpDate' => "{$value->UpdateHelpDate}",
                              'ValidationExpressionVersion' => "{$value->ValidationExpressionVersion}", 'ValidationExpressionDate' => "{$value->ValidationExpressionDate}",
                              'ValidationLookupVersion' => "{$value->ValidationLookupVersion}", 'ValidationLookupDate' => "{$value->ValidationLookupDate}",
                              'ValidationExternalVersion' => "{$value->ValidationExternalVersion}", 'ValidationExternalDate' => "{$value->ValidationExternalDate}");
      }
    }

    // send back array
    return $this_resource;
  }


  public function GetMetadataInfo($id = 0) {
    if (empty($this->capability_url['GetMetadata'])) {
      die("GetMetadataInfo() called but unable to find GetMetadata location.  Failed login?\n");
    }
    return $this->GetMetadataResources($id);
  }


  public function GetMetadataTable($resource, $class) {
    $this->reset_error_info();

    $id = $resource.':'.$class;
    if (empty($resource)) {
      die("Resource parameter is required in GetMetadata() request.");
    }
    if (empty($class)) {
      die("Class parameter is required in GetMetadata() request.");
    }
    if (empty($this->capability_url['GetMetadata'])) {
      die("GetMetadataTable() called but unable to find GetMetadata location.  Failed login?\n");
    }

    // request specific metadata
    $result = $this->RETSRequest($this->capability_url['GetMetadata'], array('Type' => 'METADATA-TABLE', 'ID' => $id, 'Format' => 'STANDARD-XML'));
    if (!$result) {
      return false;
    }
    list($headers, $body) = $result;

    $xml = $this->ParseXMLResponse($body);
    if (!$xml) {
      return false;
    }

    // log replycode and replytext for reference later
    $this->last_request['ReplyCode'] = "{$xml['ReplyCode']}";
    $this->last_request['ReplyText'] = "{$xml['ReplyText']}";

    if ($xml['ReplyCode'] != 0) {
      $this->set_error_info("rets", "{$xml['ReplyCode']}", "{$xml['ReplyText']}");
      return false;
    }

    $this_table = array();

    // parse XML into a nice array
    if ($xml->METADATA) {
      foreach ($xml->METADATA->{'METADATA-TABLE'}->Field as $key) {
        $this_table[] = array('SystemName' => "{$key->SystemName}", 'StandardName' => "{$key->StandardName}", 'LongName' => "{$key->LongName}", 'DBName' => "{$key->DBName}",
                  'ShortName' => "{$key->ShortName}",  'MaximumLength' => "{$key->MaximumLength}", 'DataType' => "{$key->DataType}", 'Precision' => "{$key->Precision}",
                  'Searchable' => "{$key->Searchable}", 'Interpretation' => "{$key->Interpretation}", 'Alignment' => "{$key->Alignment}",
                  'UseSeparator' => "{$key->UseSeparator}", 'EditMaskID' => "{$key->EditMaskID}", 'LookupName' => "{$key->LookupName}",
                  'MaxSelect' => "{$key->MaxSelect}", 'Units' => "{$key->Units}", 'Index' => "{$key->Index}", 'Minimum' => "{$key->Minimum}",
                  'Maximum' => "{$key->Maximum}", 'Default' => "{$key->Default}", 'Required' => "{$key->Required}", 'SearchHelpID' => "{$key->SearchHelpID}",
                  'Unique' => "{$key->Unique}", 'MetadataEntryID' => "{$key->MetadataEntryID}", 'ModTimeStamp' => "{$key->ModTimeStamp}",
                  'ForeignKeyName' => "{$key->ForiengKeyName}", 'ForeignField' => "{$key->ForeignField}", 'InKeyIndex' => "{$key->InKeyIndex}");
      }
    }

    // return the big array
    return $this_table;
  }


  public function GetMetadata($resource, $class) {
    if (empty($this->capability_url['GetMetadata'])) {
      die("GetMetadata() called but unable to find GetMetadata location.  Failed login?\n");
    }
    return $this->GetMetadataTable($resource, $class);
  }


  public function GetMetadataObjects($id) {
    $this->reset_error_info();

    if (empty($id)) {
      die("ID parameter is required in GetMetadataObjects() request.");
    }
    if (empty($this->capability_url['GetMetadata'])) {
      die("GetMetadataObjects() called but unable to find GetMetadata location.  Failed login?\n");
    }

    // request basic metadata information
    $result = $this->RETSRequest($this->capability_url['GetMetadata'], array('Type' => 'METADATA-OBJECT', 'ID' => $id, 'Format' => 'STANDARD-XML'));
    if (!$result) {
      return false;
    }
    list($headers, $body) = $result;

    $xml = $this->ParseXMLResponse($body);
    if (!$xml) {
      return false;
    }

    // log replycode and replytext for reference later
    $this->last_request['ReplyCode'] = "{$xml['ReplyCode']}";
    $this->last_request['ReplyText'] = "{$xml['ReplyText']}";

    if ($xml['ReplyCode'] != 0) {
      $this->set_error_info("rets", "{$xml['ReplyCode']}", "{$xml['ReplyText']}");
      return false;
    }

    $return_data = array();

    if (isset($xml->METADATA->{'METADATA-OBJECT'})) {
      // parse XML into a nice array
      foreach ($xml->METADATA->{'METADATA-OBJECT'} as $key => $value) {
        foreach ($value->Object as $key) {
          if (!empty($key->ObjectType)) {
            $return_data[] = array('MetadataEntryID' => "{$key->MetadataEntryID}", 'VisibleName' => "{$key->VisibleName}", 'ObjectTimeStamp' => "{$key->ObjectTimeStamp}",
                      'ObjectCount' => "{$key->ObjectCount}", 'ObjectType' => "{$key->ObjectType}", 'StandardName' => "{$key->StandardName}",
                      'MimeType' => "{$key->MimeType}", 'Description' => "{$key->Description}");
          }
        }
      }
    }

    // send back array
    return $return_data;
  }


  public function GetMetadataClasses($id) {
    $this->reset_error_info();

    if (empty($id)) {
      die("ID parameter is required in GetMetadataClasses() request.");
    }
    if (empty($this->capability_url['GetMetadata'])) {
      die("GetMetadataClasses() called but unable to find GetMetadata location.  Failed login?\n");
    }

    // request basic metadata information
    $result = $this->RETSRequest($this->capability_url['GetMetadata'], array('Type' => 'METADATA-CLASS', 'ID' => $id, 'Format' => 'STANDARD-XML'));
    if (!$result) {
      return false;
    }
    list($headers, $body) = $result;

    $xml = $this->ParseXMLResponse($body);
    if (!$xml) {
      return false;
    }

    // log replycode and replytext for reference later
    $this->last_request['ReplyCode'] = "{$xml['ReplyCode']}";
    $this->last_request['ReplyText'] = "{$xml['ReplyText']}";

    if ($xml['ReplyCode'] != 0) {
      $this->set_error_info("rets", "{$xml['ReplyCode']}", "{$xml['ReplyText']}");
      return false;
    }

    $return_data = array();

    // parse XML into a nice array
    if ($xml->METADATA) {
      foreach ($xml->METADATA->{'METADATA-CLASS'} as $key => $value) {
        foreach ($value->Class as $key) {
          if (!empty($key->ClassName)) {
            $return_data[] = array('ClassName' => "{$key->ClassName}", 'VisibleName' => "{$key->VisibleName}", 'StandardName' => "{$key->StandardName}",
                      'Description' => "{$key->Description}", 'TableVersion' => "{$key->TableVersion}", 'TableDate' => "{$key->TableDate}",
                      'UpdateVersion' => "{$key->UpdateVersion}", 'UpdateDate' => "{$key->UpdateDate}", 'ClassTimeStamp' => "{$key->ClassTimeStamp}",
                      'DeletedFlagField' => "{$key->DeletedFlagField}", 'DeletedFlagValue' => "{$key->DeletedFlagValue}",
                      'HasKeyIndex' => "{$key->HasKeyIndex}" );
          }
        }
      }
    }

    // send back array
    return $return_data;
  }


  public function GetMetadataTypes($id = 0) {
    $this->reset_error_info();

    if (empty($this->capability_url['GetMetadata'])) {
      die("GetMetadataTypes() called but unable to find GetMetadata location.  Failed login?\n");
    }

    // request basic metadata information
    $result = $this->RETSRequest($this->capability_url['GetMetadata'], array('Type' => 'METADATA-CLASS', 'ID' => $id, 'Format' => 'STANDARD-XML'));
    if (!$result) {
      return false;
    }
    list($headers, $body) = $result;
    $xml = $this->ParseXMLResponse($body);
    if (!$xml) {
      return false;
    }

    // log replycode and replytext for reference later
    $this->last_request['ReplyCode'] = "{$xml['ReplyCode']}";
    $this->last_request['ReplyText'] = "{$xml['ReplyText']}";

    if ($xml['ReplyCode'] != 0) {
      $this->set_error_info("rets", "{$xml['ReplyCode']}", "{$xml['ReplyText']}");
      return false;
    }

    $return_data = array();

    // parse XML into a nice array
    if ($xml->METADATA) {
      foreach ($xml->METADATA->{'METADATA-CLASS'} as $key => $value) {
        $resource = $value['Resource'];
        $this_resource = array();
        foreach ($value->Class as $key) {
          if (!empty($key->ClassName)) {
            $this_resource[] = array('ClassName' => "{$key->ClassName}", 'VisibleName' => "{$key->VisibleName}", 'StandardName' => "{$key->StandardName}",
                      'Description' => "{$key->Description}", 'TableVersion' => "{$key->TableVersion}", 'TableDate' => "{$key->TableDate}",
                      'UpdateVersion' => "{$key->UpdateVersion}", 'UpdateDate' => "{$key->UpdateDate}");
          }
        }

        // prepare 2-deep array
        $return_data[] = array('Resource' => "{$resource}", 'Data' => $this_resource);
      }
    }

    // send back array
    return $return_data;
  }


  public function GetServerSoftware() {
    return $this->server_software;
  }


  public function GetServerVersion() {
    return $this->server_version;
  }


  public function CheckAuthSupport($type = "") {
    if ($type == "basic") {
      return $this->auth_support_basic;
    }
    if ($type == "digest") {
      return $this->auth_support_digest;
    }
    $this->set_error_info("phrets", -1, "Unknown auth type requested.");
    return false;
  }


  public function GetAllTransactions() {
    // read through capability_urls read during the Login and return
    $transactions = array();
    if (is_array($this->capability_url)) {
      foreach ($this->capability_url as $key => $value) {
        $transactions[] = $key;
      }
    }
    return $transactions;
  }


  public function LastRequestURL() {
    return $this->last_request_url;
  }


  public function GetLoginURL() {
    // see if the saved Login URL has a hostname included.
    // if not, make it based on the URL given in the Connect() call
    $parse_results = parse_url($this->capability_url['Login'], PHP_URL_HOST);
    if (empty($parse_results)) {
      // login transaction gave a relative path for this action
      $request_url = $this->server_protocol.'://'.$this->server_hostname.':'.$this->server_port.''.$this->capability_url['Login'];
    }
    else {
      // login transaction gave an absolute path for this action
      $request_url = $this->capability_url['Login'];
    }
    if (empty($request_url)) {
      $this->set_error_info("phrets", -1, "Unable to find a login URL.  Did initial login fail?");
      return false;
    }
    return $request_url;
  }


  public function GetServerInformation() {
    $this->reset_error_info();

    if (empty($this->capability_url['GetMetadata'])) {
      die("GetServerInformation() called but unable to find GetMetadata location.  Failed login?\n");
    }

    // request server information
    $result = $this->RETSRequest($this->capability_url['GetMetadata'], array('Type' => 'METADATA-SYSTEM', 'ID' => 0, 'Format' => 'STANDARD-XML'));
    if (!$result) {
      return false;
    }
    list($headers, $body) = $result;

    $xml = $this->ParseXMLResponse($body);
    if (!$xml) {
      return false;
    }

    if ($xml['ReplyCode'] != 0) {
      $this->set_error_info("rets", "{$xml['ReplyCode']}", "{$xml['ReplyText']}");
      return false;
    }


    if ($this->is_server_version("1_5_or_below")) {
      $system_id = isset($xml->METADATA->{'METADATA-SYSTEM'}->System->SystemID) ? "{$xml->METADATA->{'METADATA-SYSTEM'}->System->SystemID}" : "";
      $system_description = isset($xml->METADATA->{'METADATA-SYSTEM'}->System->SystemDescription) ? "{$xml->METADATA->{'METADATA-SYSTEM'}->System->SystemDescription}" : "";
      $timezone_offset = "";
    }
    else {
      $system_id = isset($xml->METADATA->{'METADATA-SYSTEM'}->SYSTEM->attributes()->SystemID) ? "{$xml->METADATA->{'METADATA-SYSTEM'}->SYSTEM->attributes()->SystemID}" : "";
      $system_description = isset($xml->METADATA->{'METADATA-SYSTEM'}->SYSTEM->attributes()->SystemDescription) ? "{$xml->METADATA->{'METADATA-SYSTEM'}->SYSTEM->attributes()->SystemDescription}" : "";
      $timezone_offset = isset($xml->METADATA->{'METADATA-SYSTEM'}->SYSTEM->attributes()->TimeZoneOffset) ? "{$xml->METADATA->{'METADATA-SYSTEM'}->SYSTEM->attributes()->TimeZoneOffset}" : "";
    }

    $system_comments = isset($xml->METADATA->{'METADATA-SYSTEM'}->SYSTEM->Comments) ? "{$xml->METADATA->{'METADATA-SYSTEM'}->SYSTEM->Comments}" : "";

    return array('SystemID' => $system_id, 'SystemDescription' => $system_description, 'TimeZoneOffset' => $timezone_offset, 'Comments' => $system_comments);
  }


  public function Disconnect() {
    $this->reset_error_info();

    if (empty($this->capability_url['Logout'])) {
      die("Disconnect() called but unable to find Logout location.  Failed login?\n");
    }

    // make request
    $result = $this->RETSRequest($this->capability_url['Logout']);
    if (!$result) {
      return false;
    }
    list($headers,$body) = $result;

    // close cURL connection
    curl_close($this->ch);

    if ($this->debug_mode == true) {
      // close cURL debug log file handler
      fclose($this->debug_log);
    }

    if (file_exists($this->cookie_file)) {
      @unlink($this->cookie_file);
    }

    return true;

  }


  public function Connect($login_url, $username, $password, $ua_pwd = "") {
    $this->reset_error_info();

    if (empty($login_url)) {
      die("PHRETS: Login URL missing from Connect()");
    }
    if (empty($username)) {
      die("PHRETS: Username missing from Connect()");
    }
    if (empty($password)) {
      die("PHRETS: Password missing from Connect()");
    }
    if (empty($this->static_headers['RETS-Version'])) {
      $this->AddHeader("RETS-Version", "RETS/1.5");
    }
    if (empty($this->static_headers['User-Agent'])) {
      $this->AddHeader("User-Agent", "PHRETS/1.0");
    }
    if (empty($this->static_headers['Accept']) && $this->static_headers['RETS-Version'] == "RETS/1.5") {
      $this->AddHeader("Accept", "*/*");
    }

    // chop up Login URL to use for later requests
    $url_parts = parse_url($login_url);
    $this->server_hostname = $url_parts['host'];
    $this->server_port = (empty($url_parts['port'])) ? 80 : $url_parts['port'];
    $this->server_protocol = $url_parts['scheme'];

    $this->capability_url['Login'] = $url_parts['path'];

    if (isset($url_parts['query']) && !empty($url_parts['query'])) {
      $this->capability_url['Login'] .= "?{$url_parts['query']}";
    }

    $this->username = $username;
    $this->password = $password;

    if (!empty($ua_pwd)) {
      // force use of RETS 1.7 User-Agent Authentication
      $this->ua_auth = true;
      $this->ua_pwd = $ua_pwd;
    }

    if (empty($this->cookie_file)) {
      $this->cookie_file = tempnam("", "phrets");
    }

    @touch($this->cookie_file);

    if (!is_writable($this->cookie_file)) {
      $this->set_error_info("phrets", -1, "Cookie file \"{$this->cookie_file}\" cannot be written to.  Must be an absolute path and must be writable");
      return false;
    }

    // start cURL magic
    $this->ch = curl_init();
    curl_setopt($this->ch, CURLOPT_HEADERFUNCTION, array(&$this, 'read_custom_curl_headers'));
    if ($this->debug_mode == true) {
      // open file handler to be used by cURL debug log
      $this->debug_log = fopen($this->debug_file, 'a');

      curl_setopt($this->ch, CURLOPT_VERBOSE, 1);
      curl_setopt($this->ch, CURLOPT_STDERR, $this->debug_log);
    }
    curl_setopt($this->ch, CURLOPT_HEADER, false);
    if ($this->force_basic_authentication == true) {
      curl_setopt($this->ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
    }
    else {
      curl_setopt($this->ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST|CURLAUTH_BASIC);
    }
    if ($this->disable_follow_location != true) {
      curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, 1);
    }
    curl_setopt($this->ch, CURLOPT_USERPWD, $this->username.":".$this->password);
    curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($this->ch, CURLOPT_COOKIEFILE, $this->cookie_file);

    // make request to Login transaction
    $result =  $this->RETSRequest($this->capability_url['Login']);
    if (!$result) {
      return false;
    }

    list($headers,$body) = $result;

    // parse body response
    $xml = $this->ParseXMLResponse($body);
    if (!$xml) {
      return false;
    }

    // log replycode and replytext for reference later
    $this->last_request['ReplyCode'] = "{$xml['ReplyCode']}";
    $this->last_request['ReplyText'] = "{$xml['ReplyText']}";

    // chop up login response
    // if multiple parts of the login response aren't found splitting on \r\n, redo using just \n
    $login_response = array();

    if ($this->server_version == "RETS/1.0") {
      if (isset($xml)) {
        $login_response = explode("\r\n", $xml);
        if (empty($login_response[3])) {
          $login_response = explode("\n", $xml);
        }
      }
    }
    else {
      if (isset($xml->{'RETS-RESPONSE'})) {
        $login_response = explode("\r\n", $xml->{'RETS-RESPONSE'});
        if (empty($login_response[3])) {
          $login_response = explode("\n", $xml->{'RETS-RESPONSE'});
        }
      }
    }

    // parse login response.  grab all capability URLs known and ones that begin with X-
    // otherwise, it's a piece of server information to save for reference
    foreach ($login_response as $line) {
      @list($name,$value) = @explode("=", $line, 2);
      $name = trim($name);
      $value = trim($value);
      if (!empty($name) && !empty($value)) {
        if (isset($this->allowed_capabilities[$name]) || preg_match('/^X\-/', $name) == true) {
          $this->capability_url[$name] = $value;
        }
        else {
          $this->server_information[$name] = $value;
        }
      }
    }

    // if 'Action' capability URL is provided, we MUST request it following the successful Login
    if (isset($this->capability_url['Action']) && !empty($this->capability_url['Action'])) {
      $result = $this->RETSRequest($this->capability_url['Action']);
      if (!$result) {
        return false;
      }
      list($headers,$body) = $result;
    }

    if ($this->compression_enabled == true) {
      curl_setopt($this->ch, CURLOPT_ENCODING, "gzip");
    }

    if ($this->last_request['ReplyCode'] == 0) {
      return true;
    }
    else {
      $this->set_error_info("rets", $this->last_request['ReplyCode'], $this->last_request['ReplyText']);
      return false;
    }

  }


  public function LastRequest() {
    // return replycode and replytext from last request
    return $this->last_request;
  }


  public function AddHeader($name, $value) {
    // add static header for cURL requests
    $this->static_headers[$name] = $value;
    return true;
  }


  public function DeleteHeader($name) {
    // delete static header from cURL requests
    unset($this->static_headers[$name]);
    return true;
  }


  public function ParseXMLResponse($data = "") {
    $this->reset_error_info();

    if (!empty($data)) {
      // parse XML function.  ability to replace SimpleXML with something later fairly easily
      $xml = @simplexml_load_string($data);
      if (!is_object($xml)) {
        $this->set_error_info("xml", -1, "XML parsing error: {$data}");
        $this->err = "XML Parsing error: {$data}";
        return false;
      }
      return $xml;
    }
    else {
      $this->set_error_info("xml", -1, "XML parsing error.  No data to parse");
      return false;
    }
  }


  public function RETSRequest($action, $parameters = "") {
    $this->reset_error_info();

    $this->err = "";
    $this->last_response_headers = array();
    $this->last_response_headers_raw = "";
    // exposed raw RETS request function.  used internally and externally

    if (empty($action)) {
      die ("RETSRequest called but Action passed has no value.  Failed login?\n");
    }

    $parse_results = parse_url($action, PHP_URL_HOST);
    if (empty($parse_results)) {
      // login transaction gave a relative path for this action
      $request_url = $this->server_protocol.'://'.$this->server_hostname.':'.$this->server_port.''.$action;
    }
    else {
      // login transaction gave an absolute path for this action
      $request_url = $action;
    }

    // build query string from arguments
    $request_arguments = "";
    if (is_array($parameters)) {
      $request_arguments .= "?";
      foreach ($parameters as $key => $value) {
        $request_arguments .= "{$key}=".urlencode($value)."&";
      }
      $request_arguments = preg_replace('/\&$/', '', $request_arguments);
    }

    // build entire URL
    $request_url = $request_url.$request_arguments;

    // build headers to pass in cURL
    $request_headers = "";
    if (is_array($this->static_headers)) {
      foreach ($this->static_headers as $key => $value) {
        $request_headers .= "{$key}: {$value}\r\n";
      }
    }

    if ($this->ua_auth == true) {
      $session_id_to_calculate_with = "";

      // calculate RETS-UA-Authorization header
      $ua_a1 = md5($this->static_headers['User-Agent'] .':'. $this->ua_pwd);
      $session_id_to_calculate_with = ($this->use_interealty_ua_auth == true) ? "" : $this->session_id;
      $ua_dig_resp = md5(trim($ua_a1) .':'. trim($this->request_id) .':'. trim($session_id_to_calculate_with) .':'. trim($this->static_headers['RETS-Version']));
      $request_headers .= "RETS-UA-Authorization: Digest {$ua_dig_resp}\r\n";
    }

    $this->last_request_url = $request_url;
    curl_setopt($this->ch, CURLOPT_URL, $request_url);

    curl_setopt($this->ch, CURLOPT_HTTPHEADER, array($request_headers));
    // do it
    $response_body = curl_exec($this->ch);
    $response_code = curl_getinfo($this->ch, CURLINFO_HTTP_CODE);

    if ($this->debug_mode == true) {
      fwrite($this->debug_log, $response_body ."\n");
    }

        if ($this->catch_last_response == true) {
      $this->last_server_response = $this->last_response_headers_raw . $response_body;
    }

    if (isset($this->last_response_headers['WWW-Authenticate'])) {
      if (preg_match('/Basic/', $this->last_response_headers['WWW-Authenticate'])) {
        $this->auth_support_basic = true;
      }
      if (preg_match('/Digest/', $this->last_response_headers['WWW-Authenticate'])) {
        $this->auth_support_digest = true;
      }
    }

    if (isset($this->last_response_headers['RETS-Version'])) {
      $this->server_version = $this->last_response_headers['RETS-Version'];
    }

    if (isset($this->last_response_headers['Server'])) {
      $this->server_software = $this->last_response_headers['Server'];
    }

    if (isset($this->last_response_headers['Set-Cookie'])) {
      if (preg_match('/RETS-Session-ID\=(.*?)(\;|\s+|$)/', $this->last_response_headers['Set-Cookie'], $matches)) {
        $this->session_id = $matches[1];
      }
    }

    if ($response_code != 200) {
      $this->set_error_info("http", $response_code, $response_body);
      return false;
    }

    // return raw headers and body
    return array($this->last_response_headers_raw, $response_body);
  }


  private function read_custom_curl_headers($handle, $call_string) {
    static $last_remembered_header;
    $this->last_response_headers_raw .= $call_string;
    $header = "";
    $value = "";
    @list($header,$value) = explode(': ',$call_string,2);
    $header = trim($header);
    $value = trim($value);
    if (!empty($header)) {
      // new header
      $this->last_response_headers[$header] = $value;
      $last_remembered_header = $header;
    }
    else {
      // continuation of last header.  append to previous
      $this->last_response_headers[$last_remembered_header] .= $call_string;
    }

    return strlen($call_string);
  }


  public function Error() {
    if (isset($this->error_info['type']) && !empty($this->error_info['type'])) {
      return $this->error_info;
    }
    else {
      return false;
    }
  }


  private function set_error_info($type, $code, $text) {
    $this->error_info['type'] = $type;
    $this->error_info['code'] = $code;
    $this->error_info['text'] = $text;
    return true;
  }


  private function reset_error_info() {
    $this->error_info['type'] = "";
    $this->error_info['code'] = "";
    $this->error_info['text'] = "";
    return true;
  }


  private function is_server_version($check_version) {
    if ($check_version == "1_5_or_below") {
      if ($this->GetServerVersion() == "RETS/1.5" || $this->GetServerVersion() == "RETS/1.0") {
        return true;
      }
      else {
        return false;
      }
    }
    if ($check_version == "1_7_or_higher") {
      if ($this->GetServerVersion() == "RETS/1.7" || $this->GetServerVersion() == "RETS/1.7.1" || $this->GetServerVersion() == "RETS/1.7.2") {
        return true;
      }
      else {
        return false;
      }
    }
    return false;
  }


  private function fix_encoding($in_str) {
    if ($this->disable_encoding_fix == true) {
      return $in_str;
    }

    $in_str = preg_replace('/\&\s/', '&amp; ', $in_str);
    $cur_encoding = mb_detect_encoding($in_str);
    if ($cur_encoding == "UTF-8" && mb_check_encoding($in_str, "UTF-8")) {
      return $in_str;
    }
    else {
      return utf8_encode($in_str);
    }
  }


  public function ServerDetail($detail) {
    if (isset($this->server_information[$detail])) {
      return $this->server_information[$detail];
    }
    else {
      return "";
    }
  }


  public function SetParam($name, $value) {
    switch ($name) {
      case "cookie_file":
        $this->cookie_file = $value;
        break;
      case "debug_file":
        $this->debug_file = $value;
        break;
      case "debug_mode":
        $this->debug_mode = $value;
        break;
      case "compression_enabled":
        $this->compression_enabled = $value;
        break;
      case "force_ua_authentication":
        $this->ua_auth = $value;
        break;
      case "disable_follow_location":
        $this->disable_follow_location = $value;
        break;
      case "force_basic_authentication":
        $this->force_basic_authentication = $value;
        break;
      case "use_interealty_ua_auth":
        $this->use_interealty_ua_auth = $value;
        break;
      case "catch_last_response":
        $this->catch_last_response = $value;
        break;
      case "disable_encoding_fix":
        $this->disable_encoding_fix = $value;
        break;
      case "offset_support":
        $this->offset_support = $value;
        break;
      case "override_offset_protection":
        $this->override_offset_protection = $value;
        break;
      default:
        return false;
    }

    return true;
  }


}