<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | PHP Version 4                                                        |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2002 The PHP Group                                |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.0 of the PHP license,       |
// | that is bundled with this package in the file LICENSE, and is        |
// | available at through the world-wide-web at                           |
// | http://www.php.net/license/2_02.txt.                                 |
// | If you did not receive a copy of the PHP license and are unable to   |
// | obtain it through the world-wide-web, please send a note to          |
// | license@php.net so we can mail you a copy immediately.               |
// +----------------------------------------------------------------------+
// | Author: Stoyan Stefanov <ssttoo@gmail.com>                           |
// +----------------------------------------------------------------------+
//

/**
 * Use PEAR for error handling
 */
require_once 'PEAR.php';

/**
 * Use HTTP_Request lib to send requests
 */
require_once 'HTTP/Request.php';

/**
 * Services_Safari
 *
 * CVS: $Id$
 * Services_Safari is an interface for making requests to the 
 * web service provided by O'Reilly's Safari Bookshelf 
 * electronic reference library.
 * More information about the service is available at
 * http://safari.oreilly.com/affiliates/
 *
 * The XML responses from the service can be returned either
 * as is in XML or as associative arrays, produced by 
 * XML_Unserializer from the original response.
 *
 * Example usages :
 * <sample>
 *     // simple search that returns an array
 *     $safari  = new Services_Safari('1234567890mydevtoken');
 *     $results = $safari->search('php');
 * </sample>
 *
 * <sample>
 *     // category listing that returns XML and doesn't strip
 *     // the highlighting tags
 *     $safari  = new Services_Safari('1234567890mydevtoken');
 *     $safari->response_as_xml = true;
 *     $safari->strip_highlight = false;
 *     $results = $safari->listCategory('itbooks.markup.xml');
 * </sample>
 *
 * <sample>
 *     // array is returned but we had an XML_Unserializer object
 *     // lying around and we reused it to save resources
 *     require_once 'XML/Unserializer.php';
 *     $us_obj = &new XML_Unserializer(array('parseAttributes' => true));
 *     // ... do something meanwhile ...
 *     $safari  = new Services_Safari('1234567890mydevtoken');
 *     $safari->unserializer_object = &$us_obj;
 *     $results = $safari->search('php');
 * </sample>
 *
 * <sample>
 *     // present as another user agent
 *     $safari  = new Services_Safari('1234567890mydevtoken');
 *     $safari->setUserAgent('Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)');
 *     $results = $safari->search('php');
 * </sample>
 *
 * <sample>
 *     // get information on a specific book
 *     $safari  = new Services_Safari('1234567890mydevtoken');
 *     $results = $safari->getBook('1565926102');
 * </sample>
 *
 * @author      Stoyan Stefanov <ssttoo@gmail.com>
 * @package     Services_Safari
 * @version     0.1
 * @link        http://safari.oreilly.com/affiliates/
 */
class Services_Safari
{
    
/**
     * URI of the API
     *
     * @access  private
     * @var     string
     */
    
var $_safari_api 'http://safari.oreilly.com/xmlapi/';
    
    
/**
     * Safari developer token.
     *
     * You need to sign up to the service
     * in order to get a token.
     *
     * @access  private
     * @var     string
     * @link    http://safari.oreilly.com/affiliates/?p=joinnow
     */
    
var $_token   null;

    
/**
     * Instance of the XML_Unserializer class.
     *
     * It's used to parse the XML returned by the web service.
     * If not provided, it will be created when and if needed.
     * Not needed if you plan to use the XML response directly.
     *
     * @access  public
     * @var     object XML_Unserializer
     * @see     XML_Unserializer
     */
    
var $unserializer_object null;
    
    
/**
     * Options for the XML_Unserializer object.
     *
     * If an instance of XML_Unserializer is not supplied
     * and it's needed, it will be created, using these 
     * options.
     *
     * @access  public
     * @var     array
     * @see     XML_Unserializer
     * @see     $unserializer_object
     */
    
var $us_options = array('parseAttributes' => true);
    
    
/**
     * Parameters to the request to the service.
     *
     * The parameters array contain the parameter as a key and 
     * the parameter value as a value.
     * Example values:
     * <sample>
     *     array ('view' => 'book', 'sortOrder' => 'desc')
     * </sample>
     *
     * @access  private
     * @var     array
     * @see     setRequestParameter()
     * @link    http://safari.oreilly.com/affiliates/?p=quickref
     */
    
var $_parameters = array();
    
    
    
/**
     * User agent for making requests.
     *
     * When making a request to the service this user agent
     * string will be passed as part of the HTTP headers.
     *
     * @access  private
     * @var     string
     */
    
var $_user_agent 'PEAR::Services_Safari';

    
/**
     * Should the highlighting XML tags be 
     * stripped from the response.
     *
     * On some searches, the service returns extracts
     * from the books. In these extracts, the searched terms are 
     * enclosed by "hlhit" tags. When the XML content is unserialized
     * these tags are added to _content array index which
     * makes the extracts harder to use.
     * TRUE is the default value for this class variable.
     * It makes sense to set it to FALSE when for example
     * you use XSLT to display the results from the XML response and
     * don't need to convert the XML to an array.
     *
     * @access  public
     * @var     bool
     */
    
var $strip_highlight true;


    
/**
     * Determines the output format of the "get" methods - 
     *            XML or associative array.
     * 
     * If TRUE, getResponse() and all the other "get" methods
     * such as search() and getBook() will return an XML string
     * and not an array produced by the unserializer object.
     * Default is FALSE.
     *
     * @access  public
     * @var     bool
     * @see     getResponse()
     */
    
var $response_as_xml false;

    
/**
     * Contains the XML response from the request to the service.
     *
     * @access  private
     * @var     string
     */
    
var $_response_xml '';
    
   
/**
     * Constructor
     *
     * @access  public
     * @param   string  Developer token
     */
    
function Services_Safari($token)
    {
        
$this->_token $token;
    }
    
    
/**
     * Sets a parameter to the request.
     *
     * See the Safari documentation for updated parameters set.
     * Currently (2005-03-01) the following parameters exist.
     * <ul>
     *   <li><strong>view</strong> - with values 
     *                  "book" (default) and "section"</li>
     *   <li><strong>sort</strong> - with values 
     *                  "rank" (default), "title", "publishingDate", 
     *                  "insertDate", "publisher", "hits"</li>
     *   <li><strong>sortOrder</strong> - with values 
     *                  "asc" (default) and "desc"</li>
     *   <li><strong>sectionsperpage</strong> - an integer, 
     *                  10 is the default value</li>
     *   <li><strong>page</strong> - an integer, 
     *                  0 is the deafult value </li>
     * </ul>
     * Example 
     * <sample>
     *     $safari = new Services_Safari('1234567890mydevtoken');
     *     $safari->setRequestParameter('view', 'book');
     *     $safari->setRequestParameter('page', 15);
     * </sample>
     *
     * @access  public
     * @param   string  Parameter name
     * @param   string  Parameter value
     * @link    http://safari.oreilly.com/affiliates/?p=quickref
     */
    
function setRequestParameter($name$value)
    {
        
$this->_request_parameters[$name] = $value;
    }

    
/**
     * Merges all request parameters into a query string.
     *
     * An example return value would be 
     * <sample>
     *     &view=book&page=15
     * </sample>
     * If a non-empty value is to be returned, 
     * it always starts with an <strong>&amp;</strong>.
     *
     * @access  private
     * @return  string  All set request parameters as a query string
     * @see     setRequestParameter()
     */
    
function _getRequestParameters() 
    {
        
$return '';
        
        if (empty(
$this->_request_parameters) || !is_array($this->_request_parameters)) {
            return 
$return;
        }
        
        foreach (
$this->_request_parameters AS $name => $value) {
            
$return .= '&' urlencode($name) . '=' urlencode($value);
        }
        
        return 
$return;
    }

    
/**
     * Sets the user agent string.
     *
     * This will be used as a User-Agent header 
     * when making an HTTP request.
     * If not set, the default value "PEAR::Services_Safari"
     * will be used.
     *
     * @access  public
     * @param   string String to be used in the User-Agent header
     * @see     HTTP_Request::addHeader()
     */
    
function setUserAgent($user_agent
    {
        if (
is_string($user_agent)) {
            
$this->_user_agent $user_agent;
        }
    }

    
/**
     * Search the book library.
     *
     * There is a variety of search strings that can be provided.
     * More about the search syntax can be found on the Safari site.
     * This method basically wraps sendRequest() and getResponse()
     * methods.
     * Example:
     * <sample>
     *     $safari  = new Services_Safari('1234567890mydevtoken');
     *     // simple search
     *     $results = $safari->search('pear php');
     *     // search for code in the books
     *     $results = $safari->search('CODE php date');
     * </sample>
     * Apart from the documentation in the affiliate section,
     * you can test the advanced search on http://safari.oreilly.com/
     * as it is using similar search syntax.
     * You can find information on what is returned by the response 
     * at http://safari.oreilly.com/affiliates/?p=response
     *
     * @access  public
     * @param   string Search string
     * @return  string|array|PEAR_Error The response as an 
     *                  XML string or as an associative array.
     * @see     sendResponse()
     * @see     getResponse()
     * @link    http://safari.oreilly.com/affiliates/?p=search-syntax
     * @link    http://safari.oreilly.com/affiliates/?p=response
     */
    
function search($string)
    {
        
$query_string '&search=' urlencode($string);
        
$query_string.= $this->_getRequestParameters();
        
        
$result $this->sendRequest($query_string);
        
        if (
PEAR::isError($result)) {
            return 
$result;
        }
        return 
$this->getResponse();
    }

    
/**
     * Returns information about a specific book.
     *
     * This method uses the "ID" request method, as specified
     * in the documentation http://safari.oreilly.com/affiliates/?p=requests
     *
     * @access  public
     * @param   string  ID of the book
     * @return  string|array|PEAR_Error Book information as an 
     *                  XML string or as an associative array.
     */
    
function getBook($id
    {    
        return 
$this->_getId($id);
    }

    
/**
     * Returns information about a section in a book.
     *
     * This method uses the "ID" request method, as specified
     * in the documentation http://safari.oreilly.com/affiliates/?p=requests
     *
     * @access  public
     * @param   string  ID of the section
     * @return  string|array|PEAR_Error Section information as an 
     *                  XML string or as an associative array.
     */
    
function getSection($id
    {
        
$this->setRequestParameter('view''section');
        return 
$this->_getId($id);
    }

    
/**
     * Method for making "ID" requests.
     *
     * This method uses the "ID" request method, as specified
     * in the documentation http://safari.oreilly.com/affiliates/?p=requests
     * It is used by getBook() and getSection().
     * Works quite like the search() method, calling 
     * sendRequest() and getResponse()
     *
     * @access  public
     * @param   string  ID of the section
     * @return  string|array|PEAR_Error Section information as an 
     *                  XML string or as an associative array.
     */
    
function _getId($id
    {
        
$query_string '&id=' urlencode($id);
        
$query_string.= $this->_getRequestParameters();
        
        
$result $this->sendRequest($query_string);
        if (
PEAR::isError($result)) {
            return 
$result;
        }
        return 
$this->getResponse();
    }

    
/**
     * Lists the books in a category.
     *
     * An example category is "itbooks.prog.php".
     * All categories are listed on the Safari documentation site.
     * This methos is just a friendlier search().
     * For example the following samples work exactly the same.
     * <sample>
     *     $safari  = new Services_Safari('1234567890mydevtoken');
     *     $results = $safari->search('CATEGORY=itbooks.inet.scripting');
     * </sample>
     * <sample>
     *     $safari  = new Services_Safari('1234567890mydevtoken');
     *     $results = $safari->listCategory('itbooks.inet.scripting');
     * </sample>
     *
     * @access  public
     * @param   string Category identifier
     * @return  string|array|PEAR_Error Category listing as an 
     *                  XML string or as an associative array.
     * @link    http://safari.oreilly.com/affiliates/?p=search-syntax
     */
    
function listCategory($category_identifier)
    {
        return 
$this->search('CATEGORY=' $category_identifier);
    }

   
/**
    * Method to send a request to the service.
    *
    * Using this method gives a complete freedom over your request.
    * You can use it to make search , id or any other request types
    * that may be offered in the future.
    *
    * @access   public
    * @param    string Query string to be used to make the request
    * @return   bool|PEAR_Error TRUE on success, PEAR_Error otherwise.
    */
    
function sendRequest($query_string)
    {
        
$url $this->_safari_api '?token=' $this->_token;
        
$url .= $query_string;
        
        
$request = &new HTTP_Request($url);
        
$request->addHeader('User-Agent'$this->_user_agent);

        
$request->sendRequest();

        if (
$request->getResponseCode() !== 200) {
            return 
PEAR::raiseError('Error during request'$request->getResponseCode());
        }
        
        
$this->_response_xml $request->getResponseBody();

        if (
$this->strip_highlight) {
            
$this->_response_xml  str_replace(
                array(
'<hlhit>','</hlhit>'), 
                array(
'',''), 
                
$this->_response_xml
            
);
        }
        return 
true;
    }

    
/**
     * Returns the response from the service.
     *
     * Can return XML or an associative array.
     * If array is chosen as return type (default), an instance
     * of XML_Unserializer is created, if it doesn't exist already.
     *
     * @access  public
     * @see     XML_Unserializer::getUnserializedData()
     * @return  string|array|PEAR_Error The service response as an 
     *                  XML string or as an associative array.
     */
    
function getResponse()
    {
        
        if (
$this->response_as_xml) {
            return 
$this->_response_xml;
        }
        
        if (!
is_object($this->unserializer_object)) {
            require_once 
'XML/Unserializer.php';
            
$this->unserializer_object = &new XML_Unserializer($this->us_options);
        }
        
        
$result $this->unserializer_object->unserialize($this->_response_xml);
        if (
PEAR::isError($result)) {
            return 
$result;
        }
        return 
$this->unserializer_object->getUnserializedData();
    }

}

?>