<?php

/* vim: set expandtab tabstop=4 shiftwidth=4: */

/**
 * Net_Dict
 *
 * PHP Versions 4 and 5
 *
 * LICENSE: This source file is subject to version 2.02 of the PHP license
 * that is available through the world-wide-web at the following URI:
 * 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 web, please
 * send a note to license@php.net so we can mail you a copy immediately.
 *
 * @category  Networking
 * @package   Net_Dict
 * @author    Chandrashekhar Bhosle <cnb@freedomink.org>
 * @author    Ian Eure <ieure@php.net>
 * @copyright 2002 Chandrashekhar Bhosle
 * @copyright 2005 - 2006 Ian Eure
 * @license   http://www.php.net/license/2_02.txt  PHP License 2.02
 * @version   CVS: $Revision: 296454 $
 * @link      http://pear.php.net/package/Net_Dict
 */

require_once 'PEAR.php';
require_once 'Net/Socket.php';


define('NET_DICT_SERVER', 'dict.org');
define('NET_DICT_PORT', '2628');

/**
 * The main Net_Dict class
 *
 * Net_Dict is a PHP interface for talking to dictd servers.
 *
 * @category Networking
 * @package  Net_Dict
 * @author   Chandrashekhar Bhosle <cnb@freedomink.org>
 * @license  http://www.php.net/license/2_02.txt PHP License v2.02
 * @version  Release: @package_version@
 * @link     http://pear.php.net/package/Net_Dict
 */
class Net_Dict
{
    /**
     * Default DICT server name
     *
     * @var string
     */
    var $server = NET_DICT_SERVER;

    /**
     * Default DICT Port
     *
     * @var int
     */
    var $port = NET_DICT_PORT;

    /**
     * Socket object
     *
     * @var object
     */
    var $_socket;

    /**
     * Server Information
     *
     * @var string
     */
    var $servinfo;

    /**
     * if caching is on or off
     *
     * @var boolean
     */
    var $caching = false;

    /**
     * PEAR Cache
     *
     * @var object
     */
    var $cache;


    /**
     * Gets definitions for the specified word in the specified database.
     *
     * @param string $word     specified word
     * @param string $database specified database
     *
     * @return  mixed   Array of definitions if sucessful, else PEAR_Error
     */
    function define($word, $database = '*')
    {
        if ($this->caching) {
            if ($defines = $this->cache->get($word, 'Net_Dict_Defs')) {
                return $defines;
            }
        }

        if (!is_object($this->_socket)) {
            $res = $this->connect();
            if (PEAR::isError($res)) {
                return $res;
            }
        }

        $resp = $this->_sendCmd("DEFINE $database '$word'");

        if (PEAR::isError($resp)) {
            return $resp;
        }

        list($num) = explode(' ', $resp['text'], 2);

        for ($i = 0; $i < $num; $i++) {
            $resp = $this->_socket->readLine();

            preg_match(
                "/(\d{3})\s+?\"(.+)?\"\s+?(\S+)\s+?\"(.+)?\"/",
                $resp,
                $matches
            );

            $defines[$i]['response']    = $resp;
            $defines[$i]['word']        = $matches[2];
            $defines[$i]['database']    = $matches[3];
            $defines[$i]['description'] = $matches[4];

            $resp = $this->_getMultiline();

            $defines[$i]['definition'] = $resp['text'];
        }

        $this->readLine(); /* discard status */

        if ($this->caching) {
            $this->cache->save($word, $defines, 0, 'Net_Dict_Defs');
        }

        return $defines;
    }

    /**
     * Searches an index for the dictionary, and reports words
     * which were found using a particular strategy.
     *
     * @param string $word     word to search for
     * @param string $strategy strategy used, defaults to 'substring'
     * @param string $database database to search, all if not specified.
     *
     * @return  mixed   Array of matches if successful, else PEAR_Error
     */
    function match($word, $strategy = 'substring', $database = '*')
    {
        $resp = $this->_sendCmd("MATCH $database $strategy '$word'");

        if (PEAR::isError($resp)) {
            return $resp;
        }

        $resp = $this->_getMultiLine();

        $this->readLine(); /* discard status */

        preg_match_all("/(\S+)?\s\"(.+?)\"/", $resp['text'], $matches);

        for ($i = 0; $i < count($matches[0]); $i++) {
            $matched[$i]['database'] = $matches[1][$i];
            $matched[$i]['word']     = $matches[2][$i];
        }

        return $matched;
    }

    /**
     * Gets list of available databases
     *
     * @return  mixed  Array of databases if successful, else PEAR_Error
     */
    function showDatabases()
    {
        $resp = $this->_sendCmd('SHOW DB');

        if (PEAR::isError($resp)) {
            return $resp;
        }

        $resp = $this->_getMultiLine();

        $this->readLine(); /* discard status */

        preg_match_all("/(\S+)?\s+?\"(.+?)\"/", $resp['text'], $matches);

        for ($i = 0; $i < count($matches[0]); $i++) {
            $databases[$i]['database']    = $matches[1][$i];
            $databases[$i]['description'] = $matches[2][$i];
        }

        return $databases;
    }

    /**
     * Gets a list of available strategies
     *
     * @return mixed Array of strategies if successful, else PEAR_Error
     */
    function showStrategies()
    {
        $resp = $this->_sendCmd('SHOW STRAT');

        if (PEAR::isError($resp)) {
            return $resp;
        }

        $resp = $this->_getMultiLine();

        $this->readLine(); /* discard status */

        preg_match_all("/(\S+)?\s+?\"(.+?)\"/", $resp['text'], $matches);

        for ($i = 0; $i < count($matches[0]); $i++) {
            $strategies[$i]['strategy']    = $matches[1][$i];
            $strategies[$i]['description'] = $matches[2][$i];
        }

        return $strategies;
    }

    /**
     * Gets source, copyright, and licensing information about the
     * specified database.
     *
     * @param string $database database name
     *
     * @return  mixed   string if successful, else PEAR_Error
     */
    function showInfo($database)
    {
        return $this->simpleQuery('SHOW INFO ' . $database);
    }

    /**
     * Gets local server information written by the local administrator.
     * This could include information about local databases or strategies,
     * or administrative information such as who to contact for access to
     * databases requiring authentication.
     *
     * @return  mixed  string if sucessful, else PEAR_Error
     */
    function showServer()
    {
        return $this->simpleQuery('SHOW SERVER');
    }

    /**
     * Allows the client to provide information about itself
     * for possible logging and statistical purposes.  All clients SHOULD
     * send this command after connecting to the server.  All DICT servers
     * MUST implement this command (note, though, that the server doesn't
     * have to do anything with the information provided by the client).
     *
     * @param string $text defaults to 'cnb'
     *
     * @return  mixed   string if successful, else PEAR_Error
     */
    function client($text = 'cnb')
    {
        $this->_sendCmd('CLIENT ' . $text);
    }

    /**
     * Display some server-specific timing or debugging information.  This
     * information may be useful in debugging or tuning a DICT server.  All
     * DICT servers MUST implement this command (note, though, that the text
     * part of the response is not specified and may be omitted).
     *
     * @return  mixed  string if successful, else PEAR_Error
     */
    function status()
    {
        $resp = $this->_sendCmd('STATUS');
        return $resp['text'];
    }

    /**
     * Provides a short summary of commands that are understood by this
     * implementation of the DICT server.  The help text will be presented
     * as a textual response, terminated by a single period on a line by
     * itself.  All DICT servers MUST implement this command.
     *
     * @return  mixed  string on success, else PEAR_Error
     */
    function help()
    {
        return $this->simpleQuery('HELP');
    }

    /**
     * This command is used by the client to cleanly exit the server.
     * All DICT servers MUST implement this command.
     *
     * @return  mixed  string on success, else PEAR_Error
     */
    function quit()
    {
        return $this->_sendCmd('QUIT');
    }

    /**
     * Requests that all text responses be prefaced by a MIME header
     * [RFC2045] followed by a single blank line (CRLF).
     *
     * @return  mixed
     * @todo    Implement this method
     */
    function optionMIME()
    {
    }

    /**
     * The client can authenticate itself to the server using a username and
     * password.  The authentication-string will be computed as in the APOP
     * protocol discussed in [RFC1939].
     *
     * @param string $user username
     * @param string $auth password
     *
     * @return  mixed
     * @todo    Implement this method.
     */
    function auth($user, $auth)
    {
    }

    /**
     * The Simple Authentication and Security Layer (SASL) is currently
     * being developed [RFC2222].  The DICT protocol reserves the SASLAUTH
     * and SASLRESP commands for this method of authentication.
     *
     * @param string $mechanism        mechanism used
     * @param string $initial_response initial response
     *
     * @return  mixed
     *
     * @todo    Implement this method.
     */
    function SASLAuth($mechanism, $initial_response)
    {
    }

    /**
     * The client will send all responses using the SASLRESP command and a
     * BASE64-encoded parameter.
     *
     * @param string $response the response
     *
     * @return mixed
     * @todo   Implement this method.
     */
    function SASLResp($response)
    {
    }

    /**
     * Connects to a dict server and sets up a socket
     *
     * @param string  $server dict server
     * @param integer $port   port to connect to
     *
     * @return mixed    true on success, else PEAR_Error
     */
    function connect($server = '', $port = 0)
    {
        $s = new Net_Socket;

        if (empty($server)) {
            $server = $this->server;
        }

        if (0 == $port) {
            $port = $this->port;
        }

        $err = $s->connect($server, $port);

        if (PEAR::isError($err)) {

            return $err;
        }

        $banner = $s->readLine();
        $resp['code'] = substr($banner, 0, 3);
        $resp['text'] = ltrim(substr($banner, 3));

        if (!Net_Dict::isOK($resp)) {
            return new PEAR_Error($resp['text'],
                                  $resp['code']);
        }

        $reg = array();
        preg_match("/\d{3} (.*) <(.*)> <(.*)>/", $banner, $reg);
        $this->servinfo["signature"]    = $reg[1];
        $this->servinfo["capabilities"] = explode(".", $reg[2]);
        $this->servinfo["msg-id"]       = $reg[3];

        $this->_socket =& $s;

        return true;
    }

    /**
     * Disconnect from the dict server
     *
     * @see     Net_Socket::disconnect()
     * @return  mixed  Net_Socket::disconnect()'s return value
     * @author  Ian Eure <ieure@php.net>
     */
    function disconnect()
    {
        if (isset($this->_socket)) {
            return $this->_socket->disconnect();
        }
        return new PEAR_Error('not connected');
    }

    /**
     * Sets the server and port of dict server
     *
     * @param string $server server address
     * @param int    $port   port number to use
     *
     * @return  void
     */
    function setServer($server, $port = 0)
    {
        $this->server = $server;

        if (0 < $port) {
            $this->port = $port;
        }
    }

    /**
     * Sets caching on or off and provides the cache type and parameters
     *
     * @param boolean $flag              true if caching is required
     * @param string  $container         name of container
     * @param array   $container_options options
     *
     * @return  void
     */
    function setCache($flag = false, $container = '', $container_options = '')
    {
        $this->caching = $flag;

        if ($this->caching) {
            include_once 'Cache.php';

            if (is_object($this->cache)) {
                unset($this->cache);
            }

            $this->cache = new Cache($container, $container_options);
        }
    }

    /**
     * Sends a command, checks the reponse, and
     * if good returns the reponse, other wise
     * returns false.
     *
     * @param string $cmd Command to send (\r\n will be appended)
     *
     * @return  mixed  First line of response if successful, otherwise false
     */
    function _sendCmd($cmd)
    {
        $result = $this->_socket->writeLine($cmd);

        if (PEAR::isError($result) && $result) {
            return $result;
        }

        $data = $this->_socket->readLine();

        if (PEAR::isError($data)) {
            return $data;
        }

        $resp['code'] = substr($data, 0, 3);
        $resp['text'] = ltrim(substr($data, 3));

        if (!Net_Dict::isOK($resp)) {
            return new PEAR_Error($resp['text'],
                                  $resp['code']);
        }

        return $resp;
    }

    /**
     * Reads a multiline reponse and returns the data
     *
     * @return mixed string on success or PEAR_Error
     */
    function _getMultiline()
    {
        $data = '';
        while (($tmp = $this->readLine()) != '.') {
            if (substr($tmp, 0, 2) == '..') {
                $tmp = substr($tmp, 1);
            }
            $data .= $tmp . "\r\n";
        }

        $resp['text'] = substr($data, 0, -2);

        return $resp;
    }

    /**
     * Alias to Net_Socket::readLine();
     *
     * @see Net_Socket::readLine();

     * @return All available data up to a newline, without that
     *         newline, or until the end of the socket, or a PEAR_Error if
     *         not connected.
     */
    function readLine()
    {
        return $this->_socket->readLine();
    }

    /**
     * Runs a generic dict query
     *
     * @param string $query dict query
     *
     * @return  mixed   string on success, else PEAR_Error
     */
    function simpleQuery($query)
    {
        $resp = $this->_sendCmd($query);

        if (PEAR::isError($resp)) {
            return $resp;
        }

        $resp = $this->_getMultiLine();

        $this->readLine(); /* discard status */

        return $resp['text'];
    }

    /**
     * Checks if a response code is positive
     *
     * @param array $resp response code
     *
     * @return  boolean
     */
    function isOK($resp)
    {
        $positives = array(1, 2, 3);

        return in_array(substr($resp['code'], 0, 1), $positives);
    }
}

?>
