<?php
#### FX.php #############################################################
#                                                                       #
#       By: Chris Hansen with Chris Adams, Gjermund Thorsen, and others #
#     Date: 01 Aug 2004                                                 #
# Web Site: www.iviking.org                                             #
#  Details: FX is a free open-source PHP class for accessing FileMaker  #
#          data.  For complete details about this class, please visit   #
#          www.iviking.org.                                             #
#                                                                       #
#########################################################################

require_once('FX_Error.php');                                           // This version of FX.php adds object based error handling.  See
                                                                        // FX_Error.php for more information.

define("EMAIL_ERROR_MESSAGES", FALSE);                                  // Set this to TRUE to enable emailing of specific error messages.
define("DISPLAY_ERROR_MESSAGES", TRUE);                                 // Set this to FALSE to display the $altErrorMessage to the user.
$webmasterEmailAddress = 'webmaster@yourdomain.com';                    // If you set the above to TRUE, enter the appropriate email address on this line.
$emailFromAddress = 'you@yourdomain.com';                               // Sets who the error message will show as the sender.

function EmailError ($errorText)
{
    global $webmasterEmailAddress;
    global $emailFromAddress;

    if (EMAIL_ERROR_MESSAGES) {
        $emailSubject = "PHP Server Error";
        $emailMessage = "The following error just occured:\r\n\r\nMessage: {$errorText}\r\n\r\n**This is an automated message**";
        $emailStatus = mail($webmasterEmailAddress, $emailSubject, $emailMessage, "From: $emailFromAddress\r\n");
    }
}

function EmailErrorHandler ($FXErrorObj)
{
    $altErrorMessage = 'The Server was unable to process your request.<br />The WebMaster has been emailed.<br /> Thank you for your patience.';

    EmailError($FXErrorObj->message);
    if (DISPLAY_ERROR_MESSAGES) {
        echo($FXErrorObj->message);
    } else {
        echo($altErrorMessage);
    }
    return true;
}

class FX
{
    // These are the basic database variables.
    var $dataServer = "";
    var $dataServerType = 'FMPro7';
    var $dataPort;
    var $dataPortSuffix;
    var $database = "";
    var $layout = ""; // the layout to be accessed for FM databases.  For SQL, the table to be accessed.
    var $responseLayout = "";
    var $groupSize;
    var $currentSkip = 0;
    var $defaultOperator = 'bw';
    var $dataParams = array();
    var $sortParams = array();

    // Variables to help with SQL queries
    var $primaryKeyField = '';
    var $modifyDateField = '';
    var $dataKeySeparator = '';
    var $fuzzyKeyLogic = false;
    var $genericKeys = false;

    // These are the variables to be used for storing the retrieved data.
    var $fieldInfo = array();
    var $currentData = array();
    var $valueLists = array();
    var $totalRecordCount = -1;
    var $foundCount = -1;
    var $dateFormat = "";
    var $timeFormat = "";
    var $dataURL = "";
    var $dataURLParams = "";
    var $dataQuery = "";

    // Flags and Error Tracking
    var $currentFlag = '';
    var $currentRecord = '';
    var $currentField = '';
    var $currentValueList = '';
    var $fieldCount = 0;
    var $columnCount = -1;                                                // columnCount is ++ed BEFORE looping
    var $fxError = 'No Action Taken';
    var $errorTracking = 0;

    // These variables will be used if you need a password to access your data.
    var $DBUser = 'FX';
    var $DBPassword = '';                                                 // This can be left blank, or replaced with a default or dummy password.
    var $userPass = '';

    // These variables are related to sending data to FileMaker via a Post.
    var $isPostQuery = false;
    var $useCURL = true;

    // Other variables
    var $invalidXMLChars = array("\x0B", "\x0C", "\x12");

    /*
        Translation arrays used with str_replace to handle special
        characters in UTF-8 data received from FileMaker. The two arrays
        should have matching numeric indexes such that $UTF8SpecialChars[0]
        contains the raw binary equivalent of $UTF8HTMLEntities[0].

        This would be a perfect use for strtr(), except that it only works
        with single-byte data. Instead, we use preg_replace, which means
        that we need to delimit our match strings

        Please note that in this latest release I've removed the need for
        the include files which contained long lists of characters. Gjermund
        was sure there was a better way and he was right. With the two six
        element arrays below, every unicode character is allowed for. Let
        me know how this works for you. A link to Gjermund's homepage can
        be found in the FX Links section of www.iViking.org.
     */
    var $UTF8SpecialChars = array(
        "|([\xC2-\xDF])([\x80-\xBF])|e",
        "|(\xE0)([\xA0-\xBF])([\x80-\xBF])|e",
        "|([\xE1-\xEF])([\x80-\xBF])([\x80-\xBF])|e",
        "|(\xF0)([\x90-\xBF])([\x80-\xBF])([\x80-\xBF])|e",
        "|([\xF1-\xF3])([\x80-\xBF])([\x80-\xBF])([\x80-\xBF])|e",
        "|(\xF4)([\x80-\x8F])([\x80-\xBF])([\x80-\xBF])|e"
    );

    var $UTF8HTMLEntities = array(
        "\$this->BuildExtendedChar('\\1','\\2')",
        "\$this->BuildExtendedChar('\\1','\\2','\\3')",
        "\$this->BuildExtendedChar('\\1','\\2','\\3')",
        "\$this->BuildExtendedChar('\\1','\\2','\\3','\\4')",
        "\$this->BuildExtendedChar('\\1','\\2','\\3','\\4')",
        "\$this->BuildExtendedChar('\\1','\\2','\\3','\\4')"
    );

    function BuildExtendedChar ($byteOne, $byteTwo="\x00", $byteThree="\x00", $byteFour="\x00")
    {
        if (ord($byteTwo) >= 128) {
            $tempChar = substr(decbin(ord($byteTwo)), -6);
            if (ord($byteThree) >= 128) {
                $tempChar .= substr(decbin(ord($byteThree)), -6);
                if (ord($byteFour) >= 128) {
                    $tempChar .= substr(decbin(ord($byteFour)), -6);
                    $tempChar = substr(decbin(ord($byteOne)), -3) . $tempChar;
                } else {
                    $tempChar = substr(decbin(ord($byteOne)), -4) . $tempChar;
                }
            } else {
                $tempChar = substr(decbin(ord($byteOne)), -5) . $tempChar;
            }
        } else $tempChar = $byteOne;
        $tempChar = '&#' . bindec($tempChar) . ';';
        return $tempChar;
    }

    function isError($data) {
        return (bool)(is_object($data) &&
                      (strtolower(get_class($data)) == 'fx_error' ||
                      is_subclass_of($data, 'fx_error')));
    }

    function ClearAllParams ()
    {
        $this->userPass = "";
        $this->dataURL = "";
        $this->dataURLParams = "";
        $this->dataQuery = "";
        $this->dataParams = array();
        $this->sortParams = array();
        $this->fieldInfo = array();
        $this->valueLists = array();
        $this->fieldCount = 0;
        $this->currentSkip = 0;
        $this->currentData = array();
        $this->columnCount = -1;
        $this->currentRecord = "";
        $this->currentField = "";
        $this->currentFlag = "";
        $this->isPostQuery = true;
        $this->primaryKeyField = '';
        $this->modifyDateField = '';
        $this->dataKeySeparator = '';
        $this->fuzzyKeyLogic = false;
        $this->genericKeys = false;
    }

    function ErrorHandler ($errorText)
    {
        $this->fxError = $errorText;
        $this->errorTracking = 3300;
        return $errorText;
    }

    function FX ($dataServer, $dataPort=591, $dataType='')
    {
        $this->dataServer = $dataServer;
        $this->dataPort = $dataPort;
        $this->dataPortSuffix = ":" . $dataPort;
        if (strlen($dataType) > 0) {
            $this->dataServerType = $dataType;
        }

        $this->ClearAllParams();
    }

    function CreateCurrentSort ()
    {
        $currentSort = "";

        foreach ($this->sortParams as $key1 => $value1) {
            foreach ($value1 as $key2 => $value2) {
                $$key2 = $value2;
            }
            if ($this->dataServerType == 'FMPro7') {
                if ($sortOrder == "") {
                    $currentSort .= "&-sortfield.{$key1}=" . str_replace ("%3A%3A", "::", rawurlencode($field));
                }
                else {
                    $currentSort .= "&-sortfield.{$key1}=" . str_replace ("%3A%3A", "::", rawurlencode($field)) . "&-sortorder.{$key1}=" . $sortOrder;
                }
            } else {
                if ($sortOrder == "") {
                    $currentSort .= "&-sortfield=" . str_replace ("%3A%3A", "::", rawurlencode($field));
                }
                else {
                    $currentSort .= "&-sortfield=" . str_replace ("%3A%3A", "::", rawurlencode($field)) . "&-sortorder=" . $sortOrder;
                }
            }
        }
        return $currentSort;
    }

    function CreateCurrentSearch ()
    {
        $currentSearch = '';

        foreach ($this->dataParams as $key1 => $value1) {
            foreach ($value1 as $key2 => $value2) {
                $$key2 = $value2;
            }
            if ($op == "" && $this->defaultOperator == 'bw') {
                $currentSearch .= "&" . str_replace ("%3A%3A", "::", urlencode($name)) . "=" . urlencode($value);
            } else {
                if ($op == "") {
                    $op = $this->defaultOperator;
                }
                switch ($this->dataServerType) {
                    case 'FMPro5/6':
                        $currentSearch .= "&-op=" . $op . "&" . str_replace("%3A%3A", "::", urlencode($name)) . "=" . urlencode($value);
                        break;
                    case 'FMPro7':
                        $tempFieldName = str_replace("%3A%3A", "::", urlencode($name));
                        $currentSearch .= "&" . $tempFieldName . ".op=" . $op . "&" . $tempFieldName . "=" . urlencode($value);
                        break;
                }
            }
        }
        return $currentSearch;
    }

    function AssembleCurrentSearch ($layRequest, $skipRequest, $currentSort, $currentSearch, $action, $FMV=6)
    {
        $tempSearch = '';

        $tempSearch = "-db=" . urlencode($this->database);               // add the name of the database...
        $tempSearch .= $layRequest;                                      // and any layout specified...
        if ($FMV < 7) {
            $tempSearch .= "&-format=-fmp_xml";                              // then set the FileMaker XML format to use...
        }
        $tempSearch .= "&-max=$this->groupSize$skipRequest";             // add the set size and skip size data...
        $tempSearch .= $currentSort . $currentSearch . "&" . $action;    // finally, add sorting, search parameters, and action data.
        return $tempSearch;
    }

    function StartElement($parser, $name, $attrs)                        // The functions to start XML parsing begin here
    {
        switch(strtolower($name)) {
             case "data":
                $this->currentFlag = "parseData";
                $this->currentData[$this->currentRecord][$this->currentField][$this->currentFieldIndex] = "";
                break;
            case "col":
                $this->currentFieldIndex = 0;
                ++$this->columnCount;
                $this->currentField = $this->fieldInfo[$this->columnCount]['name'];
                $this->currentData[$this->currentRecord][$this->currentField] = array();
                break;
            case "row":
                foreach ($attrs as $key => $value) {
                    $key = strtolower($key);
                    $$key = $value;
                }
                if (substr_count($this->dataURL, '-dbnames') > 0 || substr_count($this->dataURL, '-layoutnames') > 0) {
                    $modid = count($this->currentData);
                }
                $this->currentRecord = $recordid . '.' . $modid;
                $this->currentData[$this->currentRecord] = array();
                break;
            case "field":
                foreach ($attrs as $key => $value) {
                    $key = strtolower($key);
                    $this->fieldInfo[$this->fieldCount][$key] = $value;
                }
                $this->fieldInfo[$this->fieldCount]['extra'] = ''; // for compatibility w/ SQL databases
                if (substr_count($this->dataURL, '-view') < 1) {
                    $this->fieldCount++;
                }
                break;
            case "style":
                foreach ($attrs as $key => $value) {
                    $key = strtolower($key);
                    $this->fieldInfo[$this->fieldCount][$key] = $value;
                }
                break;
            case "resultset":
                foreach ($attrs as $key => $value) {
                    switch(strtolower($key)) {
                        case "found":
                          $this->foundCount = (int)$value;
                          break;
                    }
                }
                break;
            case "errorcode":
                $this->currentFlag = "fmError";
                break;
            case "valuelist":
                foreach ($attrs as $key => $value) {
                    if (strtolower($key) == "name") {
                        $this->currentValueList = $value;
                    }
                }
                $this->valueLists[$this->currentValueList] = array();
                $this->currentFlag = "values";
                $this->currentValueListElement = -1;
                break;
            case "value":
                $this->currentValueListElement++;
                $this->valueLists[$this->currentValueList][$this->currentValueListElement] = "";
                break;
            case "database":
                foreach ($attrs as $key => $value) {
                    switch(strtolower($key)) {
                        case "dateformat":
                          $this->dateFormat = $value;
                          break;
                        case "records":
                          $this->totalRecordCount = $value;
                          break;
                        case "timeformat":
                          $this->timeFormat = $value;
                          break;
                    }
                }
                break;
            default:
                break;
        }
    }

    function ElementContents($parser, $data)
    {
        switch($this->currentFlag) {
            case "parseData":
                $this->currentData[$this->currentRecord][$this->currentField][$this->currentFieldIndex] .= preg_replace($this->UTF8SpecialChars, $this->UTF8HTMLEntities, $data);
                break;
            case "fmError":
                $this->fxError = $data;
                break;
            case "values":
                $this->valueLists[$this->currentValueList][$this->currentValueListElement] .= preg_replace($this->UTF8SpecialChars, $this->UTF8HTMLEntities, $data);
                break;
        }
    }

    function EndElement($parser, $name)
    {
        switch(strtolower($name)) {
            case "data":
                $this->currentFieldIndex++;
                $this->currentFlag = "";
                break;
            case "col":
                break;
            case "row":
                $this->columnCount = -1;
                break;
            case "field":
                if (substr_count($this->dataURL, '-view') > 0) {
                    $this->fieldCount++;
                }
                break;
            case "errorcode":
            case "valuelist":
                $this->currentFlag = "";
                break;
        }
    }                                                                     // XML Parsing Functions End Here

    function RetrieveFMData ($action)
    {
        $data = '';
        if ($this->DBPassword != '') {                                      // Assemple the Password Data
            $this->userPass = $this->DBUser . ':' . $this->DBPassword . '@';
        }
        if ($this->layout != "") {                                          // Set up the layout portion of the query.
            $layRequest = "&-lay=" . urlencode($this->layout);
        }
        else {
            $layRequest = "";
        }
        if ($this->currentSkip > 0) {                                       // Set up the skip size portion of the query.
            $skipRequest = "&-skip=$this->currentSkip";
        } else {
            $skipRequest = "";
        }
        $currentSort = $this->CreateCurrentSort();
        $currentSearch = $this->CreateCurrentSearch();
        $this->dataURL = "http://$this->userPass$this->dataServer$this->dataPortSuffix/FMPro"; // First add the server info to the URL...
        $this->dataURLParams = $this->AssembleCurrentSearch($layRequest, $skipRequest, $currentSort, $currentSearch, $action);
        $this->dataURL .= '?' . $this->dataURLParams;

        if (defined("DEBUG") and DEBUG) {
            echo "<P>Using FileMaker URL: <a href=\"{$this->dataURL}\">{$this->dataURL}</a><P>\n";
        }

        if (defined("HAS_PHPCACHE") and defined("FX_USE_PHPCACHE") and strlen($this->dataURLParams) <= 510 and (substr_count($this->dataURLParams, '-find') > 0 || substr_count($this->dataURLParams, '-view') > 0 || substr_count($this->dataURLParams, '-dbnames') > 0 || substr_count($this->dataURLParams, '-layoutnames') > 0)) {
            $data = get_url_cached($this->dataURL);
            if (! $data) {
                return new FX_Error("Failed to retrieve cached URL in RetrieveFMData()");
            }
            $data = $data["Body"];
        } elseif ($this->isPostQuery) {
            if ($this->useCURL && defined("CURLOPT_TIMEVALUE")) {
                $curlHandle = curl_init($this->dataURL);
                curl_setopt($curlHandle, CURLOPT_POST, 1);
                curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $this->dataURLParams);
                ob_start();
                if (! curl_exec($curlHandle)) {
                    return new FX_Error("cURL could not retrieve Post data in RetrieveFMData(). A bad URL is the most likely reason.");
                }
                curl_close($curlHandle);
                $data = trim(ob_get_contents());
                ob_end_clean();
                if (substr($data, -1) != '>') {
                    $data = substr($data, 0, -1);
                }
            } else {
                $dataDelimiter = "\r\n";
                $socketData = "POST /FMPro HTTP/1.0{$dataDelimiter}";
                if (strlen(trim($this->userPass)) > 1) {
                    $socketData .= "Authorization: Basic " . base64_encode($this->DBUser . ':' . $this->DBPassword) . $dataDelimiter;
                }
                $socketData .= "Host: {$this->dataServer}:{$this->dataPort}{$dataDelimiter}";
                $socketData .= "Pragma: no-cache{$dataDelimiter}";
                $socketData .= "Content-length: " . strlen($this->dataURLParams) . $dataDelimiter;
                $socketData .= "Content-type: application/x-www-form-urlencoded{$dataDelimiter}";
                // $socketData .= "Connection: close{$dataDelimiter}";
                $socketData .= $dataDelimiter . $this->dataURLParams;

                $fp = fsockopen ($this->dataServer, $this->dataPort, $this->errorTracking, $this->fxError, 30);
                if (! $fp) {
                    return new FX_Error( "Could not fsockopen the URL in retrieveFMData" );
                }
                fputs ($fp, $socketData);
                while (!feof($fp)) {
                    $data .= fgets($fp, 128);
                }
                fclose($fp);
                $pos = strpos($data, chr(13) . chr(10) . chr(13) . chr(10)); // the separation code
                $data = substr($data, $pos + 4) . "\r\n";
            }
        } else {
            $fp = fopen($this->dataURL, "r");
            if (! $fp) {
                return new FX_Error("Could not fopen URL in RetrieveFMData.");
            }
            while (!feof($fp)) {
                $data .= fread($fp, 4096);
            }
            fclose($fp);
        }
        $data = str_replace($this->invalidXMLChars, '', $data);
        return $data;
    }

    function RetrieveFM7Data ($action)
    {
        $data = '';
        if ($this->DBPassword != '' || $this->DBUser != 'FX') {             // Assemple the Password Data
            $this->userPass = $this->DBUser . ':' . $this->DBPassword . '@';
        }
        if ($this->layout != "") {                                          // Set up the layout portion of the query.
            $layRequest = "&-lay=" . urlencode($this->layout);
            if ($this->responseLayout != "") {
                $layRequest .= "&-lay.response=" . urlencode($this->responseLayout);
            }
        }
        else {
            $layRequest = "";
        }
        if ($this->currentSkip > 0) {                                       // Set up the skip size portion of the query.
            $skipRequest = "&-skip=$this->currentSkip";
        } else {
            $skipRequest = "";
        }
        $currentSort = $this->CreateCurrentSort();
        $currentSearch = $this->CreateCurrentSearch();
        if ($action == '-view') {
            $FMFile = 'FMPXMLLAYOUT.xml';
        } else {
            $FMFile = 'FMPXMLRESULT.xml';
        }
        $this->dataURL = "http://$this->userPass$this->dataServer$this->dataPortSuffix/fmi/xml/$FMFile"; // First add the server info to the URL...
        $this->dataURLParams = $this->AssembleCurrentSearch($layRequest, $skipRequest, $currentSort, $currentSearch, $action, 7);
        $this->dataURL .= '?' . $this->dataURLParams;

        if (defined("DEBUG") and DEBUG) {
            echo "<P>Using FileMaker URL: <a href=\"{$this->dataURL}\">{$this->dataURL}</a><P>\n";
        }

        if (defined("HAS_PHPCACHE") and defined("FX_USE_PHPCACHE") and strlen($this->dataURLParams) <= 510 and (substr_count($this->dataURLParams, '-find') > 0 || substr_count($this->dataURLParams, '-view') > 0 || substr_count($this->dataURLParams, '-dbnames') > 0 || substr_count($this->dataURLParams, '-layoutnames') > 0)) {
            $data = get_url_cached($this->dataURL);
            if (! $data) {
                return new FX_Error("Failed to retrieve cached URL in RetrieveFM7Data()");
            }
            $data = $data["Body"];
        } elseif ($this->isPostQuery) {
            if ($this->useCURL && defined("CURLOPT_TIMEVALUE")) {
                $curlHandle = curl_init(str_replace($this->dataURLParams, '', $this->dataURL));
                curl_setopt($curlHandle, CURLOPT_POST, 1);
                curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $this->dataURLParams);
                ob_start();
                if (! curl_exec($curlHandle)) {
                    return new FX_Error("cURL could not retrieve Post data in RetrieveFM7Data(). A bad URL is the most likely reason.");
                }
                curl_close($curlHandle);
                $data = trim(ob_get_contents());
                ob_end_clean();
                if (substr($data, -1) != '>') {
                    $data = substr($data, 0, -1);
                }
            } else {
                $dataDelimiter = "\r\n";
                $socketData = "POST /fmi/xml/{$FMFile} HTTP/1.0{$dataDelimiter}";
                if (strlen(trim($this->userPass)) > 1) {
                    $socketData .= "Authorization: Basic " . base64_encode($this->DBUser . ':' . $this->DBPassword) . $dataDelimiter;
                }
                $socketData .= "Host: {$this->dataServer}:{$this->dataPort}{$dataDelimiter}";
                $socketData .= "Pragma: no-cache{$dataDelimiter}";
                $socketData .= "Content-length: " . strlen($this->dataURLParams) . $dataDelimiter;
                $socketData .= "Content-type: application/x-www-form-urlencoded{$dataDelimiter}";
                $socketData .= $dataDelimiter . $this->dataURLParams;

                $fp = fsockopen ($this->dataServer, $this->dataPort, $this->errorTracking, $this->fxError, 30);
                if (! $fp) {
                    return new FX_Error( "Could not fsockopen the URL in retrieveFM7Data" );
                }
                fputs ($fp, $socketData);
                while (!feof($fp)) {
                    $data .= fgets($fp, 128);
                }
                fclose($fp);
                $pos = strpos($data, chr(13) . chr(10) . chr(13) . chr(10)); // the separation code
                $data = substr($data, $pos + 4) . "\r\n";
            }
        } else {
            $fp = fopen($this->dataURL, "r");
            if (! $fp) {
                return new FX_Error("Could not fopen URL in RetrieveFM7Data.");
            }
            while (!feof($fp)) {
                $data .= fread($fp, 4096);
            }
            fclose($fp);
        }
        $data = str_replace($this->invalidXMLChars, '', $data);
        return $data;
    }

    function BuildSQLSorts ()
    {
        $currentOrderBy = '';

        if (count($this->sortParams) > 0) {
            $counter = 0;
            $currentOrderBy .= ' ORDER BY ';
            foreach ($this->sortParams as $key1 => $value1) {
                foreach ($value1 as $key2 => $value2) {
                    $$key2 = $value2;
                }
                if ($counter > 0) {
                    $currentOrderBy .= ', ';
                }
                $currentOrderBy .= "'{$field}'";
                if (substr_count(strtolower($sortOrder), 'desc') > 0) {
                    $currentOrderBy .= ' DESC';
                }
                ++$counter;
            }
            return $currentOrderBy;
        }
    }

    function BuildSQLQuery ($action)
    {
        $currentLOP = 'AND';
        $logicalOperators = array();
        $LOPCount = 0;
        $currentQuery = '';
        $counter = 0;

        switch ($action) {
            case '-find':
                foreach ($this->dataParams as $key1 => $value1) {
                    foreach ($value1 as $key2 => $value2) {
                        $$key2 = $value2;
                    }
                    switch ($name) {
                        case '-lop':
                            $LOPCount = array_push($logicalOperators, $currentLOP);
                            $currentLOP = $value;
                            $currentSearch .= "(";
                            break;
                        case '-lop_end':
                            $currentLOP = array_pop($logicalOperators);
                            --$LOPCount;
                            $currentSearch .= ")";
                            break;
                        case '-recid':
                            if ($counter > 0) {
                                $currentSearch .= " {$currentLOP} ";
                            }
                            $currentSearch .= $this->primaryKeyField . " = '" . $value . "'";
                            ++$counter;
                            break;
                        case '-script':
                        case '-script.prefind':
                        case '-script.presort':
                            return new FX_Error("The '-script' parameter is not currently supported for SQL.");
                            break;
                        default:
                            if ($op == "") {
                                $op = $this->defaultOperator;
                            }
                            if ($counter > 0) {
                                $currentSearch .= " {$currentLOP} ";
                            }
                            switch ($op) {
                                case 'eq':
                                    $currentSearch .= $name . " = '" . $value . "'";
                                    break;
                                case 'neq':
                                    $currentSearch .= $name . " != '" . $value . "'";
                                    break;
                                case 'cn':
                                    $currentSearch .= $name . " LIKE '%" . $value . "%'";
                                    break;
                                case 'bw':
                                    $currentSearch .= $name . " LIKE '" . $value . "%'";
                                    break;
                                case 'ew':
                                    $currentSearch .= $name . " LIKE '%" . $value . "'";
                                    break;
                                case 'gt':
                                    $currentSearch .= $name . " > '" . $value . "'";
                                    break;
                                case 'gte':
                                    $currentSearch .= $name . " >= '" . $value . "'";
                                    break;
                                case 'lt':
                                    $currentSearch .= $name . " < '" . $value . "'";
                                    break;
                                case 'lte':
                                    $currentSearch .= $name . " <= '" . $value . "'";
                                    break;
                                default: // default is a 'begins with' search for historical reasons (default in FM)
                                    $currentSearch .= $name . " LIKE '" . $value . "%'";
                                    break;
                            }
                            ++$counter;
                            break;
                    }
                }
                while ($LOPCount > 0) {
                    --$LOPCount;
                    $currentSearch .= ")";
                }
                $currentQuery = "SELECT * FROM {$this->layout} WHERE {$currentSearch}" . $this->BuildSQLSorts();
                break;
            case '-findall':
                $currentQuery = "SELECT * FROM " . $this->layout . $this->BuildSQLSorts();
                break;
            case '-delete':
                foreach ($this->dataParams as $key1 => $value1) {
                    foreach ($value1 as $key2 => $value2) {
                        $$key2 = $value2;
                    }
                    if ($name == '-recid') {
                        $currentQuery = "DELETE FROM {$this->layout} WHERE {$this->primaryKeyField} = '{$value}'";
                    }
                }
                break;
            case '-edit':
                $whereClause = '';
                $currentQuery = "UPDATE {$this->layout} SET ";
                foreach ($this->dataParams as $key1 => $value1) {
                    foreach ($value1 as $key2 => $value2) {
                        $$key2 = $value2;
                    }
                    if ($name == '-recid') {
                        $whereClause = " WHERE {$this->primaryKeyField} = '{$value}'";
                    } else {
                        if ($counter > 0) {
                            $currentQuery .= ", ";
                        }
                        $currentQuery .= "{$name} = '{$value}'";
                        ++$counter;
                    }
                }
                $currentQuery .= $whereClause;
                break;
            case '-new':
                $tempColList = '(';
                $tempValueList = '(';
                foreach ($this->dataParams as $key1 => $value1) {
                    foreach ($value1 as $key2 => $value2) {
                        $$key2 = $value2;
                    }
                    if ($name == '-recid') {
                        $currentQuery = "DELETE FROM {$this->layout} WHERE {$this->primaryKeyField} = '{$value}'";
                    }
                    if ($counter > 0) {
                        $tempColList .= ", ";
                        $tempValueList .= ", ";
                    }
                    $tempColList .= $name;
                    $tempValueList .= "'{$value}'";
                    ++$counter;
                }
                $tempColList .= ')';
                $tempValueList .= ')';
                $currentQuery = "INSERT INTO {$this->layout} {$tempColList} VALUES {$tempValueList}";
                break;
        }
        $currentQuery .= ';';
        return $currentQuery;
    }

    function RetrieveMySQLData ($action)
    {
        if (strlen(trim($this->dataServer)) < 1) {
            return new FX_Error('No MySQL server specified.');
        }
        if (strlen(trim($this->dataPort)) > 0) {
            $tempServer = $this->dataServer . ':' . $this->dataPort;
        } else {
            $tempServer = $this->dataServer;
        }
        $mysql_res = @mysql_connect($tempServer, $this->DBUser, $this->DBPassword); // although username and password are optional for this function, FX.php expects them to be set
        if ($mysql_res == false) {
            return new FX_Error('Unable to connect to MySQL server.');
        }
        if ($action != '-dbopen') {
            if (! mysql_select_db($this->database, $mysql_res)) {
                return new FX_Error('Unable to connect to specified MySQL database.');
            }
        }
        if (substr_count($action, '-db') == 0 && substr_count($action, 'names') == 0) {
            $theResult = mysql_query('SHOW COLUMNS FROM ' . $this->layout);
            if (! $theResult) {
                return new FX_Error('Unable to access MySQL column data: ' . mysql_error());
            }
            $counter = 0;
            $keyPrecedence = 0;
            while ($tempRow = mysql_fetch_assoc($theResult)) {
                $this->fieldInfo[$counter]['name'] = $tempRow['Field'];
                $this->fieldInfo[$counter]['type'] = $tempRow['Type'];
                $this->fieldInfo[$counter]['emptyok'] = $tempRow['Null'];
                $this->fieldInfo[$counter]['maxrepeat'] = 1;
                $this->fieldInfo[$counter]['extra'] = $tempRow['Key'] . ' ' . $tempRow['Extra'];
                if ($this->fuzzyKeyLogic) {
                    if (strlen(trim($this->primaryKeyField)) < 1 || $keyPrecedence < 3) {
                        if (substr_count($this->fieldInfo[$counter]['extra'], 'UNI ') > 0 && $keyPrecedence < 3) {
                            $this->primaryKeyField = $this->fieldInfo[$counter]['name'];
                            $keyPrecedence = 3;
                        } elseif (substr_count($this->fieldInfo[$counter]['extra'], 'auto_increment') > 0 && $keyPrecedence < 2) {
                            $this->primaryKeyField = $this->fieldInfo[$counter]['name'];
                            $keyPrecedence = 2;
                        } elseif (substr_count($this->fieldInfo[$counter]['extra'], 'PRI ') > 0 && $keyPrecedence < 1) {
                            $this->primaryKeyField = $this->fieldInfo[$counter]['name'];
                            $keyPrecedence = 1;
                        }
                    }
                }
                ++$counter;
            }
        }
        switch ($action) {
            case '-dbopen':
            case '-dbclose':
                return new FX_Error('Opening and closing MySQL databases not available.');
                break;
            case '-delete':
            case '-edit':
            case '-find':
            case '-findall':
            case '-new':
                $this->dataQuery = $this->BuildSQLQuery($action);
                if (FX::isError($this->dataQuery)) {
                    return $this->dataQuery;
                }
                $theResult = mysql_query($this->dataQuery);
                if (! $theResult) {
                    return new FX_Error('Invalid query: ' . mysql_error());
                }
                if (substr_count($action, '-find') > 0) {
                    $this->foundCount = mysql_num_rows($theResult);
                } else {
                    $this->foundCount = mysql_affected_rows($theResult);
                }
                if ($action == '-dup' || $action == '-edit') {
                    // pull in data on relevant record
                }
                while ($tempRow = mysql_fetch_assoc($theResult)) {
                    foreach ($tempRow as $key => $value) {
                        $tempRow[$key] = array($value);
                        if ($key == $this->primaryKeyField) {
                            $currentKey = $value;
                        }
                    }
                    if ($this->genericKeys || $this->primaryKeyField == '') {
                        $this->currentData[] = $tempRow;
                    } else {
                        $this->currentData[$currentKey] = $tempRow;
                    }
                }
                break;
            case '-findany':
                break;
            case '-dup':
                break;
        }
        $this->fxError = 0;
        return true;
    }

    function ExecuteQuery ($action)
    {
        switch (strtolower($this->dataServerType)) {
            case 'fmpro5/6':
                if (defined("DEBUG") and DEBUG) {
                    echo "<P>Accessing FileMaker Pro data.<P>\n";
                }
                $data = $this->RetrieveFMData($action);

                $xml_parser = xml_parser_create("UTF-8");
                xml_set_object($xml_parser, $this);
                xml_set_element_handler($xml_parser, "StartElement", "EndElement");
                xml_set_character_data_handler($xml_parser, "ElementContents");
                $xmlParseResult = xml_parse($xml_parser, $data, true);
                if (! $xmlParseResult) {
                    $theMessage = sprintf("ExecuteQuery XML error: %s at line %d",
                        xml_error_string(xml_get_error_code($xml_parser)),
                        xml_get_current_line_number($xml_parser));
                    xml_parser_free($xml_parser);
                    return new FX_Error($theMessage);
                }
                xml_parser_free($xml_parser);
                break;
            case 'fmpro7':
                if (defined("DEBUG") and DEBUG) {
                    echo "<P>Accessing FileMaker Pro 7 data.<P>\n";
                }
                $data = $this->RetrieveFM7Data($action);

                $xml_parser = xml_parser_create("UTF-8");
                xml_set_object($xml_parser, $this);
                xml_set_element_handler($xml_parser, "StartElement", "EndElement");
                xml_set_character_data_handler($xml_parser, "ElementContents");
                $xmlParseResult = xml_parse($xml_parser, $data, true);
                if (! $xmlParseResult) {
                    $theMessage = sprintf("ExecuteQuery XML error: %s at line %d",
                        xml_error_string(xml_get_error_code($xml_parser)),
                        xml_get_current_line_number($xml_parser));
                    xml_parser_free($xml_parser);
                    return new FX_Error($theMessage);
                }
                xml_parser_free($xml_parser);
                break;
            case 'mysql':
                if (defined("DEBUG") and DEBUG) {
                    echo "<P>Accessing MySQL data.<P>\n";
                }
                $mySQLResult = $this->RetrieveMySQLData($action);
                if (FX::isError($mySQLResult)) {
                    return $mySQLResult;
                }
                break;
        }
    }

    function SetDBData ($database, $layout="", $groupSize=50, $responseLayout="") // the layout parameter is equivalent to the table to be used in SQL queries
    {
        $this->database = $database;
        $this->layout = $layout;
        $this->groupSize = $groupSize;
        $this->responseLayout = $responseLayout;
        $this->ClearAllParams();
    }

    function SetDBPassword ($DBPassword, $DBUser='FX') // Note that for historical reasons, password is the FIRST parameter for this function
    {
        if ($DBUser == '') {
            $DBUser = 'FX';
        }
        $this->DBPassword = $DBPassword;
        $this->DBUser = $DBUser;
    }

    function SetDBUserPass ($DBUser, $DBPassword='') // Same as above function, but paramters are in the opposite order
    {
        $this->SetDBPassword($DBPassword, $DBUser);
    }

    function SetDefaultOperator ($op)
    {
        $this->defaultOperator = $op;
        return true;
    }

    function AddDBParam ($name, $value, $op="")                          // Add a search parameter.  An operator is usually not necessary.
    {
        $this->dataParams[]["name"] = $name;
        end($this->dataParams);
        $this->dataParams[key($this->dataParams)]["value"] = $value;
        $this->dataParams[key($this->dataParams)]["op"] = $op;
    }

    function AddSortParam ($field, $sortOrder="", $performOrder=0)        // Add a sort parameter.  An operator is usually not necessary.
    {
        if ($performOrder > 0) {
            $this->sortParams[$performOrder]["field"] = $field;
            $this->sortParams[$performOrder]["sortOrder"] = $sortOrder;
        } else {
            if (count($this->sortParams) == 0) {
                $this->sortParams[1]["field"] = $field;
            } else {
                $this->sortParams[]["field"] = $field;
            }
            end($this->sortParams);
            $this->sortParams[key($this->sortParams)]["sortOrder"] = $sortOrder;
        }
    }

    function FMSkipRecords ($skipSize)
    {
        $this->currentSkip = $skipSize;
    }

    function FMPostQuery ($isPostQuery = true)
    {
        $this->isPostQuery = $isPostQuery;
    }

    function FMUseCURL ($useCURL = true)
    {
        $this->useCURL = $useCURL;
    }

    function AssembleDataSet ($returnData)
    {
        global $HTTP_SERVER_VARS;
        global $HTTP_POST_VARS;
        $dataSet = array();
        $FMNext = $this->currentSkip + $this->groupSize;
        $FMPrevious = $this->currentSkip - $this->groupSize;

        switch ($returnData) {
            case 'full':
                $dataSet['data'] = $this->currentData;
            case 'basic':
                if ($FMNext < $this->foundCount || $FMPrevious >= 0) {              // BEGIN PARAMETER ASSEMBLY FOR NEXT/PREV LINKS
                    $tempQueryString = '';
                    if ($HTTP_SERVER_VARS['REQUEST_METHOD'] == 'POST') {
                        $paramSetCount = 0;
                        $appendFlag = true;
                        foreach ($HTTP_POST_VARS as $key => $value) {
                            if ($appendFlag && strcasecmp($key, '-foundSetParams_begin') != 0 && strcasecmp($key, '-foundSetParams_end') != 0) {
                                $tempQueryString .= urlencode($key) . '=' . urlencode($value) . '&';
                            } elseif (strcasecmp($key, '-foundSetParams_begin') == 0) {
                                $appendFlag = true;
                                if ($paramSetCount < 1) {
                                    $tempQueryString = '';
                                    ++$paramSetCount;
                                }
                            } elseif (strcasecmp($key, '-foundSetParams_end') == 0) {
                                $appendFlag = false;
                            }
                        }
                    } else {
                        $beginTagLower = strtolower('-foundSetParams_begin');
                        $endTagLower = strtolower('-foundSetParams_end');
                        if (! isset($HTTP_SERVER_VARS['QUERY_STRING'])) {
                            $HTTP_SERVER_VARS['QUERY_STRING'] = '';
                        }
                        $queryStringLower = strtolower($HTTP_SERVER_VARS['QUERY_STRING']);
                        if (substr_count($queryStringLower, $beginTagLower) > 0 && substr_count($queryStringLower, $beginTagLower) == substr_count($queryStringLower, $endTagLower)) {
                            $tempOffset = 0;
                            for ($i = 0; $i < substr_count($queryStringLower, $beginTagLower); ++$i) {
                                $tempBeginFoundSetParams = strpos($queryStringLower, $beginTagLower, $tempOffset);
                                $tempEndFoundSetParams = strpos($queryStringLower, $endTagLower, $tempOffset) + (strlen($endTagLower) - 1);
                                $tempFoundSetParams = substr($HTTP_SERVER_VARS['QUERY_STRING'], $tempBeginFoundSetParams, ($tempEndFoundSetParams - $tempBeginFoundSetParams) + 1);
                                $tempQueryString .= preg_replace("/(?i)$beginTagLower=[^&]*&(.*)&$endTagLower/", "\$1", $tempFoundSetParams);
                                $tempOffset = $tempEndFoundSetParams;
                            }
                        } else {
                            $tempQueryString = $HTTP_SERVER_VARS['QUERY_STRING'];
                        }
                        $tempQueryString = preg_replace("/skip=[\d]*[&]?/", "", $tempQueryString);
                    }
                }                                                                   // END PARAMETER ASSEMBLY FOR NEXT/PREV LINKS
                if ($FMNext >= $this->foundCount) {
                    $dataSet['linkNext'] = "";
                } else {
                    $dataSet['linkNext'] = $HTTP_SERVER_VARS['SCRIPT_NAME'] . "?skip=$FMNext&{$tempQueryString}";
                }

                if ($FMPrevious < 0) {
                    $dataSet['linkPrevious'] = "";
                } else {
                    $dataSet['linkPrevious'] = $HTTP_SERVER_VARS['SCRIPT_NAME'] . "?skip=$FMPrevious&{$tempQueryString}";
                }

                $dataSet['foundCount'] = $this->foundCount;
                $dataSet['fields'] = $this->fieldInfo;
                $dataSet['URL'] = $this->dataURL;
                $dataSet['query'] = $this->dataQuery;
                $dataSet['errorCode'] = $this->fxError;
                $dataSet['valueLists'] = $this->valueLists;
        }

        $this->ClearAllParams();
        return $dataSet;
    }

    function FMAction ($Action, $returnDataSet, $returnData)
    {
        $queryResult = $this->ExecuteQuery($Action);
        if (FX::isError($queryResult)){
            if (EMAIL_ERROR_MESSAGES) {
                EmailErrorHandler($queryResult);
            }
            return $queryResult;
        }
        if ($returnDataSet) {
            $dataSet = $this->AssembleDataSet($returnData);
            return $dataSet;
        } else {
            $this->ClearAllParams();
            return true;
        }
    }

/* The actions that you can send to FileMaker start here */

    function FMDBOpen ()
    {
        $queryResult = $this->ExecuteQuery("-dbopen");
        if (FX::isError($queryResult)){
            return $queryResult;
        }
    }

    function FMDBClose ()
    {
        $queryResult = $this->ExecuteQuery("-dbclose");
        if (FX::isError($queryResult)){
            return $queryResult;
        }
    }

    function FMDelete ($returnDataSet = false, $returnData = 'basic')
    {
        return $this->FMAction("-delete", $returnDataSet, $returnData);
    }

    function FMDup ($returnDataSet = true, $returnData = 'full')
    {
        return $this->FMAction("-dup", $returnDataSet, $returnData);
    }

    function FMEdit ($returnDataSet = true, $returnData = 'full')
    {
        return $this->FMAction("-edit", $returnDataSet, $returnData);
    }

    function FMFind ($returnDataSet = true, $returnData = 'full')
    {
        return $this->FMAction("-find", $returnDataSet, $returnData);
    }

    function FMFindAll ($returnDataSet = true, $returnData = 'full')
    {
        return $this->FMAction("-findall", $returnDataSet, $returnData);
    }

    function FMFindAny ($returnDataSet = true, $returnData = 'full')
    {
        return $this->FMAction("-findany", $returnDataSet, $returnData);
    }

    function FMNew ($returnDataSet = true, $returnData = 'full')
    {
        return $this->FMAction("-new", $returnDataSet, $returnData);
    }

    function FMView ($returnDataSet = true, $returnData = 'full')
    {
        return $this->FMAction("-view", $returnDataSet, $returnData);
    }

    function FMDBNames ($returnDataSet = true, $returnData = 'full')
    {
        return $this->FMAction("-dbnames", $returnDataSet, $returnData);
    }

    function FMLayoutNames ($returnDataSet = true, $returnData = 'full')
    {
        return $this->FMAction("-layoutnames", $returnDataSet, $returnData);
    }

    function FMScriptNames ($returnDataSet = true, $returnData = 'full')
    {
        return $this->FMAction("-scriptnames", $returnDataSet, $returnData);
    }

/* The actions that you can send to FileMaker end here */

    // SetDataKey() is used for SQL queries as a way to provide parity with the RecordID/ModID combo provided by FileMaker Pro
    function SetDataKey ($keyField, $modifyField = '', $separator = '.')
    {
        $this->primaryKeyField = $keyField;
        $this->modifyDateField = $modifyField;
        $this->dataKeySeparator = $separator;
        return true;
    }

    // SQLFuzzyKeyLogicOn() can be used to have FX.php make it's best guess as to a viable key in an SQL DB
    function SQLFuzzyKeyLogicOn ($logicSwitch = false)
    {
        $this->fuzzyKeyLogic = $logicSwitch;
        return true;
    }

    // By default, FX.php uses records' keys as the indices for the returned array.  UseGenericKeys() is used to change this behavior.
    function UseGenericKeys($genericKeys=true)
    {
        $this->genericKeys = $genericKeys;
        return true;
    }

}
?>