1<?php
2
3/**
4 * FeedCreator is the abstract base implementation for concrete
5 * implementations that implement a specific format of syndication.
6 *
7 * @author  Kai Blankenhorn <kaib@bitfolge.de>
8 * @since   1.4
9 */
10abstract class FeedCreator extends HtmlDescribable
11{
12
13    /**
14     * Mandatory attributes of a feed.
15     */
16    public $title, $description, $link;
17    public $format = 'BASE';
18
19    /**
20     * Optional attributes of a feed.
21     */
22    public $syndicationURL, $image, $language, $copyright, $pubDate, $lastBuildDate, $editor, $editorEmail, $webmaster, $category, $docs, $ttl, $rating, $skipHours, $skipDays;
23
24    /**
25     * The url of the external xsl stylesheet used to format the naked rss feed.
26     * Ignored in the output when empty.
27     */
28    public $xslStyleSheet = "";
29
30
31    /** @var FeedItem[] */
32    public $items = Array();
33
34    /**
35     * Generator string
36     */
37    public $generator = "info@mypapit.net";
38
39    /**
40     * This feed's MIME content type.
41     *
42     * @since  1.4
43     * @access private
44     */
45    protected $contentType = "application/xml";
46
47    /**
48     * This feed's character encoding.
49     *
50     * @since 1.6.1
51     */
52    protected $encoding = "UTF-8"; //"ISO-8859-1";
53
54    /**
55     * Any additional elements to include as an associated array. All $key => $value pairs
56     * will be included unencoded in the feed in the form
57     *     <$key>$value</$key>
58     * Again: No encoding will be used! This means you can invalidate or enhance the feed
59     * if $value contains markup. This may be abused to embed tags not implemented by
60     * the FeedCreator class used.
61     */
62    public $additionalElements = Array();
63
64    /**
65     * Adds a FeedItem to the feed.
66     *
67     * @param FeedItem $item The FeedItem to add to the feed.
68     */
69    public function addItem($item)
70    {
71        $this->items[] = $item;
72    }
73
74    /**
75     * Get the version string for the generator
76     *
77     * @return string
78     */
79    public function version()
80    {
81        return FEEDCREATOR_VERSION." (".$this->generator.")";
82    }
83
84    /**
85     * Truncates a string to a certain length at the most sensible point.
86     * First, if there's a '.' character near the end of the string, the string is truncated after this character.
87     * If there is no '.', the string is truncated after the last ' ' character.
88     * If the string is truncated, " ..." is appended.
89     * If the string is already shorter than $length, it is returned unchanged.
90     *
91     * @param string $string A string to be truncated.
92     * @param int $length    the maximum length the string should be truncated to
93     * @return string the truncated string
94     */
95    public static function iTrunc($string, $length)
96    {
97        if (strlen($string) <= $length) {
98            return $string;
99        }
100
101        $pos = strrpos($string, ".");
102        if ($pos >= $length - 4) {
103            $string = substr($string, 0, $length - 4);
104            $pos = strrpos($string, ".");
105        }
106        if ($pos >= $length * 0.4) {
107            return substr($string, 0, $pos + 1)." ...";
108        }
109
110        $pos = strrpos($string, " ");
111        if ($pos >= $length - 4) {
112            $string = substr($string, 0, $length - 4);
113            $pos = strrpos($string, " ");
114        }
115        if ($pos >= $length * 0.4) {
116            return substr($string, 0, $pos)." ...";
117        }
118
119        return substr($string, 0, $length - 4)." ...";
120
121    }
122
123    /**
124     * Creates a comment indicating the generator of this feed.
125     * The format of this comment seems to be recognized by
126     * Syndic8.com.
127     */
128    protected function _createGeneratorComment()
129    {
130        return "<!-- generator=\"".FEEDCREATOR_VERSION."\" -->\n";
131    }
132
133    /**
134     * Creates a string containing all additional elements specified in
135     * $additionalElements.
136     *
137     * @param array $elements      an associative array containing key => value pairs
138     * @param string $indentString a string that will be inserted before every generated line
139     * @return string the XML tags corresponding to $additionalElements
140     */
141    protected function _createAdditionalElements($elements, $indentString = "")
142    {
143        $ae = "";
144        if (is_array($elements)) {
145            foreach ($elements AS $key => $value) {
146                $ae .= $indentString."<$key>$value</$key>\n";
147            }
148        }
149
150        return $ae;
151    }
152
153    protected function _createStylesheetReferences()
154    {
155        $xml = "";
156        if (!empty($this->cssStyleSheet)) {
157            $xml .= "<?xml-stylesheet href=\"".$this->cssStyleSheet."\" type=\"text/css\"?>\n";
158        }
159        if (!empty($this->xslStyleSheet)) {
160            $xml .= "<?xml-stylesheet href=\"".$this->xslStyleSheet."\" type=\"text/xsl\"?>\n";
161        }
162
163        return $xml;
164    }
165
166    /**
167     * Builds the feed's text.
168     *
169     * @return string the feed's complete text
170     */
171    abstract public function createFeed();
172
173    /**
174     * Generate a filename for the feed cache file. The result will be $_SERVER["PHP_SELF"] with the extension changed
175     * to .xml. For example: echo $_SERVER["PHP_SELF"]."\n"; echo FeedCreator::_generateFilename(); would produce:
176     * /rss/latestnews.php
177     * latestnews.xml
178     *
179     * @return string the feed cache filename
180     * @since  1.4
181     * @access private
182     */
183    protected function _generateFilename()
184    {
185        $fileInfo = pathinfo($_SERVER["PHP_SELF"]);
186
187        return substr($fileInfo["basename"], 0, -(strlen($fileInfo["extension"]) + 1)).".xml";
188    }
189
190    /**
191     * Send given file to Browser
192     *
193     * @since 1.4
194     * @param string $filename
195     */
196    protected function _redirect($filename)
197    {
198        // attention, heavily-commented-out-area
199
200        // maybe use this in addition to file time checking
201        //header("Expires: ".date("r",time()+$this->_timeout));
202
203        /* no caching at all, doesn't seem to work as good:
204         header("Cache-Control: no-cache");
205        header("Pragma: no-cache");
206        */
207
208        // HTTP redirect, some feed readers' simple HTTP implementations don't follow it
209        //header("Location: ".$filename);
210
211        header("Content-Type: ".$this->contentType."; charset=".$this->encoding."; filename=".basename($filename));
212        if (preg_match('/\.(kml|gpx)$/', $filename)) {
213            header("Content-Disposition: attachment; filename=".basename($filename));
214        } else {
215            header("Content-Disposition: inline; filename=".basename($filename));
216        }
217        readfile($filename);
218        exit();
219    }
220
221    /**
222     * Turns on caching and checks if there is a recent version of this feed in the cache.
223     * If there is, an HTTP redirect header is sent.
224     * To effectively use caching, you should create the FeedCreator object and call this method
225     * before anything else, especially before you do the time consuming task to build the feed
226     * (web fetching, for example).
227     *
228     * @since 1.4
229     * @param string $filename optional    the filename where a recent version of the feed is saved. If not specified,
230     *                         the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see
231     *                         _generateFilename()).
232     * @param int $timeout     optional    the timeout in seconds before a cached version is refreshed (defaults to
233     *                         3600 = 1 hour)
234     */
235    public function useCached($filename = "", $timeout = 3600)
236    {
237        $this->_timeout = $timeout;
238        if ($filename == "") {
239            $filename = $this->_generateFilename();
240        }
241        if (file_exists($filename) AND (time() - filemtime($filename) < $timeout)) {
242            $this->_redirect($filename);
243        }
244    }
245
246    /**
247     * Saves this feed as a file on the local disk. After the file is saved, a redirect
248     * header may be sent to redirect the user to the newly created file.
249     *
250     * @since 1.4
251     * @param string $filename      optional    the filename where a recent version of the feed is saved. If not
252     *                              specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml
253     *                              (see _generateFilename()).
254     * @param bool $displayContents optional    send an HTTP redirect header or not. If true, the user will be
255     *                              automatically redirected to the created file.
256     */
257    public function saveFeed($filename = "", $displayContents = true)
258    {
259        if ($filename == "") {
260            $filename = $this->_generateFilename();
261        }
262        $feedFile = fopen($filename, "w+");
263        if ($feedFile) {
264            fputs($feedFile, $this->createFeed());
265            fclose($feedFile);
266            if ($displayContents) {
267                $this->_redirect($filename);
268            }
269        } else {
270            echo "<br /><b>Error creating feed file, please check write permissions.</b><br />";
271        }
272    }
273}
274