1<?php
2/**
3 * Portions Copyright 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
4 * Copyright 2007-2016 Horde LLC (http://www.horde.org/)
5 *
6 * @author   Chuck Hagenbuch <chuck@horde.org>
7 * @license  http://www.horde.org/licenses/bsd BSD
8 * @category Horde
9 * @package  Feed
10 */
11
12/**
13 * @author   Chuck Hagenbuch <chuck@horde.org>
14 * @license  http://www.horde.org/licenses/bsd BSD
15 * @category Horde
16 * @package  Feed
17 */
18class Horde_Feed
19{
20    /**
21     * Create a Feed object based on a DOMDocument.
22     *
23     * @param DOMDocument $doc The DOMDocument object to import.
24     *
25     * @throws Horde_Feed_Exception
26     *
27     * @return Horde_Feed_Base The feed object imported from $doc
28     */
29    public static function create(DOMDocument $doc, $uri = null)
30    {
31        // Try to find the base feed element or a single <entry> of an
32        // Atom feed.
33        if ($feed = $doc->getElementsByTagName('feed')->item(0)) {
34            // Return an Atom feed.
35            return new Horde_Feed_Atom($feed, $uri);
36        } elseif ($entry = $doc->getElementsByTagName('entry')->item(0)) {
37            // Return an Atom single-entry feed.
38            $feeddoc = new DOMDocument($doc->version,
39                                       $doc->actualEncoding);
40            $feed = $feeddoc->appendChild($feeddoc->createElement('feed'));
41            $feed->appendChild($feeddoc->importNode($entry, true));
42
43            return new Horde_Feed_Atom($feed, $uri);
44        }
45
46        // Try to find the base feed element of an RSS feed.
47        if ($channel = $doc->getElementsByTagName('channel')->item(0)) {
48            // Return an RSS feed.
49            return new Horde_Feed_Rss($channel, $uri);
50        }
51
52        // Try to find an outline element of an OPML blogroll.
53        if ($outline = $doc->getElementsByTagName('outline')->item(0)) {
54            // Return a blogroll feed.
55            return new Horde_Feed_Blogroll($doc->documentElement, $uri);
56        }
57
58        // $doc does not appear to be a valid feed of the supported
59        // types.
60        throw new Horde_Feed_Exception('Invalid or unsupported feed format: '
61                                       . substr($doc->saveXML(), 0, 80) . '...');
62    }
63
64    /**
65     * Reads a feed represented by $string.
66     *
67     * @param string $string The XML content of the feed.
68     * @param string $uri The feed's URI location, if known.
69     *
70     * @throws Horde_Feed_Exception
71     *
72     * @return Horde_Feed_Base
73     */
74    public static function read($string, $uri = null)
75    {
76        // Load the feed as a DOMDocument object.
77        libxml_use_internal_errors(true);
78        $doc = new DOMDocument;
79        $doc->recover = true;
80        $loaded = $doc->loadXML($string);
81        if (!$loaded) {
82            $loaded = $doc->loadHTML($string);
83            if (!$loaded) {
84                self::_exception('DOMDocument cannot parse XML', libxml_get_last_error());
85            }
86        }
87
88        return self::create($doc);
89    }
90
91    /**
92     * Read a feed located at $uri
93     *
94     * @param string $uri The URI to fetch the feed from.
95     * @param Horde_Http_Client $httpclient The HTTP client to use.
96     *
97     * @throws Horde_Feed_Exception
98     *
99     * @return Horde_Feed_Base
100     */
101    public static function readUri($uri, Horde_Http_Client $httpclient = null)
102    {
103        if (is_null($httpclient)) {
104            $httpclient = new Horde_Http_Client();
105        }
106
107        try {
108            $response = $httpclient->get($uri);
109        } catch (Horde_Http_Exception $e) {
110            throw new Horde_Feed_Exception('Error reading feed: ' . $e->getMessage());
111        }
112        if ($response->code != 200) {
113            throw new Horde_Feed_Exception('Unable to read feed, got response code ' . $response->code);
114        }
115        $feed = $response->getBody();
116        return self::read($feed, $uri);
117    }
118
119    /**
120     * Read a feed from $filename
121     *
122     * @param string $filename The location of the feed file on an accessible
123     * filesystem or through an available stream wrapper.
124     *
125     * @throws Horde_Feed_Exception
126     *
127     * @return Horde_Feed_Base
128     */
129    public static function readFile($filename)
130    {
131        libxml_use_internal_errors(true);
132        $doc = new DOMDocument;
133        $doc->recover = true;
134        $contents = file_get_contents($filename);
135        $loaded = $doc->loadXML($contents);
136        if (!$loaded) {
137            $loaded = $doc->loadHTML($contents);
138            if (!$loaded) {
139                self::_exception('File could not be read or parsed', libxml_get_last_error());
140            }
141        }
142
143        return self::create($doc);
144    }
145
146    /**
147     * Builds an exception message from a libXMLError object.
148     *
149     * @param string $msg         An error message.
150     * @param libXMLError $error  An error object.
151     *
152     * @throws Horde_Feed_Exception
153     */
154    protected static function _exception($msg, $error)
155    {
156        if ($error) {
157            $msg .= ': ' . $error->message;
158            if ($error->file) {
159                $msg .= sprintf(' in file %s, line %d, column %d',
160                                $error->file, $error->line, $error->column);
161            }
162        }
163        throw new Horde_Feed_Exception($msg);
164    }
165}
166