Location: PHPKode > projects > VuFind > vufind-1.0.1/web/sys/Summon.php
<?php
/**
 *
 * Copyright (C) Andrew Nagy 2009.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

require_once 'HTTP/Request.php';
require_once 'sys/ConfigArray.php';
require_once 'sys/SolrUtils.php';

/**
 * Summon REST API Interface
 *
 * @version     $Revision$
 * @author      Andrew S. Nagy <hide@address.com>
 * @access      public
 */
class Summon {
    /**
     * A boolean value determining whether to print debug information
     * @var bool
     */
    public $debug = false;

    /**
     * The HTTP_Request object used for API transactions
     * @var object HTTP_Request
     */
    public $client;
    
    /**
     * The HTTP_Request object used for API transactions
     * @var object HTTP_Request
     */
    public $host;

    /**
     * The secret Key used for authentication
     * @var string
     */
    public $apiKey;

    /**
     * The Client ID used for authentication
     * @var string
     */
    public $apiId;

    /**
     * The session for the current transaction
     * @var string
     */
    public $sessionId;

    /**
     *
     * Configuration settings from web/conf/Summon.ini
     * @var array
     */
    private $config;
    
    /**
     * Should boolean operators in the search string be treated as
     * case-insensitive (false), or must they be ALL UPPERCASE (true)?
     */
    private $caseSensitiveBooleans = true;
    
    /**
     * Constructor
     *
     * Sets up the Summon API Client
     *
     * @access  public
     */     
    function __construct($apiId, $apiKey)
    {
        global $configArray;
        
        if ($configArray['System']['debug']) {
            $this->debug = true;
        }
        
        $this->host = 'http://api.summon.serialssolutions.com';
        $this->apiId = $apiId;
        $this->apiKey = $apiKey;
        $this->client = new HTTP_Request(null, array('useBrackets' => false));
        $this->config = getExtraConfigArray('Summon');
        
        // Store preferred boolean behavior:
        if (isset($this->config['General']['case_sensitive_bools'])) {
            $this->caseSensitiveBooleans = 
                $this->config['General']['case_sensitive_bools'];
        }
    }

    /**
     * Retrieves a document specified by the ID.
     *
     * @param   string  $id         The document to retrieve from the Summon API
     * @access  public
     * @throws  object              PEAR Error
     * @return  string              The requested resource
     */
    function getRecord($id)
    {
        if ($this->debug) {
            echo "<pre>Get Record: $id</pre>\n";
        }

        // Query String Parameters
        $options = array('s.st' => "id,$id");
        $result = $this->call($options);
        if (PEAR::isError($result)) {
            PEAR::raiseError($result);
        }

        return $result;
    }

    /**
     * Escape a string for inclusion as part of a Summon parameter.
     *
     * @param   string  $input      The string to escape.
     * @access  private
     * @return  string              The escaped string.
     */
    private function escapeParam($input)
    {
        // List of characters to escape taken from:
        //      http://api.summon.serialssolutions.com/help/api/search/parameters
        return addcslashes($input, ",:\\()\${}");
    }

    /**
     * Build Query string from search parameters
     *
     * @access  private
     * @param   array   $search     An array of search parameters
     * @return  string              The query
     */
    private function buildQuery($search)
    {
        $groups   = array();
        $excludes = array();
        if (is_array($search)) {
            $query = '';

            foreach ($search as $params) {
                // Advanced Search
                if (isset($params['group'])) {
                    $thisGroup = array();
                    // Process each search group
                    foreach ($params['group'] as $group) {
                        // Build this group individually as a basic search
                        $thisGroup[] = $this->buildQuery(array($group));
                    }
                    // Is this an exclusion (NOT) group or a normal group?
                    if ($params['group'][0]['bool'] == 'NOT') {
                        $excludes[] = join(" OR ", $thisGroup);
                    } else {
                        $groups[] = join(" ".$params['group'][0]['bool']." ", $thisGroup);
                    }
                }

                // Basic Search
                if (isset($params['lookfor']) && $params['lookfor'] != '') {
                    // Clean and validate input -- note that index may be in a different
                    // field depending on whether this is a basic or advanced search.
                    $lookfor = $params['lookfor'];
                    if (isset($params['field'])) {
                        $index = $params['field'];
                    } else if (isset($params['index'])) {
                        $index = $params['index'];
                    } else {
                        $index = 'AllFields';
                    }

                    // Force boolean operators to uppercase if we are in a case-insensitive
                    // mode:
                    if (!$this->caseSensitiveBooleans) {
                        $lookfor = SolrUtils::capitalizeBooleans($lookfor);
                    }

                    // Prepend the index name, unless it's the special "AllFields" index:
                    if ($index != 'AllFields') {
                        $query .= "{$index}:($lookfor)";
                    } else {
                        $query .= "$lookfor";
                    }
                }
            }
        }

        // Put our advanced search together
        if (count($groups) > 0) {
            $query = "(" . join(") " . $search[0]['join'] . " (", $groups) . ")";
        }
        // and concatenate exclusion after that
        if (count($excludes) > 0) {
            $query .= " NOT ((" . join(") OR (", $excludes) . "))";
        }

        // Ensure we have a valid query to this point
        return isset($query) ? $query : '';
    }

    /**
     * Execute a search.
     *
     * @param   array   $query      The search terms from the Search Object
     * @param   array   $filter     The fields and values to filter results on
     * @param   string  $start      The record to start with
     * @param   string  $limit      The amount of records to return
     * @param   string  $sortBy     The value to be used by for sorting
     * @param   array   $facets     The facets to include (null for defaults)
     * @param   bool    $returnErr  If Summon reports a fatal error, should we 
     *                              fail outright (false) or treat it as an 
     *                              empty result set with an error key set (true)?
     * @access  public
     * @throws  object              PEAR Error
     * @return  array               An array of query results
     */
    function query($query, $filterList = null, $start = 1, $limit = 20, 
        $sortBy = null, $facets = null, $returnErr = false)
    {
        // Query String Parameters
        $options = array('s.q' => $this->buildQuery($query));

        // Which facets should we include in results?  Set defaults if not provided.
        if (!$facets) {
            $facets = array_keys($this->config['Facets']); 
        }

        // Default to "holdings only" unless a different setting is found in the
        // filters:
        $options['s.ho'] = 'true';

        // Which filters should be applied to our query?
        if (!empty($filterList)) {
            // Loop through all filters and add appropriate values to request:
            $options['s.fvf'] = array();
            foreach ($filterList as $filterArray) {
                foreach($filterArray as $filter) {
                    $safeValue = $this->escapeParam($filter['value']);
                    // Special case -- "holdings only" is a separate parameter from
                    // other facets.
                    if ($filter['field'] == 'holdingsOnly') {
                        $options['s.ho'] = $safeValue;
                    } else {
                        $options['s.fvf'][] = "{$filter['field']},{$safeValue}";
                    }
                }
            }
        }
        
        if (is_array($facets)) {
            $options['s.ff'] = array();
            foreach($facets as $facet) {
                // See if parameters are included as part of the facet name;
                // if not, override them with defaults.
                $parts = explode(',', $facet);
                $facetName = $parts[0];
                $facetMode = isset($parts[1]) ? $parts[1] : 'and';
                $facetPage = isset($parts[2]) ? $parts[2] : 1;
                if (isset($parts[3])) {
                    $facetLimit = $parts[3];
                } else {
                    $facetLimit = 
                        isset($this->config['Facet_Settings']['facet_limit']) ?
                        $this->config['Facet_Settings']['facet_limit'] : 30;
                }
                $facetParams = "{$facetMode},{$facetPage},{$facetLimit}";

                $options['s.ff'][] = "{$facetName},{$facetParams}";
            }
        }

        if (isset($sortBy)) {
            $options['s.sort'] = $sortBy;
        }
        
        $options['s.ps'] = $limit;
        $options['s.pn'] = $start;

        // Define Highlighting
        $options['s.hl'] = 'false';  // Disable highlighting
        $options['s.hs'] = '<span class="highlight">';
        $options['s.he'] = '</span>';

        if ($this->debug) {
            echo '<pre>Query: ';
            print_r($options);
            echo "</pre>\n";
        }

        $result = $this->call($options);
        if (PEAR::isError($result)) {
            if ($returnErr) {
                return array(
                    'recordCount' => 0,
                    'documents' => array(),
                    'errors' => $result->getMessage()
                );
            } else {
                PEAR::raiseError($result);
            }
        }
        
        return $result;
    }

    /**
     * Submit REST Request
     *
     * @param   string      $service    The API Service to call
     * @param   array       $params     An array of parameters for the request
     * @param   string      $method     The HTTP Method to use
     * @param   bool        $raw        Whether to return raw XML or processed
     * @return  object                  The response from the Summon API (or a
     *                                  PEAR_Error object in case of trouble).
     * @access  private
     */
    private function call($params = array(), $service = 'search', $method = 'POST', $raw = false)
    {
        $this->client->setURL($this->host . '/' . $service);
        //$this->client->setMethod($method);
        $this->client->setMethod('GET');

        // Build Query String
        $query = array();
        foreach ($params as $function => $value) {
            if(is_array($value)) {
                foreach ($value as $additional) {
                    $additional = urlencode($additional);
                    $query[] = "$function=$additional";
                }
            } else {
                $value = urlencode($value);
                $query[] = "$function=$value";
            }
        }
        asort($query);
        $queryString = implode('&', $query);
        $this->client->addRawQueryString($queryString);

        if ($this->debug) {
            echo "<pre>$method: ";
            print_r($this->host . "/$service?" . $queryString);
            echo "</pre>\n";
        }

        // Build Authorization Headers
        $headers = array('Accept' => 'application/json',
                         'x-summon-date' => date('D, d M Y H:i:s T'),
                         'Host' => 'api.summon.serialssolutions.com');
        $data = implode($headers, "\n") . "\n/$service\n" . urldecode($queryString) . "\n";
        $hmacHash = $this->hmacsha1($this->apiKey, $data);
        foreach ($headers as $key => $value) {
            $this->client->addHeader($key, $value);
        }
        $this->client->addHeader('Authorization', "Summon $this->apiId;$hmacHash");
        if ($this->sessionId) {
            $this->client->addHeader('x-summon-session-id', $this->sessionId);
        }

        // Send Request
        $result = $this->client->sendRequest();
        if (!PEAR::isError($result)) {
            return $this->_process($this->client->getResponseBody());
        } else {
            return $result;
        }
    }

    /**
     * Perform normalization and analysis of Summon return value.
     *
     * @param   array       $input              The raw response from Summon
     * @return  array                           The processed response from Summon
     * @access  private
     */
    function _process($input)
    {
        // Unpack JSON Data
        $result = json_decode($input, true);

        // Catch decoding errors -- turn a bad JSON input into an empty result set
        // containing an appropriate error code.
        if (!$result) {
            $result = array(
                'recordCount' => 0,
                'documents' => array(),
                'errors' => array(
                    array(
                        'code' => 'VuFind-Internal',
                        'message' => 'Cannot decode JSON response: ' . $input
                    )
                )
            );
        }

        // Detect errors
        if (isset($result['errors']) && is_array($result['errors'])) {
            foreach($result['errors'] as $current) {
                $errors[] = "{$current['code']}: {$current['message']}";
            }
            return new PEAR_Error('Unable to process query<br />Summon returned: ' . 
                implode('<br />', $errors));
        }

        return $result;
    }

    function hmacsha1($key,$data)
    {
        $blocksize=64;
        $hashfunc='sha1';
        if (strlen($key)>$blocksize) {
            $key=pack('H*', $hashfunc($key));
        }
        $key=str_pad($key,$blocksize,chr(0x00));
        $ipad=str_repeat(chr(0x36),$blocksize);
        $opad=str_repeat(chr(0x5c),$blocksize);
        $hmac = pack(
                    'H*',$hashfunc(
                        ($key^$opad).pack(
                            'H*',$hashfunc(
                                ($key^$ipad).$data
                            )
                        )
                    )
                );
        return base64_encode($hmac);
    }
}

?>
Return current item: VuFind