Skip to content
Snippets Groups Projects
Webquery.php 17.52 KiB
<?php

namespace Library;

use GuzzleHttp\Psr7\Request;


class Webquery {
    use \Library\LoggerTrait;

    /**
     * @var array log context
     */

    protected $context = ['webquery'];
    /**
     * @var \GuzzleHttp\Client psr-7 http client
     */

    protected $http_client;
    /**
     * @var string webquery host name, default: 'library.wur.nl'
     */

    protected $host = 'library.wur.nl';
    /**
     * @var string webquery service name
     */

    protected $service = '';
    /**
     * @var string webquery suffix name, default: 'xml'
     */

    protected $suffix = 'xml';
    /**
     * @var string wq_ofs query parameter
     */

    protected $wq_ofs;

    /**
     * @var string wq_max query parameter
     */
    protected $wq_max;

    /**
     * @var query string
     */
    protected $query;

    /**
     * @var string record element name
     * if not set then the service name is used
     */
    protected $record_name = '';

    /**
     * @var GuzzleHttp\Psr7\Response
     */
    protected $response;

    /**
     * @var SimpleXMLElement parsed xml from response
     */
    protected $resp_xml;

    /**
     * @var array with children and attributes from error element
     */
    protected $resp_error;


    public function __construct() {

    }


    /**
     * setter for http_client
     * @param \GuzzleHttp\Client $http_client
     * @return $this
     */
    public function set_http_client(\GuzzleHttp\Client $http_client) {

        $this->http_client = $http_client;
        return $this;
    }


    /**
     *  getter for http_client
     *  if no http_client is set a default http_client is created
     * @return \GuzzleHttp\Client
     */
    public function get_http_client() {

        if (!isset($this->http_client)) {
            $this->http_client = new \GuzzleHttp\Client();
        }
        return $this->http_client;
    }


    /**
     * set host name for webquery url
     * @param sting $host webquery host
     * @return $this
     */
    public function set_host($host) {

        $this->host = $host;
        return $this;
    }


    /**
     * set webquery service name for url
     * @param string $service webquery service
     * @return $this
     */
    public function set_service($service) {

        $this->service = $service;
        return $this;
    }


    /**
     * set webquery suffix for url
     * @param string $suffix webquery suffix
     * @return $this
     */
    public function set_suffix($suffix) {

        $this->suffix = $suffix;
        return $this;
    }

    /**
     * set wq_ofs parameter for webquery search query
     * @param string $wq_ofs webquery wq_ofs parameter
     * @return $this
     */
    public function set_wq_ofs($wq_ofs) {

        $this->wq_ofs = $wq_ofs;
        return $this;
    }


    /**
     * set wq_max parameter for webquery search query
     * @param string $wq_max webquery wq_max parameter
     * @return $this
     */
    public function set_wq_max($wq_max) {

        $this->wq_max = $wq_max;
        return $this;
    }


    /**
     * set query string
     * @param string $query query string or key/value array
     * @return $this
     */
    public function set_query($query) {

        $this->query = $query;
        return $this;
    }


    /**
     * set name of record element
     * @param string $record_name record element name
     * @return $this
     */
    public function set_record_name($record_name) {

        $this->record_name = $record_name;
        return $this;
    }


    /**
     * get name of record element
     * if record_name is not set return service name
     * @return record name
     */
    public function get_record_name() {

        return isset($this->record_name) ? $this->record_name : $this->service;
    }

/**
 * create query string from query parameters
 *
 * @param type $query string or array with key/value pairs
 * @param type $wq_ofs value for wq_ofs
 * @param type $wq_max value for wq_max
 * @return string
 */
    public static function get_query_string($query, $wq_ofs = null, $wq_max = null) {

        if (empty($query)) {
            return "";
        }

        if (is_string($query)) {
            $result = $query;
            if (isset($wq_max)) {
                $result = "wq_max=$wq_max&$result";
            }
            if (isset($wq_ofs)) {
                $result = "wq_ofs=$wq_ofs&$result";
            }
            return $result;
        }

        if (is_array($query)) {
            $result = [];
            if (isset($wq_ofs)) {
                array_push($result, "wq_ofs=$wq_ofs");
            }
            if (isset($wq_max)) {
                array_push($result, "wq_max=$wq_max");
            }
            foreach ($query as $key => $value) {
                # check for 'and not' exclamation mark before key
                $is_wqnot = strpos($key, "!") === 0;
                if ($is_wqnot) {
                    # remove exclamation mark from key
                    $key = substr($key, 1);
                    array_push($result, "wq_rel=AND+NOT");
                }
                # tread a single value as an array with one value
                # one value is special case of key=val1&key=val2
                $values = is_array($value) ? $value : [$value];

                # surround multiple OR values with parentheses if rel = and not
                $use_open_close = $is_wqnot && count($values) > 1;
                if ($use_open_close) {
                    array_push($result, "wq_par=open");
                }
                foreach ($values as $v) {
                    array_push($result, "$key=".rawurlencode($v));
                }
                if ($use_open_close) {
                    array_push($result, "wq_par=close");
                }
            }
            return implode("&", $result);
        }

        return "";
    }


    public function get_search_url() {

        $qry = $this->get_query_string($this->query, $this->wq_ofs, $this->wq_max);
        return "http://$this->host/WebQuery/$this->service/$this->suffix".(empty($qry) ? "" : "?$qry");
    }


    public function get_new_url() {

        return "http://$this->host/WebQuery/$this->service/new_$this->suffix";
    }


    public function search() {

        array_push($this->context, 'search');
        $this->reset_response();
        if (empty($this->service)) {
            $this->error("search: service is not set", $this->context);
            array_pop($this->context);
            return $this;
        }

        $this->response = $this->http_client_get($this->get_search_url());

        array_pop($this->context);
        return $this;
    }


    /**
     * run a search request with set request parameters
     * continue searches and collect all records until no next element is found
     * return all collected records from the search or false if any search fails
     */
    public function search_all_records() {

        array_push($this->context, 'search_all_records');

        if ( ! $this->search()->is_success()) {
            array_pop($this->context);
            return false;
        }

        $records = $this->get_records();
        while ($ofs = $this->get_next_ofs()) {
            if ( ! $this->set_wq_ofs($ofs)->search()->is_success()) {
                array_pop($this->context);
                return false;
            }
            array_push($records, $this->get_records());
        }

        array_pop($this->context);
        return $records;
    }


    /**
     * return first record from response
     */
    public function get_record() {

        $xml = $this->get_resp_xml();
        if (!isset($xml)) {
            return false;
        }

        $record_name = $this->get_record_name();
        $records = $xml->xpath("//$record_name");
        if (empty($records)) {
            return false;
        }

        return $records[0];
    }


    /**
     * return all records from response
     */
    public function get_records() {

        $xml = $this->get_resp_xml();
        if (!isset($xml)) {
            return false;
        }

        $record_name = $this->get_record_name();
        return $xml->xpath("//$record_name");
    }


    /**
     * return value of the hits element in search response
     * @return number of hits or false if there is no valid search response
     */
    public function get_hits() {

        $resp_xml = $this->get_resp_xml();
        if (!isset($resp_xml)) {
            return false;
        }

        $hits = $resp_xml->xpath("//hits");
        return empty($hits) ? false : (string) $hits[0];
    }


    /**
     * @return boolean true if response contains next element
     */
    public function has_next() {

        $xml = $this->get_resp_xml();
        if (!isset($xml)) {
            return false;
        }

        $next = $xml->xpath("//next");
        return !empty($next);
    }


    /**
     * return value of wq_ofs attribute of next element in search response
     * @return value of next wq_ofs or false if there is no valid search response
     */
    public function get_next_ofs() {

        $resp_xml = $this->get_resp_xml();
        if (!isset($resp_xml)) {
            return false;
        }

        $next = $resp_xml->xpath("//next");
        if (empty($next)) {
            return false;
        }
        $attr = $next[0]->attributes();
        return empty($attr['wq_ofs']) ? false : (string) $attr['wq_ofs'];
    }


/**
 * convert xml tree to x-www-form-urlencoded string
 * @param SimpleXMLElement $xml  xml tree
 * @return string xml as x-www-form-urlencoded string
 */
    public function xml_to_wwwform($xml) {

        $result = [];
        foreach($xml as $key=>$value) {
            $result[] = "$key=".($value->count() > 0 ? '&'.$this->xml_to_wwwform($value) : urlencode($value));
        }
        return implode('&', $result);
    }


    /**
     * create new record in webquery
     * uses properties host, service and suffix for the url
     * @param SimpleXMLElement $xml post data (xmol tree)
     * @return boolean true on success
     */
    public function create_record($xml) {

        array_push($this->context, 'create_record');
        $this->reset_response();

        if (empty($this->service)) {
            $this->error("create_record: service is not set", $this->context);
            array_pop($this->context);
            return false;
        }

        $url = $this->get_new_url();
        $headers = ['Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8'];
        $body = $this->xml_to_wwwform($xml);
        $this->debug("request url: $url", $this->context);
        $this->debug("request data: $body", $this->context);
        $request = new Request('POST', $url, $headers, $body);
        $options = [
            'timeout' => 2,
            'http_errors' => false, // disable exceptions on 4xx and 5xx responses
        ];
        try {
            $this->response = $this->http_client->send($request, $options);
        } catch (\GuzzleHttp\Exception\RequestException $ex) {
            $this->error("exception posting to webquery: ".$ex->getMessage(), $this->context);
            $this->resp_error = [
                'status' => '500',
                'code' => $ex->getCode(),
                'message' => 'request exception: '.$ex->getMessage(),
            ];
            array_pop($this->context);
            return false;
        }

        $success = $this->is_success();
        $code = $this->get_error_code();
        $message = $this->get_error_message();
        if ($success) {
            $this->info("success, code=$code, message=$message, isn=".$this->get_isn(), $this->context);
        } else {
            $this->error("failed, code=$code, message=$message", $this->context);
        }
        array_pop($this->context);
        return $success;
    }


    /**
     * return success status of last webquery action
     * @return boolean true on success
     */
    public function is_success() {

        if ($this->get_http_status() != 200) {
            return false;
        }

        // check status attribute of webquery error element if available
        $error_status = $this->get_error_status();
        if (!empty($error_status) && $error_status != 200) {
            return false;
        }

        // check code attribute of webquery error element if available
        $error_code = $this->get_error_code();
        if (!empty($error_code)) {
            // check if code is known to be a success indication
            return !empty(array_search($error_code, ["WQW_NO_HITS", "WQW_RECORD_NOT_FOUND", "WQW_UPDATE_OK", "WQW_UPDATE_NOCHANGE"]));
        }

         return true;
    }


    /**
     * return the http status code of last webquery action
     * @return string http status code
     */
    public function get_http_status() {

        return $this->response->getStatusCode();
    }


    /**
     *  return response object of last webquery action
     * @return psr-7 response object
     */
    public function get_response() {

        return $this->response;
    }


    /**
     *  return response object of last webquery action
     * @return psr-7 response object
     */
    public function get_resp_xml() {

        if (isset($this->response) && ! isset($this->resp_xml)) {
            $this->resp_xml = $this->parse_xml($this->response->getBody());
            if (empty($this->resp_xml)) {
                $fake_resp = '<root><error status="500"><code>NO_XML_RESP</code><message>failed to parse xml response</message></error></root>';
                $this->resp_xml = $this->parse_xml($fake_resp);
            }
        }
        return $this->resp_xml;
    }


    /**
     * return content of first webquery error element of last webquery action
     * @return hash array with all webquery error attributes and elements
     */
    public function get_error() {

        if (isset($this->response) && empty($this->resp_error)) {
            $xml = $this->get_resp_xml();
            $error = $xml->error[0];
            if (isset($error)) {
                foreach ($error->attributes() as $attr => $value) {
                    $this->resp_error[$attr] = (string) $value;
                }
                foreach ($error->children() as $key => $value) {
                    $this->resp_error[$key] = (string) $value;
                }
            }
        }
        return $this->resp_error;
    }

    /**
     * return value of given error property of last webquery action
     * @param string $name webquery error property name (attribute or ellement
     * @return string value or empty string if not set
     */
    public function get_error_value($name) {

        $error = $this->get_error();
        return isset($error[$name]) ? $error[$name] : '';
    }


    /**
     * return value of webquery error code of last webquery action
     * @return string value of code error property or empty string
     */
    public function get_error_code() {

        return $this->get_error_value('code');
    }


    /**
     * return value of webquery error status of last webquery action
     * @return string value of status error property or empty string
     */
    public function get_error_status() {

        return $this->get_error_value('status');
    }


    /**
     * return value of webquery error message of last webquery action
     * the return message is a composite of the message, field and xmlerror properties
     * @return string value of message error property or empty string
     */
    public function get_error_message() {

        $message = $this->get_error_value('message');
        $field = $this->get_error_value('field');
        if (!empty($field)) {
            $message = "$message: $field";
        }
        $xmlerror = $this->get_error_value('xmlerror');
        return empty($xmlerror) ? $message : "$message ($xmlerror)";
    }


    /**
     * return value of webquery error isn of last webquery action
     * isn is set even if webquery failed to create a record
     * @return string value of isn error property or empty string
     */
    public function get_isn() {

        return $this->get_error_value('isn');
    }


    protected function reset_response() {

        $this->response = null;
        $this->resp_xml = null;
        $this->resp_error = [];
    }


    public function http_client_get($url) {

        $options = [
            'timeout' => 2,
            'http_errors' => false, // disable exceptions on 4xx and 5xx responses
        ];
        try {
            return $this->http_client->get($url, $options);
        } catch (\GuzzleHttp\Exception\RequestException $ex) {
            $this->error("exception get request to webquery: ".$ex->getMessage(), $this->context);
            return new \GuzzleHttp\Psr7\Response(500, [], "<exception><error code=$ex->getCode()><message>$ex->getMessage()</message></error></exception>");
        }

    }


    /**
     * parse xml content from message
     *
     * @param $text
     * @return SimpleXMLElement parsed xml or false on errors
     */
    public function parse_xml($text) {

        if (trim($text) == '') {
            $this->error("xml string is empty", $this->context);
            return false;
        }

        libxml_use_internal_errors(true);
        $xml = simplexml_load_string($text);
        $errors = libxml_get_errors();
        libxml_clear_errors();
        $error_count = count($errors);
        if ($error_count > 0) {
            $this->error("parsing xml string failed with $error_count errors (max. 10 listed", $this->context);
            // only log first 10 error messages
            foreach (array_slice($errors, 0, 9) as $error) {
                $this->error('xml parse error: ' . print_r($error, true), $this->context);
            }
            return false;
        }
        return $xml;
    }

}