[AtomFormat] Update to comply with RFC 4287 (#995)
https://tools.ietf.org/html/rfc4287
This commit is contained in:
parent
493e76e4b9
commit
ab2e566ee1
6 changed files with 312 additions and 23 deletions
|
@ -1,21 +1,30 @@
|
|||
<?php
|
||||
/**
|
||||
* Atom
|
||||
* Documentation Source http://en.wikipedia.org/wiki/Atom_%28standard%29 and
|
||||
* http://tools.ietf.org/html/rfc4287
|
||||
* AtomFormat - RFC 4287: The Atom Syndication Format
|
||||
* https://tools.ietf.org/html/rfc4287
|
||||
*
|
||||
* Validator:
|
||||
* https://validator.w3.org/feed/
|
||||
*/
|
||||
class AtomFormat extends FormatAbstract{
|
||||
public function stringify(){
|
||||
$https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 's' : '';
|
||||
$httpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
|
||||
$httpInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
|
||||
const LIMIT_TITLE = 140;
|
||||
|
||||
$serverRequestUri = isset($_SERVER['REQUEST_URI']) ? $this->xml_encode($_SERVER['REQUEST_URI']) : '';
|
||||
public function stringify(){
|
||||
$urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
|
||||
$urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
|
||||
$urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
|
||||
$urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
|
||||
|
||||
$feedUrl = $this->xml_encode($urlPrefix . $urlHost . $urlRequest);
|
||||
|
||||
$extraInfos = $this->getExtraInfos();
|
||||
$title = $this->xml_encode($extraInfos['name']);
|
||||
$uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
|
||||
|
||||
// since we can't guarantee that all items have an author,
|
||||
// a global feed author is mandatory
|
||||
$feedAuthor = 'RSS-Bridge';
|
||||
|
||||
$uriparts = parse_url($uri);
|
||||
if(!empty($extraInfos['icon'])) {
|
||||
$icon = $extraInfos['icon'];
|
||||
|
@ -27,11 +36,35 @@ class AtomFormat extends FormatAbstract{
|
|||
|
||||
$entries = '';
|
||||
foreach($this->getItems() as $item) {
|
||||
$entryAuthor = $this->xml_encode($item->getAuthor());
|
||||
$entryTimestamp = $item->getTimestamp();
|
||||
$entryTitle = $this->xml_encode($item->getTitle());
|
||||
$entryUri = $this->xml_encode($item->getURI());
|
||||
$entryTimestamp = $this->xml_encode(date(DATE_ATOM, $item->getTimestamp()));
|
||||
$entryContent = $this->xml_encode($this->sanitizeHtml($item->getContent()));
|
||||
$entryContent = $item->getContent();
|
||||
$entryUri = $item->getURI();
|
||||
|
||||
// the item id must be a valid unique URI
|
||||
$entryID = $this->xml_encode($entryUri);
|
||||
if (empty($entryID))
|
||||
$entryID = 'urn:sha1:' . hash('sha1', $entryTitle . $entryContent);
|
||||
|
||||
if (empty($entryTimestamp))
|
||||
$entryTimestamp = $this->lastModified;
|
||||
|
||||
if (empty($entryTitle)) {
|
||||
$entryTitle = str_replace("\n", ' ', strip_tags($entryContent));
|
||||
if (strlen($entryTitle) > self::LIMIT_TITLE) {
|
||||
$wrapPos = strpos(wordwrap($entryTitle, self::LIMIT_TITLE), "\n");
|
||||
$entryTitle = substr($entryTitle, 0, $wrapPos) . '...';
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($entryContent))
|
||||
$entryContent = $entryTitle;
|
||||
|
||||
$entryAuthor = $this->xml_encode($item->getAuthor());
|
||||
$entryTitle = $this->xml_encode($entryTitle);
|
||||
$entryUri = $this->xml_encode($entryUri);
|
||||
$entryTimestamp = $this->xml_encode(gmdate(DATE_ATOM, $entryTimestamp));
|
||||
$entryContent = $this->xml_encode($this->sanitizeHtml($entryContent));
|
||||
|
||||
$entryEnclosures = '';
|
||||
foreach($item->getEnclosures() as $enclosure) {
|
||||
|
@ -49,16 +82,28 @@ class AtomFormat extends FormatAbstract{
|
|||
. PHP_EOL;
|
||||
}
|
||||
|
||||
$entryLinkAlternate = '';
|
||||
if (!empty($entryUri)) {
|
||||
$entryLinkAlternate = '<link rel="alternate" type="text/html" href="'
|
||||
. $entryUri
|
||||
. '"/>';
|
||||
}
|
||||
|
||||
if (!empty($entryAuthor)) {
|
||||
$entryAuthor = '<author><name>'
|
||||
. $entryAuthor
|
||||
. '</name></author>';
|
||||
}
|
||||
|
||||
$entries .= <<<EOD
|
||||
|
||||
<entry>
|
||||
<author>
|
||||
<name>{$entryAuthor}</name>
|
||||
</author>
|
||||
<title type="html">{$entryTitle}</title>
|
||||
<link rel="alternate" type="text/html" href="{$entryUri}" />
|
||||
<id>{$entryUri}</id>
|
||||
<published>{$entryTimestamp}</published>
|
||||
<updated>{$entryTimestamp}</updated>
|
||||
<id>{$entryID}</id>
|
||||
{$entryLinkAlternate}
|
||||
{$entryAuthor}
|
||||
<content type="html">{$entryContent}</content>
|
||||
{$entryEnclosures}
|
||||
{$entryCategories}
|
||||
|
@ -67,21 +112,24 @@ class AtomFormat extends FormatAbstract{
|
|||
EOD;
|
||||
}
|
||||
|
||||
$feedTimestamp = date(DATE_ATOM, time());
|
||||
$feedTimestamp = gmdate(DATE_ATOM, $this->lastModified);
|
||||
$charset = $this->getCharset();
|
||||
|
||||
/* Data are prepared, now let's begin the "MAGIE !!!" */
|
||||
$toReturn = <<<EOD
|
||||
<?xml version="1.0" encoding="{$charset}"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0">
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
||||
<title type="text">{$title}</title>
|
||||
<id>http{$https}://{$httpHost}{$httpInfo}/</id>
|
||||
<id>{$feedUrl}</id>
|
||||
<icon>{$icon}</icon>
|
||||
<logo>{$icon}</logo>
|
||||
<updated>{$feedTimestamp}</updated>
|
||||
<author>
|
||||
<name>{$feedAuthor}</name>
|
||||
</author>
|
||||
<link rel="alternate" type="text/html" href="{$uri}" />
|
||||
<link rel="self" href="http{$https}://{$httpHost}{$serverRequestUri}" />
|
||||
<link rel="self" type="application/atom+xml" href="{$feedUrl}" />
|
||||
{$entries}
|
||||
</feed>
|
||||
EOD;
|
||||
|
|
89
tests/AtomFormatTest.php
Normal file
89
tests/AtomFormatTest.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
/**
|
||||
* AtomFormat - RFC 4287: The Atom Syndication Format
|
||||
* https://tools.ietf.org/html/rfc4287
|
||||
*/
|
||||
require_once __DIR__ . '/../lib/rssbridge.php';
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AtomFormatTest extends TestCase {
|
||||
const PATH_SAMPLES = __DIR__ . '/samples/';
|
||||
const PATH_EXPECTED = __DIR__ . '/samples/expectedAtomFormat/';
|
||||
|
||||
private $sample;
|
||||
private $format;
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* @dataProvider sampleProvider
|
||||
* @runInSeparateProcess
|
||||
* @requires function xdebug_get_headers
|
||||
*/
|
||||
public function testHeaders($path) {
|
||||
$this->setSample($path);
|
||||
$this->initFormat();
|
||||
|
||||
$this->assertContains(
|
||||
'Content-Type: application/atom+xml; charset=' . $this->format->getCharset(),
|
||||
xdebug_get_headers()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider sampleProvider
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function testOutput($path) {
|
||||
$this->setSample($path);
|
||||
$this->initFormat();
|
||||
|
||||
$this->assertXmlStringEqualsXmlFile($this->sample->expected, $this->data);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public function sampleProvider() {
|
||||
$samples = array();
|
||||
foreach (glob(self::PATH_SAMPLES . '*.json') as $path) {
|
||||
$samples[basename($path, '.json')] = array($path);
|
||||
}
|
||||
return $samples;
|
||||
}
|
||||
|
||||
private function setSample($path) {
|
||||
$data = json_decode(file_get_contents($path), true);
|
||||
if (isset($data['meta']) && isset($data['items'])) {
|
||||
if (!empty($data['server']))
|
||||
$this->setServerVars($data['server']);
|
||||
|
||||
$items = array();
|
||||
foreach($data['items'] as $item) {
|
||||
$items[] = new \FeedItem($item);
|
||||
}
|
||||
|
||||
$this->sample = (object)array(
|
||||
'meta' => $data['meta'],
|
||||
'items' => $items,
|
||||
'expected' => self::PATH_EXPECTED . basename($path, '.json') . '.xml'
|
||||
);
|
||||
} else {
|
||||
$this->fail('invalid test sample: ' . basename($path, '.json'));
|
||||
}
|
||||
}
|
||||
|
||||
private function setServerVars($list) {
|
||||
$_SERVER = array_merge($_SERVER, $list);
|
||||
}
|
||||
|
||||
private function initFormat() {
|
||||
$this->format = \Format::create('Atom');
|
||||
$this->format->setItems($this->sample->items);
|
||||
$this->format->setExtraInfos($this->sample->meta);
|
||||
$this->format->setLastModified(strtotime('2000-01-01 12:00:00 UTC'));
|
||||
|
||||
$this->data = $this->getActualOutput($this->format->display());
|
||||
$this->assertNotFalse(simplexml_load_string($this->data));
|
||||
ob_clean();
|
||||
}
|
||||
}
|
77
tests/samples/expectedAtomFormat/feed.common.xml
Normal file
77
tests/samples/expectedAtomFormat/feed.common.xml
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
||||
<title type="text">Sample feed with common data</title>
|
||||
<id>https://example.com/feed?type=common&items=4</id>
|
||||
<icon>https://example.com/logo.png</icon>
|
||||
<logo>https://example.com/logo.png</logo>
|
||||
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||
<author>
|
||||
<name>RSS-Bridge</name>
|
||||
</author>
|
||||
<link href="https://example.com/blog/" rel="alternate" type="text/html"/>
|
||||
<link href="https://example.com/feed?type=common&items=4" rel="self" type="application/atom+xml"/>
|
||||
|
||||
<entry>
|
||||
<title type="html">Test Entry</title>
|
||||
<published>2018-12-01T12:00:00+00:00</published>
|
||||
<updated>2018-12-01T12:00:00+00:00</updated>
|
||||
<id>http://example.com/blog/test-entry</id>
|
||||
<link href="http://example.com/blog/test-entry" rel="alternate" type="text/html"/>
|
||||
<author>
|
||||
<name>fulmeek</name>
|
||||
</author>
|
||||
<content type="html">Hello world, this is a test entry.</content>
|
||||
<category term="test"/>
|
||||
<category term="Hello World"/>
|
||||
<category term="example"/>
|
||||
</entry>
|
||||
<entry>
|
||||
<title type="html">Announcing JSON Feed</title>
|
||||
<published>2017-05-17T13:02:12+00:00</published>
|
||||
<updated>2017-05-17T13:02:12+00:00</updated>
|
||||
<id>https://jsonfeed.org/2017/05/17/announcing_json_feed</id>
|
||||
<link href="https://jsonfeed.org/2017/05/17/announcing_json_feed" rel="alternate" type="text/html"/>
|
||||
<author>
|
||||
<name>Brent Simmons and Manton Reece</name>
|
||||
</author>
|
||||
<content type="html"><p>We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers’ choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and it’s less prone to bugs.</p>
|
||||
|
||||
<p>So we developed JSON Feed, a format similar to <a href="http://cyber.harvard.edu/rss/rss.html">RSS</a> and <a href="https://tools.ietf.org/html/rfc4287">Atom</a> but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.</p>
|
||||
|
||||
<p><a href="https://jsonfeed.org/version/1">See the spec</a>. It’s at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.</p>
|
||||
|
||||
<h4>Notes</h4>
|
||||
|
||||
<p>We have a <a href="https://github.com/manton/jsonfeed-wp">WordPress plugin</a> and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, we’ll update the <a href="https://jsonfeed.org/code">code</a> page.</p>
|
||||
|
||||
<p>See <a href="https://jsonfeed.org/mappingrssandatom">Mapping RSS and Atom to JSON Feed</a> for more on the similarities between the formats.</p>
|
||||
|
||||
<p>This website — the Markdown files and supporting resources — <a href="https://github.com/brentsimmons/JSONFeed">is up on GitHub</a>, and you’re welcome to comment there.</p>
|
||||
|
||||
<p>This website is also a blog, and you can subscribe to the <a href="https://jsonfeed.org/xml/rss.xml">RSS feed</a> or the <a href="https://jsonfeed.org/feed.json">JSON feed</a> (if your reader supports it).</p>
|
||||
|
||||
<p>We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the <a href="https://jsonfeed.org/version/1">spec</a>. But — most importantly — <a href="http://furbo.org/">Craig Hockenberry</a> spent a little time making it look pretty. :)</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title type="html">Atom draft-07 snapshot</title>
|
||||
<published>2005-07-31T12:29:29+00:00</published>
|
||||
<updated>2005-07-31T12:29:29+00:00</updated>
|
||||
<id>http://example.org/2005/04/02/atom</id>
|
||||
<link href="http://example.org/2005/04/02/atom" rel="alternate" type="text/html"/>
|
||||
<author>
|
||||
<name>Mark Pilgrim</name>
|
||||
</author>
|
||||
<content type="html"><p><i>[Update: The Atom draft is finished.]</i></p></content>
|
||||
<link rel="enclosure" type="audio/mpeg" href="http://example.org/audio/ph34r_my_podcast.mp3"/>
|
||||
</entry>
|
||||
<entry>
|
||||
<title type="html">Star City</title>
|
||||
<published>2003-06-03T09:39:21+00:00</published>
|
||||
<updated>2003-06-03T09:39:21+00:00</updated>
|
||||
<id>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</id>
|
||||
<link href="http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp" rel="alternate" type="text/html"/>
|
||||
<content type="html">How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.</content>
|
||||
</entry>
|
||||
|
||||
</feed>
|
15
tests/samples/expectedAtomFormat/feed.empty.xml
Normal file
15
tests/samples/expectedAtomFormat/feed.empty.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
||||
<title type="text">Sample feed with minimum data</title>
|
||||
<id>https://example.com/feed</id>
|
||||
<icon>https://github.com/favicon.ico</icon>
|
||||
<logo>https://github.com/favicon.ico</logo>
|
||||
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||
<author>
|
||||
<name>RSS-Bridge</name>
|
||||
</author>
|
||||
<link href="https://github.com/RSS-Bridge/rss-bridge/" rel="alternate" type="text/html"/>
|
||||
<link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
|
||||
|
||||
</feed>
|
30
tests/samples/expectedAtomFormat/feed.emptyItems.xml
Normal file
30
tests/samples/expectedAtomFormat/feed.emptyItems.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
||||
<title type="text">Sample feed with minimum data</title>
|
||||
<id>https://example.com/feed</id>
|
||||
<icon>https://github.com/favicon.ico</icon>
|
||||
<logo>https://github.com/favicon.ico</logo>
|
||||
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||
<author>
|
||||
<name>RSS-Bridge</name>
|
||||
</author>
|
||||
<link href="https://github.com/RSS-Bridge/rss-bridge/" rel="alternate" type="text/html"/>
|
||||
<link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
|
||||
|
||||
<entry>
|
||||
<title type="html">Sample Item #1</title>
|
||||
<published>2000-01-01T12:00:00+00:00</published>
|
||||
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||
<id>urn:sha1:29f59918d266c56a935da13e4122b524298e5a39</id>
|
||||
<content type="html">Sample Item #1</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title type="html">Sample Item #2</title>
|
||||
<published>2000-01-01T12:00:00+00:00</published>
|
||||
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||
<id>urn:sha1:edf358cad1a7ae255d6bc97640dd9d27738f1b7b</id>
|
||||
<content type="html">Sample Item #2</content>
|
||||
</entry>
|
||||
|
||||
</feed>
|
30
tests/samples/expectedAtomFormat/feed.microblog.xml
Normal file
30
tests/samples/expectedAtomFormat/feed.microblog.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
||||
<title type="text">Sample microblog feed</title>
|
||||
<id>https://example.com/feed</id>
|
||||
<icon>https://example.com/logo.png</icon>
|
||||
<logo>https://example.com/logo.png</logo>
|
||||
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||
<author>
|
||||
<name>RSS-Bridge</name>
|
||||
</author>
|
||||
<link href="https://example.com/blog/" rel="alternate" type="text/html"/>
|
||||
<link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
|
||||
|
||||
<entry>
|
||||
<title type="html">Oh 😲 I found three monkeys 🙈🙉🙊</title>
|
||||
<published>2018-10-07T16:53:03+00:00</published>
|
||||
<updated>2018-10-07T16:53:03+00:00</updated>
|
||||
<id>urn:sha1:1918f084648b82057c1dd3faa3d091da82a6fac2</id>
|
||||
<content type="html">Oh 😲 I found three monkeys 🙈🙉🙊</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title type="html">Something happened</title>
|
||||
<published>2018-10-07T16:38:17+00:00</published>
|
||||
<updated>2018-10-07T16:38:17+00:00</updated>
|
||||
<id>urn:sha1:e62189168a06dfa74f61c621c79c33c4c8517e1f</id>
|
||||
<content type="html">Something happened</content>
|
||||
</entry>
|
||||
|
||||
</feed>
|
Loading…
Reference in a new issue