format: Refactor JsonFormat to JSON Feed version 1 (#988)
JsonFormat now implements https://jsonfeed.org/version/1 Closes #618
This commit is contained in:
parent
288d4de218
commit
8801ac9e64
11 changed files with 385 additions and 10 deletions
|
@ -1,17 +1,109 @@
|
|||
<?php
|
||||
/**
|
||||
* Json
|
||||
* Builds a JSON string from $this->items and return it to browser.
|
||||
*/
|
||||
* JsonFormat - JSON Feed Version 1
|
||||
* https://jsonfeed.org/version/1
|
||||
*
|
||||
* Validators:
|
||||
* https://validator.jsonfeed.org
|
||||
* https://github.com/vigetlabs/json-feed-validator
|
||||
*/
|
||||
class JsonFormat extends FormatAbstract {
|
||||
public function stringify(){
|
||||
$items = $this->getItems();
|
||||
$data = array();
|
||||
const VENDOR_EXCLUDES = array(
|
||||
'author',
|
||||
'title',
|
||||
'uri',
|
||||
'timestamp',
|
||||
'content',
|
||||
'enclosures',
|
||||
'categories',
|
||||
);
|
||||
|
||||
foreach($items as $item) {
|
||||
$data[] = $item->toArray();
|
||||
public function stringify(){
|
||||
$urlScheme = (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'] : '';
|
||||
|
||||
$extraInfos = $this->getExtraInfos();
|
||||
|
||||
$data = array(
|
||||
'version' => 'https://jsonfeed.org/version/1',
|
||||
'title' => (!empty($extraInfos['name'])) ? $extraInfos['name'] : $urlHost,
|
||||
'home_page_url' => (!empty($extraInfos['uri'])) ? $extraInfos['uri'] : REPOSITORY,
|
||||
'feed_url' => $urlScheme . $urlHost . $urlRequest
|
||||
);
|
||||
|
||||
if (!empty($extraInfos['icon'])) {
|
||||
$data['icon'] = $extraInfos['icon'];
|
||||
$data['favicon'] = $extraInfos['icon'];
|
||||
}
|
||||
|
||||
$items = array();
|
||||
foreach ($this->getItems() as $item) {
|
||||
$entry = array();
|
||||
|
||||
$entryAuthor = $item->getAuthor();
|
||||
$entryTitle = $item->getTitle();
|
||||
$entryUri = $item->getURI();
|
||||
$entryTimestamp = $item->getTimestamp();
|
||||
$entryContent = $this->sanitizeHtml($item->getContent());
|
||||
$entryEnclosures = $item->getEnclosures();
|
||||
$entryCategories = $item->getCategories();
|
||||
|
||||
$vendorFields = $item->toArray();
|
||||
foreach (self::VENDOR_EXCLUDES as $key) {
|
||||
unset($vendorFields[$key]);
|
||||
}
|
||||
|
||||
$entry['id'] = $entryUri;
|
||||
|
||||
if (!empty($entryTitle)) {
|
||||
$entry['title'] = $entryTitle;
|
||||
}
|
||||
if (!empty($entryAuthor)) {
|
||||
$entry['author'] = array(
|
||||
'name' => $entryAuthor
|
||||
);
|
||||
}
|
||||
if (!empty($entryTimestamp)) {
|
||||
$entry['date_modified'] = gmdate(DATE_ATOM, $entryTimestamp);
|
||||
}
|
||||
if (!empty($entryUri)) {
|
||||
$entry['url'] = $entryUri;
|
||||
}
|
||||
if (!empty($entryContent)) {
|
||||
if ($this->isHTML($entryContent)) {
|
||||
$entry['content_html'] = $entryContent;
|
||||
} else {
|
||||
$entry['content_text'] = $entryContent;
|
||||
}
|
||||
}
|
||||
if (!empty($entryEnclosures)) {
|
||||
$entry['attachments'] = array();
|
||||
foreach ($entryEnclosures as $enclosure) {
|
||||
$entry['attachments'][] = array(
|
||||
'url' => $enclosure,
|
||||
'mime_type' => getMimeType($enclosure)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!empty($entryCategories)) {
|
||||
$entry['tags'] = array();
|
||||
foreach ($entryCategories as $category) {
|
||||
$entry['tags'][] = $category;
|
||||
}
|
||||
}
|
||||
if (!empty($vendorFields)) {
|
||||
$entry['_rssbridge'] = $vendorFields;
|
||||
}
|
||||
|
||||
if (empty($entry['id']))
|
||||
$entry['id'] = hash('sha1', $entryTitle . $entryContent);
|
||||
|
||||
$items[] = $entry;
|
||||
}
|
||||
$data['items'] = $items;
|
||||
|
||||
$toReturn = json_encode($data, JSON_PRETTY_PRINT);
|
||||
|
||||
// Remove invalid non-UTF8 characters
|
||||
|
@ -27,4 +119,8 @@ class JsonFormat extends FormatAbstract {
|
|||
|
||||
return parent::display();
|
||||
}
|
||||
|
||||
private function isHTML($text) {
|
||||
return (strlen(strip_tags($text)) != strlen($text));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,11 @@
|
|||
timeoutForLargeTests="6" >
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Standard test suite">
|
||||
<file>tests/BridgeImplementationTest.php</file>
|
||||
<testsuite name="implementations">
|
||||
<directory suffix="ImplementationTest.php">tests</directory>
|
||||
</testsuite>
|
||||
<testsuite name="formats">
|
||||
<directory suffix="FormatTest.php">tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
|
|
89
tests/JsonFormatTest.php
Normal file
89
tests/JsonFormatTest.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
/**
|
||||
* JsonFormat - JSON Feed Version 1
|
||||
* https://jsonfeed.org/version/1
|
||||
*/
|
||||
require_once __DIR__ . '/../lib/rssbridge.php';
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class JsonFormatTest extends TestCase {
|
||||
const PATH_SAMPLES = __DIR__ . '/samples/';
|
||||
const PATH_EXPECTED = __DIR__ . '/samples/expectedJsonFormat/';
|
||||
|
||||
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/json; charset=' . $this->format->getCharset(),
|
||||
xdebug_get_headers()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider sampleProvider
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function testOutput($path) {
|
||||
$this->setSample($path);
|
||||
$this->initFormat();
|
||||
|
||||
$this->assertJsonStringEqualsJsonFile($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)
|
||||
);
|
||||
} 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('Json');
|
||||
$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->assertNotNull(json_decode($this->data), 'invalid JSON output: ' . json_last_error_msg());
|
||||
ob_clean();
|
||||
}
|
||||
}
|
51
tests/samples/expectedJsonFormat/feed.common.json
Normal file
51
tests/samples/expectedJsonFormat/feed.common.json
Normal file
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"version": "https://jsonfeed.org/version/1",
|
||||
"title": "Sample feed with common data",
|
||||
"home_page_url": "https://example.com/blog/",
|
||||
"feed_url": "https://example.com/feed?type=common&items=4",
|
||||
"icon": "https://example.com/logo.png",
|
||||
"favicon": "https://example.com/logo.png",
|
||||
"items": [
|
||||
{
|
||||
"id": "http://example.com/blog/test-entry",
|
||||
"url": "http://example.com/blog/test-entry",
|
||||
"title": "Test Entry",
|
||||
"date_modified": "2018-12-01T12:00:00+00:00",
|
||||
"author": {
|
||||
"name": "fulmeek"
|
||||
},
|
||||
"content_text": "Hello world, this is a test entry.",
|
||||
"tags": ["test", "Hello World", "example"]
|
||||
},{
|
||||
"id": "https://jsonfeed.org/2017/05/17/announcing_json_feed",
|
||||
"url": "https://jsonfeed.org/2017/05/17/announcing_json_feed",
|
||||
"title": "Announcing JSON Feed",
|
||||
"date_modified": "2017-05-17T13:02:12+00:00",
|
||||
"author": {
|
||||
"name": "Brent Simmons and Manton Reece"
|
||||
},
|
||||
"content_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>\n\n<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>\n\n<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>\n\n<h4>Notes</h4>\n\n<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>\n\n<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>\n\n<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>\n\n<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>\n\n<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>"
|
||||
},{
|
||||
"id": "http://example.org/2005/04/02/atom",
|
||||
"url": "http://example.org/2005/04/02/atom",
|
||||
"title": "Atom draft-07 snapshot",
|
||||
"date_modified": "2005-07-31T12:29:29+00:00",
|
||||
"author": {
|
||||
"name": "Mark Pilgrim"
|
||||
},
|
||||
"content_html": "<p><i>[Update: The Atom draft is finished.]</i></p>",
|
||||
"attachments": [
|
||||
{
|
||||
"url": "http://example.org/audio/ph34r_my_podcast.mp3",
|
||||
"mime_type": "audio/mpeg"
|
||||
}
|
||||
]
|
||||
},{
|
||||
"id": "http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp",
|
||||
"url": "http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp",
|
||||
"title": "Star City",
|
||||
"date_modified": "2003-06-03T09:39:21+00:00",
|
||||
"content_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>."
|
||||
}
|
||||
]
|
||||
}
|
7
tests/samples/expectedJsonFormat/feed.empty.json
Normal file
7
tests/samples/expectedJsonFormat/feed.empty.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"version": "https://jsonfeed.org/version/1",
|
||||
"title": "Sample feed with minimum data",
|
||||
"home_page_url": "https://github.com/RSS-Bridge/rss-bridge/",
|
||||
"feed_url": "https://example.com/feed",
|
||||
"items": []
|
||||
}
|
15
tests/samples/expectedJsonFormat/feed.emptyItems.json
Normal file
15
tests/samples/expectedJsonFormat/feed.emptyItems.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"version": "https://jsonfeed.org/version/1",
|
||||
"title": "Sample feed with minimum data",
|
||||
"home_page_url": "https://github.com/RSS-Bridge/rss-bridge/",
|
||||
"feed_url": "https://example.com/feed",
|
||||
"items": [
|
||||
{
|
||||
"id": "29f59918d266c56a935da13e4122b524298e5a39",
|
||||
"title": "Sample Item #1"
|
||||
},{
|
||||
"id": "edf358cad1a7ae255d6bc97640dd9d27738f1b7b",
|
||||
"title": "Sample Item #2"
|
||||
}
|
||||
]
|
||||
}
|
19
tests/samples/expectedJsonFormat/feed.microblog.json
Normal file
19
tests/samples/expectedJsonFormat/feed.microblog.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"version": "https://jsonfeed.org/version/1",
|
||||
"title": "Sample microblog feed",
|
||||
"home_page_url": "https://example.com/blog/",
|
||||
"feed_url": "https://example.com/feed",
|
||||
"icon": "https://example.com/logo.png",
|
||||
"favicon": "https://example.com/logo.png",
|
||||
"items": [
|
||||
{
|
||||
"id": "1918f084648b82057c1dd3faa3d091da82a6fac2",
|
||||
"date_modified": "2018-10-07T16:53:03+00:00",
|
||||
"content_text": "Oh 😲 I found three monkeys 🙈🙉🙊"
|
||||
},{
|
||||
"id": "e62189168a06dfa74f61c621c79c33c4c8517e1f",
|
||||
"date_modified": "2018-10-07T16:38:17+00:00",
|
||||
"content_text": "Something happened"
|
||||
}
|
||||
]
|
||||
}
|
42
tests/samples/feed.common.json
Normal file
42
tests/samples/feed.common.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"server": {
|
||||
"HTTPS": "on",
|
||||
"HTTP_HOST": "example.com",
|
||||
"REQUEST_URI": "/feed?type=common&items=4"
|
||||
},
|
||||
"meta": {
|
||||
"name": "Sample feed with common data",
|
||||
"uri": "https://example.com/blog/",
|
||||
"icon": "https://example.com/logo.png"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"uri": "http://example.com/blog/test-entry",
|
||||
"title": "Test Entry",
|
||||
"timestamp": 1543665600,
|
||||
"author": "fulmeek",
|
||||
"content": "Hello world, this is a test entry.",
|
||||
"categories": ["test", "Hello World", "example"]
|
||||
},{
|
||||
"uri": "https://jsonfeed.org/2017/05/17/announcing_json_feed",
|
||||
"title": "Announcing JSON Feed",
|
||||
"timestamp": 1495026132,
|
||||
"author": "Brent Simmons and Manton Reece",
|
||||
"content": "<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>\n\n<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>\n\n<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>\n\n<h4>Notes</h4>\n\n<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>\n\n<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>\n\n<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>\n\n<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>\n\n<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>"
|
||||
},{
|
||||
"uri": "http://example.org/2005/04/02/atom",
|
||||
"title": "Atom draft-07 snapshot",
|
||||
"timestamp": 1122812969,
|
||||
"author": "Mark Pilgrim",
|
||||
"content": "<p><i>[Update: The Atom draft is finished.]</i></p>",
|
||||
"enclosures": [
|
||||
"http://example.org/audio/ph34r_my_podcast.mp3"
|
||||
]
|
||||
},{
|
||||
"uri": "http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp",
|
||||
"title": "Star City",
|
||||
"timestamp": 1054633161,
|
||||
"content": "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>."
|
||||
}
|
||||
]
|
||||
}
|
13
tests/samples/feed.empty.json
Normal file
13
tests/samples/feed.empty.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"server": {
|
||||
"HTTPS": "on",
|
||||
"HTTP_HOST": "example.com",
|
||||
"REQUEST_URI": "/feed"
|
||||
},
|
||||
"meta": {
|
||||
"name": "Sample feed with minimum data",
|
||||
"uri": "",
|
||||
"icon": ""
|
||||
},
|
||||
"items": []
|
||||
}
|
19
tests/samples/feed.emptyItems.json
Normal file
19
tests/samples/feed.emptyItems.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"server": {
|
||||
"HTTPS": "on",
|
||||
"HTTP_HOST": "example.com",
|
||||
"REQUEST_URI": "/feed"
|
||||
},
|
||||
"meta": {
|
||||
"name": "Sample feed with minimum data",
|
||||
"uri": "",
|
||||
"icon": ""
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"title": "Sample Item #1"
|
||||
},{
|
||||
"title": "Sample Item #2"
|
||||
}
|
||||
]
|
||||
}
|
21
tests/samples/feed.microblog.json
Normal file
21
tests/samples/feed.microblog.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"server": {
|
||||
"HTTPS": "on",
|
||||
"HTTP_HOST": "example.com",
|
||||
"REQUEST_URI": "/feed"
|
||||
},
|
||||
"meta": {
|
||||
"name": "Sample microblog feed",
|
||||
"uri": "https://example.com/blog/",
|
||||
"icon": "https://example.com/logo.png"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"timestamp": 1538931183,
|
||||
"content": "Oh 😲 I found three monkeys 🙈🙉🙊"
|
||||
},{
|
||||
"timestamp": 1538930297,
|
||||
"content": "Something happened"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue