<?php

/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Feed
 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Feed.php 25160 2012-12-18 15:17:16Z matthew $
 */


/**
 * Feed utility class
 *
 * Base Zend_Feed class, containing constants and the Zend_Http_Client instance
 * accessor.
 *
 * @category   Zend
 * @package    Zend_Feed
 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Feed
{

    /**
     * HTTP client object to use for retrieving feeds
     *
     * @var Zend_Http_Client
     */
    protected static $_httpClient = null;

    /**
     * Override HTTP PUT and DELETE request methods?
     *
     * @var boolean
     */
    protected static $_httpMethodOverride = false;

    /**
     * @var array
     */
    protected static $_namespaces = array(
        'opensearch' => 'http://a9.com/-/spec/opensearchrss/1.0/',
        'atom'       => 'http://www.w3.org/2005/Atom',
        'rss'        => 'http://blogs.law.harvard.edu/tech/rss',
    );


    /**
     * Set the HTTP client instance
     *
     * Sets the HTTP client object to use for retrieving the feeds.
     *
     * @param  Zend_Http_Client $httpClient
     * @return void
     */
    public static function setHttpClient(Zend_Http_Client $httpClient)
    {
        self::$_httpClient = $httpClient;
    }


    /**
     * Gets the HTTP client object. If none is set, a new Zend_Http_Client will be used.
     *
     * @return Zend_Http_Client_Abstract
     */
    public static function getHttpClient()
    {
        if (!self::$_httpClient instanceof Zend_Http_Client) {
            /**
             * @see Zend_Http_Client
             */
            require_once 'Zend/Http/Client.php';
            self::$_httpClient = new Zend_Http_Client();
        }

        return self::$_httpClient;
    }


    /**
     * Toggle using POST instead of PUT and DELETE HTTP methods
     *
     * Some feed implementations do not accept PUT and DELETE HTTP
     * methods, or they can't be used because of proxies or other
     * measures. This allows turning on using POST where PUT and
     * DELETE would normally be used; in addition, an
     * X-Method-Override header will be sent with a value of PUT or
     * DELETE as appropriate.
     *
     * @param  boolean $override Whether to override PUT and DELETE.
     * @return void
     */
    public static function setHttpMethodOverride($override = true)
    {
        self::$_httpMethodOverride = $override;
    }


    /**
     * Get the HTTP override state
     *
     * @return boolean
     */
    public static function getHttpMethodOverride()
    {
        return self::$_httpMethodOverride;
    }


    /**
     * Get the full version of a namespace prefix
     *
     * Looks up a prefix (atom:, etc.) in the list of registered
     * namespaces and returns the full namespace URI if
     * available. Returns the prefix, unmodified, if it's not
     * registered.
     *
     * @return string
     */
    public static function lookupNamespace($prefix)
    {
        return isset(self::$_namespaces[$prefix]) ?
            self::$_namespaces[$prefix] :
            $prefix;
    }


    /**
     * Add a namespace and prefix to the registered list
     *
     * Takes a prefix and a full namespace URI and adds them to the
     * list of registered namespaces for use by
     * Zend_Feed::lookupNamespace().
     *
     * @param  string $prefix The namespace prefix
     * @param  string $namespaceURI The full namespace URI
     * @return void
     */
    public static function registerNamespace($prefix, $namespaceURI)
    {
        self::$_namespaces[$prefix] = $namespaceURI;
    }


    /**
     * Imports a feed located at $uri.
     *
     * @param  string $uri
     * @throws Zend_Feed_Exception
     * @return Zend_Feed_Abstract
     */
    public static function import($uri)
    {
        $client = self::getHttpClient();
        $client->setUri($uri);
        $response = $client->request('GET');
        if ($response->getStatus() !== 200) {
            /**
             * @see Zend_Feed_Exception
             */
            require_once 'Zend/Feed/Exception.php';
            throw new Zend_Feed_Exception('Feed failed to load, got response code ' . $response->getStatus());
        }
        $feed = $response->getBody();
        return self::importString($feed);
    }


    /**
     * Imports a feed represented by $string.
     *
     * @param  string $string
     * @throws Zend_Feed_Exception
     * @return Zend_Feed_Abstract
     */
    public static function importString($string)
    {
        // Load the feed as an XML DOMDocument object
        $libxml_errflag       = libxml_use_internal_errors(true);
        $libxml_entity_loader = libxml_disable_entity_loader(true);
        $doc = new DOMDocument;
        if (trim($string) == '') {
            require_once 'Zend/Feed/Exception.php';
            throw new Zend_Feed_Exception('Document/string being imported'
            . ' is an Empty string or comes from an empty HTTP response');
        }
        $status = $doc->loadXML($string);
        libxml_disable_entity_loader($libxml_entity_loader);
        libxml_use_internal_errors($libxml_errflag);

        if (!$status) {
            // prevent the class to generate an undefined variable notice (ZF-2590)
            // Build error message
            $error = libxml_get_last_error();
            if ($error && $error->message) {
                $errormsg = "DOMDocument cannot parse XML: {$error->message}";
            } else {
                $errormsg = "DOMDocument cannot parse XML";
            }


            /**
             * @see Zend_Feed_Exception
             */
            require_once 'Zend/Feed/Exception.php';
            throw new Zend_Feed_Exception($errormsg);
        }

        // Try to find the base feed element or a single <entry> of an Atom feed
        if ($doc->getElementsByTagName('feed')->item(0) ||
            $doc->getElementsByTagName('entry')->item(0)) {
            /**
             * @see Zend_Feed_Atom
             */
            require_once 'Zend/Feed/Atom.php';
            // return a newly created Zend_Feed_Atom object
            return new Zend_Feed_Atom(null, $string);
        }

        // Try to find the base feed element of an RSS feed
        if ($doc->getElementsByTagName('channel')->item(0)) {
            /**
             * @see Zend_Feed_Rss
             */
            require_once 'Zend/Feed/Rss.php';
            // return a newly created Zend_Feed_Rss object
            return new Zend_Feed_Rss(null, $string);
        }

        // $string does not appear to be a valid feed of the supported types
        /**
         * @see Zend_Feed_Exception
         */
        require_once 'Zend/Feed/Exception.php';
        throw new Zend_Feed_Exception('Invalid or unsupported feed format');
    }


    /**
     * Imports a feed from a file located at $filename.
     *
     * @param  string $filename
     * @throws Zend_Feed_Exception
     * @return Zend_Feed_Abstract
     */
    public static function importFile($filename)
    {
        @ini_set('track_errors', 1);
        $feed = @file_get_contents($filename);
        @ini_restore('track_errors');
        if ($feed === false) {
            /**
             * @see Zend_Feed_Exception
             */
            require_once 'Zend/Feed/Exception.php';
            throw new Zend_Feed_Exception("File could not be loaded: $php_errormsg");
        }
        return self::importString($feed);
    }


    /**
     * Attempts to find feeds at $uri referenced by <link ... /> tags. Returns an
     * array of the feeds referenced at $uri.
     *
     * @todo Allow findFeeds() to follow one, but only one, code 302.
     *
     * @param  string $uri
     * @throws Zend_Feed_Exception
     * @return array
     */
    public static function findFeeds($uri)
    {
        // Get the HTTP response from $uri and save the contents
        $client = self::getHttpClient();
        $client->setUri($uri);
        $response = $client->request();
        if ($response->getStatus() !== 200) {
            /**
             * @see Zend_Feed_Exception
             */
            require_once 'Zend/Feed/Exception.php';
            throw new Zend_Feed_Exception("Failed to access $uri, got response code " . $response->getStatus());
        }
        $contents = $response->getBody();

        // Parse the contents for appropriate <link ... /> tags
        @ini_set('track_errors', 1);
        $pattern = '~(<link[^>]+)/?>~i';
        $result = @preg_match_all($pattern, $contents, $matches);
        @ini_restore('track_errors');
        if ($result === false) {
            /**
             * @see Zend_Feed_Exception
             */
            require_once 'Zend/Feed/Exception.php';
            throw new Zend_Feed_Exception("Internal error: $php_errormsg");
        }

        // Try to fetch a feed for each link tag that appears to refer to a feed
        $feeds = array();
        if (isset($matches[1]) && count($matches[1]) > 0) {
            foreach ($matches[1] as $link) {
                // force string to be an utf-8 one
                if (!mb_check_encoding($link, 'UTF-8')) {
                    $link = mb_convert_encoding($link, 'UTF-8');
                }
                $xml = @simplexml_load_string(rtrim($link, ' /') . ' />');
                if ($xml === false) {
                    continue;
                }
                $attributes = $xml->attributes();
                if (!isset($attributes['rel']) || !@preg_match('~^(?:alternate|service\.feed)~i', $attributes['rel'])) {
                    continue;
                }
                if (!isset($attributes['type']) ||
                        !@preg_match('~^application/(?:atom|rss|rdf)\+xml~', $attributes['type'])) {
                    continue;
                }
                if (!isset($attributes['href'])) {
                    continue;
                }
                try {
                    // checks if we need to canonize the given uri
                    try {
                        $uri = Zend_Uri::factory((string) $attributes['href']);
                    } catch (Zend_Uri_Exception $e) {
                        // canonize the uri
                        $path = (string) $attributes['href'];
                        $query = $fragment = '';
                        if (substr($path, 0, 1) != '/') {
                            // add the current root path to this one
                            $path = rtrim($client->getUri()->getPath(), '/') . '/' . $path;
                        }
                        if (strpos($path, '?') !== false) {
                            list($path, $query) = explode('?', $path, 2);
                        }
                        if (strpos($query, '#') !== false) {
                            list($query, $fragment) = explode('#', $query, 2);
                        }
                        $uri = Zend_Uri::factory($client->getUri(true));
                        $uri->setPath($path);
                        $uri->setQuery($query);
                        $uri->setFragment($fragment);
                    }

                    $feed = self::import($uri);
                } catch (Exception $e) {
                    continue;
                }
                $feeds[$uri->getUri()] = $feed;
            }
        }

        // Return the fetched feeds
        return $feeds;
    }

    /**
     * Construct a new Zend_Feed_Abstract object from a custom array
     *
     * @param  array  $data
     * @param  string $format (rss|atom) the requested output format
     * @return Zend_Feed_Abstract
     */
    public static function importArray(array $data, $format = 'atom')
    {
        $obj = 'Zend_Feed_' . ucfirst(strtolower($format));
        if (!class_exists($obj)) {
            require_once 'Zend/Loader.php';
            Zend_Loader::loadClass($obj);
        }

        /**
         * @see Zend_Feed_Builder
         */
        require_once 'Zend/Feed/Builder.php';
        return new $obj(null, null, new Zend_Feed_Builder($data));
    }

    /**
     * Construct a new Zend_Feed_Abstract object from a Zend_Feed_Builder_Interface data source
     *
     * @param  Zend_Feed_Builder_Interface $builder this object will be used to extract the data of the feed
     * @param  string                      $format (rss|atom) the requested output format
     * @return Zend_Feed_Abstract
     */
    public static function importBuilder(Zend_Feed_Builder_Interface $builder, $format = 'atom')
    {
        $obj = 'Zend_Feed_' . ucfirst(strtolower($format));
        if (!class_exists($obj)) {
            require_once 'Zend/Loader.php';
            Zend_Loader::loadClass($obj);
        }
        return new $obj(null, null, $builder);
    }
}
