<?php
/*
 * SXE: Simple XML Exchange
 * Split into a PHP API and implementation, a protocol, a JavaScript API and implementation
 */
class sxe_Operation {
    protected $functionName;
    protected $require = NULL;
    protected $attributes = NULL;

    public function __construct($functionName, $attributes = NULL) {
        $this->functionName = $functionName;
        $this->attributes = array();
        if ($attributes != NULL) {
            $this->attributes = $attributes;
            foreach($attributes as $name => $value) {
                $this->attributes[$name] = $value;
            }
        }
    }
	public function getFunctionName() {
        return $this->functionName;
	}
	public function getAttribute($name, $defaultValue = NULL) {
        if (array_key_exists($name, $this->attributes)) {
            return $this->attributes[$name];
        }
        return $defaultValue;
	}
	public function execute($exchange) {
        $fn = $this->functionName;
        $fn($exchange);
	}
}

class sxe_Response {
    protected $server;
    protected $state = 0;

    public function __construct($server) {
        $this->server = $server;
    }
	public function open() {
        if ($this->state == 1) {
            return; // already openned
        } else if ($this->state != 0) {
            throw new Exception("Invalid response state, cannot open");
        }
        $this->state++;
        Header('Content-type: text/xml; charset=UTF-8'); // text/html, application/xml
        /*echo '<?xml version="1.0" encoding="UTF-8" ?>';*/
        echo '<envelope>';
        echo $this->server->getResponseDocument()->saveXML($this->server->getResponseHeader());
        echo '<body>';
    }
	public function reply($reply) {
        if ($this->state != 1) {
            throw new Exception("Invalid response state, cannot reply");
        }
        // Need some check
        echo $reply;
    }
	public function replyXML($xmlReply) {
        $elem;
        if ($xmlReply instanceof DOMElement) {
            $elem = $xmlReply;
        } else if ($xmlReply instanceof DOMDocument) {
            $elem = $xmlReply->documentElement;
        } else {
            throw new Exception("Invalid reply argument, not a DOM element");
        }
        $this->reply($this->server->getResponseDocument()->saveXML($elem));
	}
	public function fail($message = '', $id = NULL) {
        $this->open();
        $dom = $this->server->getResponseDocument();
        $failure = $dom->createElement("failure");
        if ($message instanceof Exception) {
            $failure->appendChild($dom->createTextNode($message->getMessage()));
            $failure->appendChild($dom->createComment($message->getTraceAsString()));
        } else {
            $failure->appendChild($dom->createTextNode($message));
        }
        if ($id != NULL) {
            $failure->setAttribute("id", $id);
        }
        $this->reply($dom->saveXML($failure));
	}
	public function done($message = '') {
        $dom = $this->server->getResponseDocument();
        $success = $dom->createElement("success");
        $text = $dom->createTextNode($message);
        $success->appendChild($text);
        $this->reply($dom->saveXML($success));
    }
    public function cancel() {
        if ($this->state != 0) {
            throw new Exception("Invalid response state, cannot cancel");
        }
        $this->state = -1;
	}
	public function close() {
        if ($this->state == -1) { // canceled
            return;
        }
        if ($this->state != 1) {
            throw new Exception("Invalid response state, cannot close");
        }
        $this->state++;
        echo "</body></envelope>";
	}
}

/*class sxe_Request {
    protected $xmlRequest;

    public function __construct($xmlRequest) {
        $this->xmlRequest = $xmlRequest;
    }
	public function getAttribute($name, $defaultValue = NULL) {
        if ($this->xmlRequest->hasAttribute($name)) {
            return $this->xmlRequest->getAttribute($name);
        }
        return $defaultValue;
	}
}*/

class sxe_Exchange {
    protected $server;
    protected $request;
    protected $response;

    public function __construct($server, $request, $response) {
        $this->server = $server;
        $this->request = $request;
        $this->response = $response;
    }
	public function getServer() {
        return $this->server;
	}
	public function getRequest() {
        return $this->request;
	}
	public function getResponse() {
        return $this->response;
	}
	public function createResponseElement($tagName = 'response', $attributes = NULL, $text = NULL) {
        $dom = $this->server->getResponseDocument();
        $response = $dom->createElement($tagName);
        if ($attributes != NULL) {
            foreach ($attributes as $key => $value) {
                $response->setAttribute($key, $value);
            }
        }
        if ($text != NULL) {
            $response->appendChild($dom->createTextNode($text));
        }
        return $response;
	}
	public function reply($reply) {
        $this->response->open();
        $this->response->reply($reply);
    }
	public function replyXML($xmlReply) {
        $this->response->open();
        $this->response->replyXML($xmlReply);
	}
	public function fail($message = '', $id = NULL) {
        $this->response->fail($message, $id);
	}
	public function done($message = '') {
        $this->response->open();
        $this->response->done($message);
	}
	public function close() {
        //$this->response->close();
	}
}

abstract class sxe_OperationHandler {
	abstract public function handle($server, $operation, $exchange);
}

class sxe_Server {
    protected $requestDocument = NULL;
    protected $requestHeader = NULL;
    protected $responseDocument = NULL;
    protected $responseHeader = NULL;
    protected $operations = array();
    protected $handlers = array();
    protected $context = array();

	public function getHeaders() {
        return http_get_request_headers();
	}
	public function getRequestDocument() {
        return $this->requestDocument;
	}
	public function getRequestHeader() {
        return $this->requestHeader;
	}
	public function getResponseDocument() {
        if ($this->responseDocument == NULL) {
            $this->responseDocument = new DOMDocument(/*'1.0', 'utf-8'*/);
            $this->responseDocument->formatOutput = true;
        }
        return $this->responseDocument;
	}
	public function getResponseHeader() {
        if ($this->responseHeader == NULL) {
            $this->responseHeader = $this->getResponseDocument()->createElement("header");
        }
        return $this->responseHeader;
	}
	public function registerOperation($functionName, $operationName = NULL, $attributes = NULL, $require = NULL) {
        if (! function_exists($functionName)) {
            throw new Exception("The function $functionName does not exist");
        }
        if ($operationName == NULL) {
            $operationName = $functionName;
        }
        if (array_key_exists($operationName, $this->operations)) {
            throw new Exception("The operation $operationName already exist");
        }
        $this->operations[$operationName] = new sxe_Operation($functionName, $attributes);
	}
	public function unregisterOperation($operationName) {
        if (! array_key_exists($operationName, $this->operations)) {
            throw new Exception("The operation $operationName does not exist");
        }
        unset($this->operations[$operationName]);
	}
	public function registerHandler($handler) {
        $this->handlers[] = $handler;
	}
	public function unregisterHandler($handler) {
        $count = count($this->handlers);
        for ($i = 0; $i < $count; $i++) {
            if ($this->handlers[$i] == $handler) {
                array_splice($this->handlers, $i, 1);
                break;
            }
        }
	}
	public function setContextKey($key, $value = NULL) {
        if ($value == NULL) {
            if (array_key_exists($key, $this->context)) {
                unset($this->context[$key]);
            }
        } else {
            $this->context[$key] = $value;
        }
	}
	public function getContextKey($key) {
        if (! array_key_exists($key, $this->context)) {
            return NULL;
        }
        return $this->context[$key];
	}
	public function process() {
        $xmlRequest = sxe_Server::getHTTPRequestParameter("request");
        if ($xmlRequest == NULL) {
            if (sxe_Server::hasHTTPRequestParameter("operation")) {
                $xmlRequest = sxe_Server::createXMLRequestFromHTTPParameters();
            } else {
                $xmlRequest = file_get_contents("php://input");
            }
        }
        $response = new sxe_Response($this);
        if ($xmlRequest == NULL) {
            $response->fail("No request to proceed");
            return;
        }
        $this->requestDocument = new DOMDocument(/*'1.0', 'utf-8'*/);
        $this->requestDocument->loadXML($xmlRequest);
        // For debugging purpose
        if (false) {
            $response->open();
            echo '<!-- ' . utf8_encode($xmlRequest) . ' -->';
        }
        $this->requestHeader = sxe_Server::getFirstChildNode($this->requestDocument->documentElement, 'header');
        $body = sxe_Server::getFirstChildNode($this->requestDocument->documentElement, 'body');
        $requests = sxe_Server::filterNodes($body->childNodes);
        $firstFail = true;
        foreach ($requests as $request) {
            $operationName = $request->tagName;
            if (array_key_exists($operationName, $this->operations)) {
                $operation = $this->operations[$operationName];
                $exchange = new sxe_Exchange($this, $request, $response);
                try {
                    foreach ($this->handlers as $handler) {
                        $handler->handle($this, $operation, $exchange);
                    }
                    $operation->execute($exchange);
                }
                catch(Exception $e) {
                    //$response->fail("Operation $operationName failed due to $e");
                    $response->fail($e);
                    if ($firstFail) {
                        break;
                    }
                }
            } else {
                $response->fail("Operation $operationName not found");
                if ($firstFail) {
                    break;
                }
            }
        }
        $response->close();
	}
	public static function getDOMElementAttribute($domElement, $name, $defaultValue = NULL) {
        if ($domElement->hasAttribute($name)) {
            return $domElement->getAttribute($name);
        }
        return $defaultValue;
	}
    public static function createXMLRequestFromHTTPParameters() {
        // Mimic an XML request using get or post values
        $attributes = array_merge($_GET, $_POST);
        $operationName = sxe_Server::arrayRemoveKey("operation", $attributes);
        $login = sxe_Server::arrayRemoveKey("authLogin", $attributes);
        $password = sxe_Server::arrayRemoveKey("authPassword", $attributes);
        $domRequest = new DOMDocument();
        $header = $domRequest->createElement("header");
        if (($login != NULL) && ($password != NULL)) {
            $authentication = $domRequest->createElement("authentication");
            $element = $domRequest->createElement("login");
            $element->appendChild($domRequest->createTextNode($login));
            $authentication->appendChild($element);
            $element = $domRequest->createElement("password");
            $element->appendChild($domRequest->createTextNode($password));
            $authentication->appendChild($element);
            $header->appendChild($authentication);
        }
        $operation = $domRequest->createElement($operationName);
        foreach ($attributes as $key => $value) {
            $operation->setAttribute($key, $value);
        }
        $body = $domRequest->createElement("body");
        $body->appendChild($operation);
        $envelope = $domRequest->createElement("envelope");
        $envelope->appendChild($header);
        $envelope->appendChild($body);
        return $domRequest->saveXML($envelope);
	}
    public static function decodeHTTPRequestValue($value) {
        return str_replace('\\"', '"', $value);
	}
    public static function hasHTTPRequestParameter($name, $defaultValue = NULL) {
        return array_key_exists($name, $_POST) || array_key_exists($name, $_GET);
	}
    public static function getHTTPRequestParameter($name, $defaultValue = NULL) {
        if (array_key_exists($name, $_POST)) {
            return sxe_Server::decodeHTTPRequestValue($_POST[$name]);
        } else if (array_key_exists($name, $_GET)) {
            return sxe_Server::decodeHTTPRequestValue($_GET[$name]);
        }
        return $defaultValue;
	}
    public static function arrayRemoveKey($key, &$array, $defaultResult = NULL) {
        if (array_key_exists($key, $array)) {
            $defaultResult = $array[$key];
            unset($array[$key]);
        }
        return $defaultResult;
	}
    public static function getFirstChildNode($node, $name) {
        if ($node == NULL) {
            return NULL;
        }
        $count = $node->childNodes->length;
        for ($i = 0; $i < $count; $i++) {
            $child = $node->childNodes->item($i);
            if ($child->nodeName == $name) {
                return $child;
            }
        }
        return NULL;
    }
    public static function getFirstChildValue($node, $name) {
        $child = sxe_Server::getFirstChildNode($node, $name);
        if (($child == NULL) || ($child->firstChild == NULL) || ($child->firstChild->nodeType != XML_TEXT_NODE)) {
            return NULL;
        }
        return $child->firstChild->nodeValue;
    }
    public static function filterNodes($nodeList, $type = XML_ELEMENT_NODE) {
        $nodeCount = $nodeList->length;
        $nodes = array();
        for ($i = 0; $i < $nodeCount; $i++) {
            $item = $nodeList->item($i);
            if ($item->nodeType == $type) {
                $nodes[] = $item;
            }
        }
        return $nodes;
    }
}

?>