Upgrade to PicoFeed v0.1.23

- OPML import/export with categories
- New Feed export (bookmarks)
This commit is contained in:
Frederic Guillot 2016-04-17 19:34:54 -04:00
parent e0e95d67f7
commit 427b41f892
43 changed files with 2153 additions and 1071 deletions

View File

@ -15,7 +15,7 @@
"fguillot/simple-validator": "v1.0.0",
"fguillot/json-rpc": "v1.0.2",
"fguillot/picodb": "v1.0.2",
"fguillot/picofeed": "v0.1.21"
"fguillot/picofeed": "v0.1.23"
},
"require-dev": {
"phpunit/phpunit": "4.8.3",

View File

@ -1,10 +1,10 @@
<?php
use PicoFeed\Syndication\Atom;
use PicoFeed\Syndication\AtomFeedBuilder;
use PicoFeed\Syndication\AtomItemBuilder;
// Ajax call to add or remove a bookmark
Router\post_action('bookmark', function() {
$id = Request\param('id');
$value = Request\int_param('value');
@ -17,7 +17,6 @@ Router\post_action('bookmark', function() {
// Add new bookmark
Router\get_action('bookmark', function() {
$id = Request\param('id');
$menu = Request\param('menu', 'unread');
$source = Request\param('source', 'unread');
@ -27,21 +26,22 @@ Router\get_action('bookmark', function() {
Model\Item\set_bookmark_value($id, Request\int_param('value'));
if ($source === 'show') {
Response\Redirect('?action=show&menu='.$menu.'&id='.$id);
Response\redirect('?action=show&menu='.$menu.'&id='.$id);
}
Response\Redirect('?action='.$menu.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id);
Response\redirect('?action='.$menu.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id);
});
// Display bookmarks page
Router\get_action('bookmarks', function() {
$offset = Request\int_param('offset', 0);
$group_id = Request\int_param('group_id', null);
$feed_ids = array();
if (! is_null($group_id)) {
$feed_ids = Model\Group\get_feeds_by_group($group_id);
}
$nb_items = Model\Item\count_bookmarks($feed_ids);
$items = Model\Item\get_bookmarks(
$offset,
@ -71,9 +71,9 @@ Router\get_action('bookmarks', function() {
// Display bookmark feeds
Router\get_action('bookmark-feed', function() {
// Select database if the parameter is set
$database = Request\param('database');
if (!empty($database)) {
Model\Database\select($database);
}
@ -87,25 +87,30 @@ Router\get_action('bookmark-feed', function() {
}
// Build Feed
$writer = new Atom;
$writer->title = t('Bookmarks').' - Miniflux';
$writer->site_url = Helper\get_current_base_url();
$writer->feed_url = $writer->site_url.'?action=bookmark-feed&token='.urlencode($feed_token);
$bookmarks = Model\Item\get_bookmarks();
$feedBuilder = AtomFeedBuilder::create()
->withTitle(t('Bookmarks').' - Miniflux')
->withFeedUrl(Helper\get_current_base_url().'?action=bookmark-feed&token='.urlencode($feed_token))
->withSiteUrl(Helper\get_current_base_url())
->withDate(new DateTime())
;
foreach ($bookmarks as $bookmark) {
$article = Model\Item\get($bookmark['id']);
$articleDate = new DateTime();
$articleDate->setTimestamp($article['updated']);
$writer->items[] = array(
'id' => $article['id'],
'title' => $article['title'],
'updated' => $article['updated'],
'url' => $article['url'],
'content' => $article['content'],
);
$feedBuilder
->withItem(AtomItemBuilder::create($feedBuilder)
->withId($article['id'])
->withTitle($article['title'])
->withUrl($article['url'])
->withUpdatedDate($articleDate)
->withPublishedDate($articleDate)
->withContent($article['content'])
);
}
Response\xml($writer->execute());
Response\xml($feedBuilder->build());
});

View File

@ -2,7 +2,6 @@
// Called before each action
Router\before(function($action) {
Session\open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH, 0);
// Select the requested database either from post param database or from the
@ -58,13 +57,11 @@ Router\before(function($action) {
// Show help
Router\get_action('show-help', function() {
Response\html(Template\load('show_help'));
});
// Show the menu for the mobile view
Router\get_action('more', function() {
Response\html(Template\layout('show_more', array('menu' => 'more')));
});

View File

@ -4,9 +4,7 @@ use PicoDb\Database;
// Display a form to add a new database
Router\get_action('new-db', function() {
if (ENABLE_MULTIPLE_DB) {
Response\html(Template\layout('new_db', array(
'errors' => array(),
'values' => array(
@ -23,9 +21,7 @@ Router\get_action('new-db', function() {
// Create a new database
Router\post_action('new-db', function() {
if (ENABLE_MULTIPLE_DB) {
$values = Request\values();
Model\Config\check_csrf_values($values);
list($valid, $errors) = Model\Database\validate($values);
@ -56,7 +52,6 @@ Router\post_action('new-db', function() {
// Confirmation box before auto-update
Router\get_action('confirm-auto-update', function() {
Response\html(Template\layout('confirm_auto_update', array(
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config',
@ -66,13 +61,10 @@ Router\get_action('confirm-auto-update', function() {
// Auto-update
Router\get_action('auto-update', function() {
if (ENABLE_AUTO_UPDATE) {
if (Model\AutoUpdate\execute(Model\Config\get('auto_update_url'))) {
Session\flash(t('Miniflux is updated!'));
}
else {
} else {
Session\flash_error(t('Unable to update Miniflux, check the console for errors.'));
}
}
@ -82,7 +74,6 @@ Router\get_action('auto-update', function() {
// Re-generate tokens
Router\get_action('generate-tokens', function() {
if (Model\Config\check_csrf(Request\param('csrf'))) {
Model\Config\new_tokens();
}
@ -92,7 +83,6 @@ Router\get_action('generate-tokens', function() {
// Optimize the database manually
Router\get_action('optimize-db', function() {
if (Model\Config\check_csrf(Request\param('csrf'))) {
Database::getInstance('db')->getConnection()->exec('VACUUM');
}
@ -102,7 +92,6 @@ Router\get_action('optimize-db', function() {
// Download the compressed database
Router\get_action('download-db', function() {
if (Model\Config\check_csrf(Request\param('csrf'))) {
Response\force_download('db.sqlite.gz');
Response\binary(gzencode(file_get_contents(Model\Database\get_path())));
@ -111,7 +100,6 @@ Router\get_action('download-db', function() {
// Display preferences page
Router\get_action('config', function() {
Response\html(Template\layout('config', array(
'errors' => array(),
'values' => Model\Config\get_all() + array('csrf' => Model\Config\generate_csrf()),
@ -133,7 +121,6 @@ Router\get_action('config', function() {
// Update preferences
Router\post_action('config', function() {
$values = Request\values() + array('nocontent' => 0, 'image_proxy' => 0, 'favicons' => 0, 'debug_mode' => 0, 'original_marks_read' => 0);
Model\Config\check_csrf_values($values);
list($valid, $errors) = Model\Config\validate_modification($values);
@ -188,7 +175,6 @@ Router\post_action('get-config', function() {
// Display help page
Router\get_action('help', function() {
Response\html(Template\layout('help', array(
'config' => Model\Config\get_all(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
@ -199,7 +185,6 @@ Router\get_action('help', function() {
// Display about page
Router\get_action('about', function() {
Response\html(Template\layout('about', array(
'csrf' => Model\Config\generate_csrf(),
'config' => Model\Config\get_all(),
@ -212,7 +197,6 @@ Router\get_action('about', function() {
// Display database page
Router\get_action('database', function() {
Response\html(Template\layout('database', array(
'csrf' => Model\Config\generate_csrf(),
'config' => Model\Config\get_all(),
@ -225,7 +209,6 @@ Router\get_action('database', function() {
// Display API page
Router\get_action('api', function() {
Response\html(Template\layout('api', array(
'config' => Model\Config\get_all(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
@ -236,7 +219,6 @@ Router\get_action('api', function() {
// Display bookmark services page
Router\get_action('services', function() {
Response\html(Template\layout('services', array(
'errors' => array(),
'values' => Model\Config\get_all() + array('csrf' => Model\Config\generate_csrf()),
@ -247,7 +229,6 @@ Router\get_action('services', function() {
// Update bookmark services
Router\post_action('services', function() {
$values = Request\values() + array('pinboard_enabled' => 0, 'instapaper_enabled' => 0);
Model\Config\check_csrf_values($values);

View File

@ -2,14 +2,12 @@
// Flush console messages
Router\get_action('flush-console', function() {
@unlink(DEBUG_FILENAME);
Response\redirect('?action=console');
});
// Display console
Router\get_action('console', function() {
Response\html(Template\layout('console', array(
'content' => @file_get_contents(DEBUG_FILENAME),
'nb_unread_items' => Model\Item\count_by_status('unread'),

View File

@ -1,8 +1,9 @@
<?php
use PicoFeed\Parser\MalformedXmlException;
// Refresh all feeds, used when Javascript is disabled
Router\get_action('refresh-all', function() {
Model\Feed\refresh_all();
Session\flash(t('Your subscriptions are updated'));
Response\redirect('?action=unread');
@ -10,7 +11,6 @@ Router\get_action('refresh-all', function() {
// Edit feed form
Router\get_action('edit-feed', function() {
$id = Request\int_param('feed_id');
$values = Model\Feed\get($id);
@ -30,7 +30,6 @@ Router\get_action('edit-feed', function() {
// Submit edit feed form
Router\post_action('edit-feed', function() {
$values = Request\values();
$values += array(
'enabled' => 0,
@ -65,7 +64,6 @@ Router\post_action('edit-feed', function() {
// Confirmation box to remove a feed
Router\get_action('confirm-remove-feed', function() {
$id = Request\int_param('feed_id');
Response\html(Template\layout('confirm_remove_feed', array(
@ -78,7 +76,6 @@ Router\get_action('confirm-remove-feed', function() {
// Remove a feed
Router\get_action('remove-feed', function() {
$id = Request\int_param('feed_id');
if ($id && Model\Feed\remove($id)) {
@ -93,7 +90,6 @@ Router\get_action('remove-feed', function() {
// Refresh one feed and redirect to unread items
Router\get_action('refresh-feed', function() {
$feed_id = Request\int_param('feed_id');
$redirect = Request\param('redirect', 'unread');
@ -103,7 +99,6 @@ Router\get_action('refresh-feed', function() {
// Ajax call to refresh one feed
Router\post_action('refresh-feed', function() {
$feed_id = Request\int_param('feed_id', 0);
Response\json(array(
@ -136,7 +131,6 @@ Router\get_action('feeds', function() {
// Display form to add one feed
Router\get_action('add', function() {
$values = array(
'download_content' => 0,
'rtl' => 0,
@ -157,13 +151,11 @@ Router\get_action('add', function() {
// Add a feed with the form or directly from the url, it can be used by a bookmarklet by example
Router\action('subscribe', function() {
if (Request\is_post()) {
$values = Request\values();
Model\Config\check_csrf_values($values);
$url = isset($values['url']) ? $values['url'] : '';
}
else {
} else {
$values = array();
$url = Request\param('url');
$token = Request\param('token');
@ -247,14 +239,12 @@ Router\action('subscribe', function() {
// OPML export
Router\get_action('export', function() {
Response\force_download('feeds.opml');
Response\xml(Model\Feed\export_opml());
});
// OPML import form
Router\get_action('import', function() {
Response\html(Template\layout('import', array(
'errors' => array(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
@ -265,15 +255,12 @@ Router\get_action('import', function() {
// OPML importation
Router\post_action('import', function() {
if (Model\Feed\import_opml(Request\file_content('file'))) {
try {
Model\Feed\import_opml(Request\file_content('file'));
Session\flash(t('Your feeds have been imported.'));
Response\redirect('?action=feeds');
}
else {
Session\flash_error(t('Unable to import your OPML file.'));
} catch (MalformedXmlException $e) {
Session\flash_error(t('Unable to import your OPML file.').' ('.$e->getMessage().')');
Response\redirect('?action=import');
}
});

View File

@ -2,10 +2,10 @@
// Display history page
Router\get_action('history', function() {
$offset = Request\int_param('offset', 0);
$group_id = Request\int_param('group_id', null);
$feed_ids = array();
if (! is_null($group_id)) {
$feed_ids = Model\Group\get_feeds_by_group($group_id);
}
@ -44,6 +44,7 @@ Router\get_action('history', function() {
// Confirmation box to flush history
Router\get_action('confirm-flush-history', function() {
$group_id = Request\int_param('group_id', null);
Response\html(Template\layout('confirm_flush_items', array(
'group_id' => $group_id,
'nb_unread_items' => Model\Item\count_by_status('unread'),
@ -55,10 +56,12 @@ Router\get_action('confirm-flush-history', function() {
// Flush history
Router\get_action('flush-history', function() {
$group_id = Request\int_param('group_id', null);
if (!is_null($group_id)) {
Model\Item\mark_group_as_removed($group_id);
} else {
Model\Item\mark_all_as_removed();
}
Response\redirect('?action=history');
});

View File

@ -2,7 +2,6 @@
// Display unread items
Router\get_action('unread', function() {
Model\Item\autoflush_read();
Model\Item\autoflush_unread();
@ -54,7 +53,6 @@ Router\get_action('unread', function() {
// Show item
Router\get_action('show', function() {
$id = Request\param('id');
$menu = Request\param('menu');
$item = Model\Item\get($id);
@ -101,7 +99,6 @@ Router\get_action('show', function() {
// Display feed items page
Router\get_action('feed-items', function() {
$feed_id = Request\int_param('feed_id', 0);
$offset = Request\int_param('offset', 0);
$nb_items = Model\Item\count_by_feed($feed_id);
@ -143,28 +140,24 @@ Router\post_action('download-item', function() {
// Ajax call to mark item read
Router\post_action('mark-item-read', function() {
Model\Item\set_read(Request\param('id'));
Response\json(array('Ok'));
});
// Ajax call to mark item as removed
Router\post_action('mark-item-removed', function() {
Model\Item\set_removed(Request\param('id'));
Response\json(array('Ok'));
});
// Ajax call to mark item unread
Router\post_action('mark-item-unread', function() {
Model\Item\set_unread(Request\param('id'));
Response\json(array('Ok'));
});
// Mark unread items as read
Router\get_action('mark-all-read', function() {
$group_id = Request\int_param('group_id', null);
if (!is_null($group_id)) {
@ -179,11 +172,9 @@ Router\get_action('mark-all-read', function() {
// Mark all unread items as read for a specific feed
Router\get_action('mark-feed-as-read', function() {
$feed_id = Request\int_param('feed_id');
Model\Item\mark_feed_as_read($feed_id);
Response\redirect('?action=feed-items&feed_id='.$feed_id);
});
@ -192,7 +183,6 @@ Router\get_action('mark-feed-as-read', function() {
// that where marked read from the frontend, since the number of unread items
// on page 2+ is unknown.
Router\post_action('mark-feed-as-read', function() {
Model\Item\mark_feed_as_read(Request\int_param('feed_id'));
$nb_items = Model\Item\count_by_status('unread');
@ -201,41 +191,35 @@ Router\post_action('mark-feed-as-read', function() {
// Mark item as read and redirect to the listing page
Router\get_action('mark-item-read', function() {
$id = Request\param('id');
$redirect = Request\param('redirect', 'unread');
$offset = Request\int_param('offset', 0);
$feed_id = Request\int_param('feed_id', 0);
Model\Item\set_read($id);
Response\Redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id);
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id);
});
// Mark item as unread and redirect to the listing page
Router\get_action('mark-item-unread', function() {
$id = Request\param('id');
$redirect = Request\param('redirect', 'history');
$offset = Request\int_param('offset', 0);
$feed_id = Request\int_param('feed_id', 0);
Model\Item\set_unread($id);
Response\Redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id);
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id);
});
// Mark item as removed and redirect to the listing page
Router\get_action('mark-item-removed', function() {
$id = Request\param('id');
$redirect = Request\param('redirect', 'history');
$offset = Request\int_param('offset', 0);
$feed_id = Request\int_param('feed_id', 0);
Model\Item\set_removed($id);
Response\Redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id);
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id);
});
Router\post_action('latest-feeds-items', function() {

View File

@ -2,14 +2,12 @@
// Logout and destroy session
Router\get_action('logout', function() {
Model\User\logout();
Response\redirect('?action=login');
});
// Display form login
Router\get_action('login', function() {
if (Model\User\is_loggedin()) {
Response\redirect('?action=unread');
}
@ -26,7 +24,6 @@ Router\get_action('login', function() {
// Check credentials and redirect to unread items
Router\post_action('login', function() {
$values = Request\values();
Model\Config\check_csrf_values($values);
list($valid, $errors) = Model\User\validate_login($values);

View File

@ -2,6 +2,10 @@
namespace Model\Feed;
use PicoFeed\Serialization\Subscription;
use PicoFeed\Serialization\SubscriptionList;
use PicoFeed\Serialization\SubscriptionListBuilder;
use PicoFeed\Serialization\SubscriptionListParser;
use UnexpectedValueException;
use Model\Config;
use Model\Item;
@ -11,11 +15,8 @@ use Helper;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use PicoDb\Database;
use PicoFeed\Serialization\Export;
use PicoFeed\Serialization\Import;
use PicoFeed\Reader\Reader;
use PicoFeed\PicoFeedException;
use PicoFeed\Client\InvalidUrlException;
const LIMIT_ALL = -1;
@ -53,43 +54,61 @@ function update(array $values)
// Export all feeds
function export_opml()
{
$opml = new Export(get_all());
return $opml->execute();
$feeds = get_all();
$subscriptionList = SubscriptionList::create()->setTitle(t('Subscriptions'));
foreach ($feeds as $feed) {
$groups = Group\get_feed_groups($feed['id']);
$category = '';
if (!empty($groups)) {
$category = $groups[0]['title'];
}
$subscriptionList->addSubscription(Subscription::create()
->setTitle($feed['title'])
->setSiteUrl($feed['site_url'])
->setFeedUrl($feed['feed_url'])
->setCategory($category)
);
}
return SubscriptionListBuilder::create($subscriptionList)->build();
}
// Import OPML file
function import_opml($content)
{
$import = new Import($content);
$feeds = $import->execute();
$subscriptionList = SubscriptionListParser::create($content)->parse();
if ($feeds) {
$db = Database::getInstance('db');
$db->startTransaction();
$db = Database::getInstance('db');
$db->startTransaction();
foreach ($subscriptionList->subscriptions as $subscription) {
if (! $db->table('feeds')->eq('feed_url', $subscription->getFeedUrl())->exists()) {
$db->table('feeds')->insert(array(
'title' => $subscription->getTitle(),
'site_url' => $subscription->getSiteUrl(),
'feed_url' => $subscription->getFeedUrl(),
));
foreach ($feeds as $feed) {
if ($subscription->getCategory() !== '') {
$feed_id = $db->getLastId();
$group_id = Group\get_group_id($subscription->getCategory());
if (! $db->table('feeds')->eq('feed_url', $feed->feed_url)->count()) {
if (empty($group_id)) {
$group_id = Group\create($subscription->getCategory());
}
$db->table('feeds')->save(array(
'title' => $feed->title,
'site_url' => $feed->site_url,
'feed_url' => $feed->feed_url
));
Group\add($feed_id, array($group_id));
}
}
$db->closeTransaction();
Config\write_debug();
return true;
}
$db->closeTransaction();
Config\write_debug();
return false;
return true;
}
// Add a new feed from an URL
@ -339,7 +358,7 @@ function update_cache($feed_id, $last_modified, $etag)
function remove($feed_id)
{
Group\remove_all($feed_id);
// Items are removed by a sql constraint
$result = Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->remove();
Favicon\purge_favicons();

View File

@ -80,6 +80,16 @@ function get_feed_group_ids($feed_id)
->findAllByColumn('id');
}
function get_feed_groups($feed_id)
{
return Database::getInstance('db')
->table('groups')
->columns('groups.id', 'groups.title')
->join('feeds_groups', 'group_id', 'id')
->eq('feed_id', $feed_id)
->findAll();
}
/**
* Get the id of a group
*
@ -143,7 +153,7 @@ function create($title)
* @param array $group_ids array of group ids
* @return boolean true on success, false on error
*/
function add($feed_id, $group_ids)
function add($feed_id, array $group_ids)
{
foreach ($group_ids as $group_id){
$data = array('feed_id' => $feed_id, 'group_id' => $group_id);
@ -167,7 +177,7 @@ function add($feed_id, $group_ids)
* @param array $group_ids array of group ids
* @return boolean true on success, false on error
*/
function remove($feed_id, $group_ids)
function remove($feed_id, array $group_ids)
{
$result = Database::getInstance('db')
->table('feeds_groups')
@ -212,7 +222,7 @@ function purge_groups()
$groups = Database::getInstance('db')
->table('groups')
->join('feeds_groups', 'group_id', 'id')
->isnull('feed_id')
->isNull('feed_id')
->findAllByColumn('id');
if (! empty($groups)) {
@ -231,7 +241,7 @@ function purge_groups()
* @param string $create_group group to create and assign to feed
* @return boolean
*/
function update_feed_groups($feed_id, $group_ids, $create_group = '')
function update_feed_groups($feed_id, array $group_ids, $create_group = '')
{
if ($create_group !== '') {
$id = create($create_group);

View File

@ -4,6 +4,7 @@ namespace Model\Proxy;
use Helper;
use Model\Config;
use PicoFeed\Client\ClientException;
use PicoFeed\Config\Config as PicoFeedConfig;
use PicoFeed\Filter\Filter;
use PicoFeed\Client\Client;
@ -51,7 +52,6 @@ function rewrite_html($html, $website, $proxy_images, $cloak_referrer)
function download($url)
{
try {
if ((bool) Config\get('debug_mode')) {
Logger::enable();
}
@ -61,7 +61,7 @@ function download($url)
$client->enablePassthroughMode();
$client->execute($url);
}
catch (\PicoFeed\Client\ClientException $e) {}
catch (ClientException $e) {}
Config\write_debug();
}

2
vendor/autoload.php vendored
View File

@ -4,4 +4,4 @@
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInit00bf4499632e34ac01e60c409b852c25::getLoader();
return ComposerAutoloaderInitfd7e8d436e1dc450edc3153ac8bc31b4::getLoader();

View File

@ -78,11 +78,19 @@ return array(
'PicoFeed\\Scraper\\RuleLoader' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Scraper/RuleLoader.php',
'PicoFeed\\Scraper\\RuleParser' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Scraper/RuleParser.php',
'PicoFeed\\Scraper\\Scraper' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Scraper/Scraper.php',
'PicoFeed\\Serialization\\Export' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Serialization/Export.php',
'PicoFeed\\Serialization\\Import' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Serialization/Import.php',
'PicoFeed\\Syndication\\Atom' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Syndication/Atom.php',
'PicoFeed\\Syndication\\Rss20' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20.php',
'PicoFeed\\Syndication\\Writer' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Syndication/Writer.php',
'PicoFeed\\Serialization\\Subscription' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Serialization/Subscription.php',
'PicoFeed\\Serialization\\SubscriptionList' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionList.php',
'PicoFeed\\Serialization\\SubscriptionListBuilder' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionListBuilder.php',
'PicoFeed\\Serialization\\SubscriptionListParser' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionListParser.php',
'PicoFeed\\Serialization\\SubscriptionParser' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Serialization/SubscriptionParser.php',
'PicoFeed\\Syndication\\AtomFeedBuilder' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Syndication/AtomFeedBuilder.php',
'PicoFeed\\Syndication\\AtomHelper' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Syndication/AtomHelper.php',
'PicoFeed\\Syndication\\AtomItemBuilder' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Syndication/AtomItemBuilder.php',
'PicoFeed\\Syndication\\FeedBuilder' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Syndication/FeedBuilder.php',
'PicoFeed\\Syndication\\ItemBuilder' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Syndication/ItemBuilder.php',
'PicoFeed\\Syndication\\Rss20FeedBuilder' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20FeedBuilder.php',
'PicoFeed\\Syndication\\Rss20Helper' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20Helper.php',
'PicoFeed\\Syndication\\Rss20ItemBuilder' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Syndication/Rss20ItemBuilder.php',
'SimpleValidator\\Validator' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validator.php',
'SimpleValidator\\Validators\\Alpha' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Alpha.php',
'SimpleValidator\\Validators\\AlphaNumeric' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/AlphaNumeric.php',

View File

@ -2,7 +2,7 @@
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit00bf4499632e34ac01e60c409b852c25
class ComposerAutoloaderInitfd7e8d436e1dc450edc3153ac8bc31b4
{
private static $loader;
@ -19,9 +19,9 @@ class ComposerAutoloaderInit00bf4499632e34ac01e60c409b852c25
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit00bf4499632e34ac01e60c409b852c25', 'loadClassLoader'), true, true);
spl_autoload_register(array('ComposerAutoloaderInitfd7e8d436e1dc450edc3153ac8bc31b4', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit00bf4499632e34ac01e60c409b852c25', 'loadClassLoader'));
spl_autoload_unregister(array('ComposerAutoloaderInitfd7e8d436e1dc450edc3153ac8bc31b4', 'loadClassLoader'));
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
@ -42,14 +42,14 @@ class ComposerAutoloaderInit00bf4499632e34ac01e60c409b852c25
$includeFiles = require __DIR__ . '/autoload_files.php';
foreach ($includeFiles as $file) {
composerRequire00bf4499632e34ac01e60c409b852c25($file);
composerRequirefd7e8d436e1dc450edc3153ac8bc31b4($file);
}
return $loader;
}
}
function composerRequire00bf4499632e34ac01e60c409b852c25($file)
function composerRequirefd7e8d436e1dc450edc3153ac8bc31b4($file)
{
require $file;
}

View File

@ -163,17 +163,17 @@
},
{
"name": "fguillot/picofeed",
"version": "v0.1.21",
"version_normalized": "0.1.21.0",
"version": "v0.1.23",
"version_normalized": "0.1.23.0",
"source": {
"type": "git",
"url": "https://github.com/fguillot/picoFeed.git",
"reference": "2baff3240ef187c9f443656ab26b0b626aec5776"
"reference": "a7c3d420c239fe9ffc39b0d06b6e57db39ce3797"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fguillot/picoFeed/zipball/2baff3240ef187c9f443656ab26b0b626aec5776",
"reference": "2baff3240ef187c9f443656ab26b0b626aec5776",
"url": "https://api.github.com/repos/fguillot/picoFeed/zipball/a7c3d420c239fe9ffc39b0d06b6e57db39ce3797",
"reference": "a7c3d420c239fe9ffc39b0d06b6e57db39ce3797",
"shasum": ""
},
"require": {
@ -188,7 +188,7 @@
"suggest": {
"ext-curl": "PicoFeed will use cURL if present"
},
"time": "2016-03-31 00:39:41",
"time": "2016-04-17 22:31:55",
"bin": [
"picofeed"
],

View File

@ -354,15 +354,15 @@ class Curl extends Client
{
switch ($errno) {
case 78: // CURLE_REMOTE_FILE_NOT_FOUND
throw new InvalidUrlException('Resource not found');
throw new InvalidUrlException('Resource not found', $errno);
case 6: // CURLE_COULDNT_RESOLVE_HOST
throw new InvalidUrlException('Unable to resolve hostname');
throw new InvalidUrlException('Unable to resolve hostname', $errno);
case 7: // CURLE_COULDNT_CONNECT
throw new InvalidUrlException('Unable to connect to the remote host');
throw new InvalidUrlException('Unable to connect to the remote host', $errno);
case 23: // CURLE_WRITE_ERROR
throw new MaxSizeException('Maximum response size exceeded');
throw new MaxSizeException('Maximum response size exceeded', $errno);
case 28: // CURLE_OPERATION_TIMEDOUT
throw new TimeoutException('Operation timeout');
throw new TimeoutException('Operation timeout', $errno);
case 35: // CURLE_SSL_CONNECT_ERROR
case 51: // CURLE_PEER_FAILED_VERIFICATION
case 58: // CURLE_SSL_CERTPROBLEM
@ -372,13 +372,15 @@ class Curl extends Client
case 66: // CURLE_SSL_ENGINE_INITFAILED
case 77: // CURLE_SSL_CACERT_BADFILE
case 83: // CURLE_SSL_ISSUER_ERROR
throw new InvalidCertificateException('Invalid SSL certificate');
$msg = 'Invalid SSL certificate caused by CURL error number ' .
$errno;
throw new InvalidCertificateException($msg, $errno);
case 47: // CURLE_TOO_MANY_REDIRECTS
throw new MaxRedirectException('Maximum number of redirections reached');
throw new MaxRedirectException('Maximum number of redirections reached', $errno);
case 63: // CURLE_FILESIZE_EXCEEDED
throw new MaxSizeException('Maximum response size exceeded');
throw new MaxSizeException('Maximum response size exceeded', $errno);
default:
throw new InvalidUrlException('Unable to fetch the URL');
throw new InvalidUrlException('Unable to fetch the URL', $errno);
}
}
}

View File

@ -41,7 +41,7 @@ class Atom extends Parser
*/
public function findFeedUrl(SimpleXMLElement $xml, Feed $feed)
{
$feed->feed_url = $this->getUrl($xml, 'self');
$feed->setFeedUrl($this->getUrl($xml, 'self'));
}
/**
@ -52,7 +52,7 @@ class Atom extends Parser
*/
public function findSiteUrl(SimpleXMLElement $xml, Feed $feed)
{
$feed->site_url = $this->getUrl($xml, 'alternate', true);
$feed->setSiteUrl($this->getUrl($xml, 'alternate', true));
}
/**
@ -66,7 +66,7 @@ class Atom extends Parser
$description = XmlParser::getXPathResult($xml, 'atom:subtitle', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'subtitle');
$feed->description = (string) current($description);
$feed->setDescription(XmlParser::getValue($description));
}
/**
@ -80,7 +80,7 @@ class Atom extends Parser
$logo = XmlParser::getXPathResult($xml, 'atom:logo', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'logo');
$feed->logo = (string) current($logo);
$feed->setLogo(XmlParser::getValue($logo));
}
/**
@ -94,7 +94,7 @@ class Atom extends Parser
$icon = XmlParser::getXPathResult($xml, 'atom:icon', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'icon');
$feed->icon = (string) current($icon);
$feed->setIcon(XmlParser::getValue($icon));
}
/**
@ -108,7 +108,7 @@ class Atom extends Parser
$title = XmlParser::getXPathResult($xml, 'atom:title', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'title');
$feed->title = Filter::stripWhiteSpace((string) current($title)) ?: $feed->getSiteUrl();
$feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl());
}
/**
@ -122,7 +122,7 @@ class Atom extends Parser
$language = XmlParser::getXPathResult($xml, '*[not(self::atom:entry)]/@xml:lang', $this->namespaces)
?: XmlParser::getXPathResult($xml, '@xml:lang');
$feed->language = (string) current($language);
$feed->setLanguage(XmlParser::getValue($language));
}
/**
@ -136,7 +136,7 @@ class Atom extends Parser
$id = XmlParser::getXPathResult($xml, 'atom:id', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'id');
$feed->id = (string) current($id);
$feed->setId(XmlParser::getValue($id));
}
/**
@ -150,7 +150,7 @@ class Atom extends Parser
$updated = XmlParser::getXPathResult($xml, 'atom:updated', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'updated');
$feed->date = $this->getDateParser()->getDateTime((string) current($updated));
$feed->setDate($this->getDateParser()->getDateTime(XmlParser::getValue($updated)));
}
/**
@ -172,11 +172,11 @@ class Atom extends Parser
$updated = !empty($updated) ? $this->getDateParser()->getDateTime((string) current($updated)) : null;
if ($published === null && $updated === null) {
$item->date = $feed->getDate(); // We use the feed date if there is no date for the item
$item->setDate($feed->getDate()); // We use the feed date if there is no date for the item
} elseif ($published !== null && $updated !== null) {
$item->date = max($published, $updated); // We use the most recent date between published and updated
$item->setDate(max($published, $updated)); // We use the most recent date between published and updated
} else {
$item->date = $updated ?: $published;
$item->setDate($updated ?: $published);
}
}
@ -191,7 +191,7 @@ class Atom extends Parser
$title = XmlParser::getXPathResult($entry, 'atom:title', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'title');
$item->title = Filter::stripWhiteSpace((string) current($title)) ?: $item->url;
$item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $item->getUrl());
}
/**
@ -208,7 +208,7 @@ class Atom extends Parser
?: XmlParser::getXPathResult($xml, 'atom:author/atom:name', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'author/name');
$item->author = (string) current($author);
$item->setAuthor(XmlParser::getValue($author));
}
/**
@ -219,7 +219,7 @@ class Atom extends Parser
*/
public function findItemContent(SimpleXMLElement $entry, Item $item)
{
$item->content = $this->getContent($entry);
$item->setContent($this->getContent($entry));
}
/**
@ -230,7 +230,7 @@ class Atom extends Parser
*/
public function findItemUrl(SimpleXMLElement $entry, Item $item)
{
$item->url = $this->getUrl($entry, 'alternate', true);
$item->setUrl($this->getUrl($entry, 'alternate', true));
}
/**
@ -246,11 +246,11 @@ class Atom extends Parser
?: XmlParser::getXPathResult($entry, 'id');
if (!empty($id)) {
$item->id = $this->generateId((string) current($id));
$item->setId($this->generateId(XmlParser::getValue($id)));
} else {
$item->id = $this->generateId(
$item->setId($this->generateId(
$item->getTitle(), $item->getUrl(), $item->getContent()
);
));
}
}
@ -266,8 +266,8 @@ class Atom extends Parser
$enclosure = $this->findLink($entry, 'enclosure');
if ($enclosure) {
$item->enclosure_url = Url::resolve((string) $enclosure['href'], $feed->getSiteUrl());
$item->enclosure_type = (string) $enclosure['type'];
$item->setEnclosureUrl(Url::resolve((string) $enclosure['href'], $feed->getSiteUrl()));
$item->setEnclosureType((string) $enclosure['type']);
}
}
@ -281,8 +281,7 @@ class Atom extends Parser
public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$language = XmlParser::getXPathResult($entry, './/@xml:lang');
$item->language = (string) current($language) ?: $feed->language;
$item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage());
}
/**
@ -303,7 +302,6 @@ class Atom extends Parser
if ($fallback) {
$link = $this->findLink($xml, '');
return $link ? (string) $link['href'] : '';
}
@ -329,7 +327,7 @@ class Atom extends Parser
}
}
return;
return null;
}
/**

View File

@ -42,14 +42,14 @@ class Feed
*
* @var string
*/
public $feed_url = '';
public $feedUrl = '';
/**
* Site url.
*
* @var string
*/
public $site_url = '';
public $siteUrl = '';
/**
* Feed date.
@ -86,7 +86,7 @@ class Feed
{
$output = '';
foreach (array('id', 'title', 'feed_url', 'site_url', 'language', 'description', 'logo') as $property) {
foreach (array('id', 'title', 'feedUrl', 'siteUrl', 'language', 'description', 'logo') as $property) {
$output .= 'Feed::'.$property.' = '.$this->$property.PHP_EOL;
}
@ -139,7 +139,7 @@ class Feed
*/
public function getFeedUrl()
{
return $this->feed_url;
return $this->feedUrl;
}
/**
@ -147,7 +147,7 @@ class Feed
*/
public function getSiteUrl()
{
return $this->site_url;
return $this->siteUrl;
}
/**
@ -191,4 +191,124 @@ class Feed
{
return Parser::isLanguageRTL($this->language);
}
/**
* Set feed items.
*
* @param Item[] $items
* @return Feed
*/
public function setItems(array $items)
{
$this->items = $items;
return $this;
}
/**
* Set feed id.
*
* @param string $id
* @return Feed
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Set feed title.
*
* @param string $title
* @return Feed
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Set feed description.
*
* @param string $description
* @return Feed
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Set feed url.
*
* @param string $feedUrl
* @return Feed
*/
public function setFeedUrl($feedUrl)
{
$this->feedUrl = $feedUrl;
return $this;
}
/**
* Set feed website url.
*
* @param string $siteUrl
* @return Feed
*/
public function setSiteUrl($siteUrl)
{
$this->siteUrl = $siteUrl;
return $this;
}
/**
* Set feed date.
*
* @param \DateTime $date
* @return Feed
*/
public function setDate($date)
{
$this->date = $date;
return $this;
}
/**
* Set feed language.
*
* @param string $language
* @return Feed
*/
public function setLanguage($language)
{
$this->language = $language;
return $this;
}
/**
* Set feed logo.
*
* @param string $logo
* @return Feed
*/
public function setLogo($logo)
{
$this->logo = $logo;
return $this;
}
/**
* Set feed icon.
*
* @param string $icon
* @return Feed
*/
public function setIcon($icon)
{
$this->icon = $icon;
return $this;
}
}

View File

@ -12,7 +12,7 @@ class Item
/**
* List of known RTL languages.
*
* @var public
* @var string[]
*/
public $rtl = array(
'ar', // Arabic (ar-**)
@ -72,14 +72,14 @@ class Item
*
* @var string
*/
public $enclosure_url = '';
public $enclosureUrl = '';
/**
* Item enclusure type.
*
* @var string
*/
public $enclosure_type = '';
public $enclosureType = '';
/**
* Item language.
@ -140,12 +140,14 @@ class Item
/**
* Return item information.
*
* @return string
*/
public function __toString()
{
$output = '';
foreach (array('id', 'title', 'url', 'language', 'author', 'enclosure_url', 'enclosure_type') as $property) {
foreach (array('id', 'title', 'url', 'language', 'author', 'enclosureUrl', 'enclosureType') as $property) {
$output .= 'Item::'.$property.' = '.$this->$property.PHP_EOL;
}
@ -158,6 +160,8 @@ class Item
/**
* Get title.
*
* @return string
*/
public function getTitle()
{
@ -190,6 +194,8 @@ class Item
/**
* Get id.
*
* @return string
*/
public function getId()
{
@ -198,6 +204,8 @@ class Item
/**
* Get date.
*
* @return \DateTime
*/
public function getDate()
{
@ -206,6 +214,8 @@ class Item
/**
* Get content.
*
* @return string
*/
public function getContent()
{
@ -227,22 +237,28 @@ class Item
/**
* Get enclosure url.
*
* @return string
*/
public function getEnclosureUrl()
{
return $this->enclosure_url;
return $this->enclosureUrl;
}
/**
* Get enclosure type.
*
* @return string
*/
public function getEnclosureType()
{
return $this->enclosure_type;
return $this->enclosureType;
}
/**
* Get language.
*
* @return string
*/
public function getLanguage()
{
@ -251,6 +267,8 @@ class Item
/**
* Get author.
*
* @return string
*/
public function getAuthor()
{
@ -266,4 +284,132 @@ class Item
{
return Parser::isLanguageRTL($this->language);
}
/**
* Set item id.
*
* @param string $id
* @return Item
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Set item title.
*
* @param string $title
* @return Item
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Set author.
*
* @param string $author
* @return Item
*/
public function setAuthor($author)
{
$this->author = $author;
return $this;
}
/**
* Set item date.
*
* @param \DateTime $date
* @return Item
*/
public function setDate($date)
{
$this->date = $date;
return $this;
}
/**
* Set enclosure url.
*
* @param string $enclosureUrl
* @return Item
*/
public function setEnclosureUrl($enclosureUrl)
{
$this->enclosureUrl = $enclosureUrl;
return $this;
}
/**
* Set enclosure type.
*
* @param string $enclosureType
* @return Item
*/
public function setEnclosureType($enclosureType)
{
$this->enclosureType = $enclosureType;
return $this;
}
/**
* Set item language.
*
* @param string $language
* @return Item
*/
public function setLanguage($language)
{
$this->language = $language;
return $this;
}
/**
* Set raw XML.
*
* @param \SimpleXMLElement $xml
* @return Item
*/
public function setXml($xml)
{
$this->xml = $xml;
return $this;
}
/**
* Get raw XML.
*
* @return \SimpleXMLElement
*/
public function getXml()
{
return $this->xml;
}
/**
* Set XML namespaces.
*
* @param array $namespaces
* @return Item
*/
public function setNamespaces($namespaces)
{
$this->namespaces = $namespaces;
return $this;
}
/**
* Get XML namespaces.
*
* @return array
*/
public function getNamespaces()
{
return $this->namespaces;
}
}

View File

@ -180,9 +180,9 @@ abstract class Parser
public function checkFeedUrl(Feed $feed)
{
if ($feed->getFeedUrl() === '') {
$feed->feed_url = $this->fallback_url;
$feed->feedUrl = $this->fallback_url;
} else {
$feed->feed_url = Url::resolve($feed->getFeedUrl(), $this->fallback_url);
$feed->feedUrl = Url::resolve($feed->getFeedUrl(), $this->fallback_url);
}
}
@ -194,9 +194,9 @@ abstract class Parser
public function checkSiteUrl(Feed $feed)
{
if ($feed->getSiteUrl() === '') {
$feed->site_url = Url::base($feed->getFeedUrl());
$feed->siteUrl = Url::base($feed->getFeedUrl());
} else {
$feed->site_url = Url::resolve($feed->getSiteUrl(), $this->fallback_url);
$feed->siteUrl = Url::resolve($feed->getSiteUrl(), $this->fallback_url);
}
}

View File

@ -32,7 +32,8 @@ class Rss10 extends Parser
public function getItemsTree(SimpleXMLElement $xml)
{
return XmlParser::getXPathResult($xml, 'rss:item', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'item');
?: XmlParser::getXPathResult($xml, 'item')
?: $xml->item;
}
/**
@ -43,7 +44,7 @@ class Rss10 extends Parser
*/
public function findFeedUrl(SimpleXMLElement $xml, Feed $feed)
{
$feed->feed_url = '';
$feed->setFeedUrl('');
}
/**
@ -54,10 +55,11 @@ class Rss10 extends Parser
*/
public function findSiteUrl(SimpleXMLElement $xml, Feed $feed)
{
$site_url = XmlParser::getXPathResult($xml, 'rss:channel/rss:link', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/link');
$value = XmlParser::getXPathResult($xml, 'rss:channel/rss:link', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/link')
?: $xml->channel->link;
$feed->site_url = (string) current($site_url);
$feed->setSiteUrl(XmlParser::getValue($value));
}
/**
@ -69,9 +71,10 @@ class Rss10 extends Parser
public function findFeedDescription(SimpleXMLElement $xml, Feed $feed)
{
$description = XmlParser::getXPathResult($xml, 'rss:channel/rss:description', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/description');
?: XmlParser::getXPathResult($xml, 'channel/description')
?: $xml->channel->description;
$feed->description = (string) current($description);
$feed->setDescription(XmlParser::getValue($description));
}
/**
@ -83,9 +86,9 @@ class Rss10 extends Parser
public function findFeedLogo(SimpleXMLElement $xml, Feed $feed)
{
$logo = XmlParser::getXPathResult($xml, 'rss:image/rss:url', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'image/url');
?: XmlParser::getXPathResult($xml, 'image/url');
$feed->logo = (string) current($logo);
$feed->setLogo(XmlParser::getValue($logo));
}
/**
@ -96,7 +99,7 @@ class Rss10 extends Parser
*/
public function findFeedIcon(SimpleXMLElement $xml, Feed $feed)
{
$feed->icon = '';
$feed->setIcon('');
}
/**
@ -108,9 +111,10 @@ class Rss10 extends Parser
public function findFeedTitle(SimpleXMLElement $xml, Feed $feed)
{
$title = XmlParser::getXPathResult($xml, 'rss:channel/rss:title', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/title');
?: XmlParser::getXPathResult($xml, 'channel/title')
?: $xml->channel->title;
$feed->title = Filter::stripWhiteSpace((string) current($title)) ?: $feed->getSiteUrl();
$feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl());
}
/**
@ -124,7 +128,7 @@ class Rss10 extends Parser
$language = XmlParser::getXPathResult($xml, 'rss:channel/dc:language', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/dc:language', $this->namespaces);
$feed->language = (string) current($language);
$feed->setLanguage(XmlParser::getValue($language));
}
/**
@ -135,7 +139,7 @@ class Rss10 extends Parser
*/
public function findFeedId(SimpleXMLElement $xml, Feed $feed)
{
$feed->id = $feed->getFeedUrl() ?: $feed->getSiteUrl();
$feed->setId($feed->getFeedUrl() ?: $feed->getSiteUrl());
}
/**
@ -149,7 +153,7 @@ class Rss10 extends Parser
$date = XmlParser::getXPathResult($xml, 'rss:channel/dc:date', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/dc:date', $this->namespaces);
$feed->date = $this->getDateParser()->getDateTime((string) current($date));
$feed->setDate($this->getDateParser()->getDateTime(XmlParser::getValue($date)));
}
/**
@ -163,7 +167,7 @@ class Rss10 extends Parser
{
$date = XmlParser::getXPathResult($entry, 'dc:date', $this->namespaces);
$item->date = empty($date) ? $feed->getDate() : $this->getDateParser()->getDateTime((string) current($date));
$item->setDate(empty($date) ? $feed->getDate() : $this->getDateParser()->getDateTime(XmlParser::getValue($date)));
}
/**
@ -175,9 +179,10 @@ class Rss10 extends Parser
public function findItemTitle(SimpleXMLElement $entry, Item $item)
{
$title = XmlParser::getXPathResult($entry, 'rss:title', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'title');
?: XmlParser::getXPathResult($entry, 'title')
?: $entry->title;
$item->title = Filter::stripWhiteSpace((string) current($title)) ?: $item->url;
$item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $item->getUrl());
}
/**
@ -193,7 +198,7 @@ class Rss10 extends Parser
?: XmlParser::getXPathResult($xml, 'rss:channel/dc:creator', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/dc:creator', $this->namespaces);
$item->author = (string) current($author);
$item->setAuthor(XmlParser::getValue($author));
}
/**
@ -206,12 +211,13 @@ class Rss10 extends Parser
{
$content = XmlParser::getXPathResult($entry, 'content:encoded', $this->namespaces);
if (trim((string) current($content)) === '') {
if (XmlParser::getValue($content) === '') {
$content = XmlParser::getXPathResult($entry, 'rss:description', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'description');
?: XmlParser::getXPathResult($entry, 'description')
?: $entry->description;
}
$item->content = (string) current($content);
$item->setContent(XmlParser::getValue($content));
}
/**
@ -223,10 +229,11 @@ class Rss10 extends Parser
public function findItemUrl(SimpleXMLElement $entry, Item $item)
{
$link = XmlParser::getXPathResult($entry, 'feedburner:origLink', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'rss:link', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'link');
?: XmlParser::getXPathResult($entry, 'rss:link', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'link')
?: $entry->link;
$item->url = trim((string) current($link));
$item->setUrl(XmlParser::getValue($link));
}
/**
@ -238,9 +245,9 @@ class Rss10 extends Parser
*/
public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$item->id = $this->generateId(
$item->setId($this->generateId(
$item->getTitle(), $item->getUrl(), $item->getContent()
);
));
}
/**
@ -265,6 +272,6 @@ class Rss10 extends Parser
{
$language = XmlParser::getXPathResult($entry, 'dc:language', $this->namespaces);
$item->language = (string) current($language) ?: $feed->language;
$item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage());
}
}

View File

@ -43,7 +43,7 @@ class Rss20 extends Parser
*/
public function findFeedUrl(SimpleXMLElement $xml, Feed $feed)
{
$feed->feed_url = '';
$feed->setFeedUrl('');
}
/**
@ -54,8 +54,8 @@ class Rss20 extends Parser
*/
public function findSiteUrl(SimpleXMLElement $xml, Feed $feed)
{
$site_url = XmlParser::getXPathResult($xml, 'channel/link');
$feed->site_url = (string) current($site_url);
$value = XmlParser::getXPathResult($xml, 'channel/link');
$feed->setSiteUrl(XmlParser::getValue($value));
}
/**
@ -66,8 +66,8 @@ class Rss20 extends Parser
*/
public function findFeedDescription(SimpleXMLElement $xml, Feed $feed)
{
$description = XmlParser::getXPathResult($xml, 'channel/description');
$feed->description = (string) current($description);
$value = XmlParser::getXPathResult($xml, 'channel/description');
$feed->setDescription(XmlParser::getValue($value));
}
/**
@ -78,8 +78,8 @@ class Rss20 extends Parser
*/
public function findFeedLogo(SimpleXMLElement $xml, Feed $feed)
{
$logo = XmlParser::getXPathResult($xml, 'channel/image/url');
$feed->logo = (string) current($logo);
$value = XmlParser::getXPathResult($xml, 'channel/image/url');
$feed->setLogo(XmlParser::getValue($value));
}
/**
@ -90,7 +90,7 @@ class Rss20 extends Parser
*/
public function findFeedIcon(SimpleXMLElement $xml, Feed $feed)
{
$feed->icon = '';
$feed->setIcon('');
}
/**
@ -102,7 +102,7 @@ class Rss20 extends Parser
public function findFeedTitle(SimpleXMLElement $xml, Feed $feed)
{
$title = XmlParser::getXPathResult($xml, 'channel/title');
$feed->title = Filter::stripWhiteSpace((string) current($title)) ?: $feed->getSiteUrl();
$feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl());
}
/**
@ -113,8 +113,8 @@ class Rss20 extends Parser
*/
public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed)
{
$language = XmlParser::getXPathResult($xml, 'channel/language');
$feed->language = (string) current($language);
$value = XmlParser::getXPathResult($xml, 'channel/language');
$feed->setLanguage(XmlParser::getValue($value));
}
/**
@ -125,7 +125,7 @@ class Rss20 extends Parser
*/
public function findFeedId(SimpleXMLElement $xml, Feed $feed)
{
$feed->id = $feed->getFeedUrl() ?: $feed->getSiteUrl();
$feed->setId($feed->getFeedUrl() ?: $feed->getSiteUrl());
}
/**
@ -139,15 +139,15 @@ class Rss20 extends Parser
$publish_date = XmlParser::getXPathResult($xml, 'channel/pubDate');
$update_date = XmlParser::getXPathResult($xml, 'channel/lastBuildDate');
$published = !empty($publish_date) ? $this->getDateParser()->getDateTime((string) current($publish_date)) : null;
$updated = !empty($update_date) ? $this->getDateParser()->getDateTime((string) current($update_date)) : null;
$published = !empty($publish_date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($publish_date)) : null;
$updated = !empty($update_date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($update_date)) : null;
if ($published === null && $updated === null) {
$feed->date = $this->getDateParser()->getCurrentDateTime(); // We use the current date if there is no date for the feed
$feed->setDate($this->getDateParser()->getCurrentDateTime()); // We use the current date if there is no date for the feed
} elseif ($published !== null && $updated !== null) {
$feed->date = max($published, $updated); // We use the most recent date between published and updated
$feed->setDate(max($published, $updated)); // We use the most recent date between published and updated
} else {
$feed->date = $updated ?: $published;
$feed->setDate($updated ?: $published);
}
}
@ -162,7 +162,7 @@ class Rss20 extends Parser
{
$date = XmlParser::getXPathResult($entry, 'pubDate');
$item->date = empty($date) ? $feed->getDate() : $this->getDateParser()->getDateTime((string) current($date));
$item->setDate(empty($date) ? $feed->getDate() : $this->getDateParser()->getDateTime(XmlParser::getValue($date)));
}
/**
@ -173,8 +173,8 @@ class Rss20 extends Parser
*/
public function findItemTitle(SimpleXMLElement $entry, Item $item)
{
$title = XmlParser::getXPathResult($entry, 'title');
$item->title = Filter::stripWhiteSpace((string) current($title)) ?: $item->url;
$value = XmlParser::getXPathResult($entry, 'title');
$item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($value)) ?: $item->getUrl());
}
/**
@ -186,12 +186,12 @@ class Rss20 extends Parser
*/
public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item)
{
$author = XmlParser::getXPathResult($entry, 'dc:creator', $this->namespaces)
$value = XmlParser::getXPathResult($entry, 'dc:creator', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'author')
?: XmlParser::getXPathResult($xml, 'channel/dc:creator', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/managingEditor');
$item->author = (string) current($author);
$item->setAuthor(XmlParser::getValue($value));
}
/**
@ -204,11 +204,11 @@ class Rss20 extends Parser
{
$content = XmlParser::getXPathResult($entry, 'content:encoded', $this->namespaces);
if (trim((string) current($content)) === '') {
if (XmlParser::getValue($content) === '') {
$content = XmlParser::getXPathResult($entry, 'description');
}
$item->content = (string) current($content);
$item->setContent(XmlParser::getValue($content));
}
/**
@ -224,13 +224,13 @@ class Rss20 extends Parser
?: XmlParser::getXPathResult($entry, 'atom:link/@href', $this->namespaces);
if (!empty($link)) {
$item->url = trim((string) current($link));
$item->setUrl(XmlParser::getValue($link));
} else {
$link = XmlParser::getXPathResult($entry, 'guid');
$link = trim((string) current($link));
$link = XmlParser::getValue($link);
if (filter_var($link, FILTER_VALIDATE_URL) !== false) {
$item->url = $link;
$item->setUrl($link);
}
}
}
@ -244,14 +244,14 @@ class Rss20 extends Parser
*/
public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$id = (string) current(XmlParser::getXPathResult($entry, 'guid'));
$id = XmlParser::getValue(XmlParser::getXPathResult($entry, 'guid'));
if ($id) {
$item->id = $this->generateId($id);
$item->setId($this->generateId($id));
} else {
$item->id = $this->generateId(
$item->setId($this->generateId(
$item->getTitle(), $item->getUrl(), $item->getContent()
);
));
}
}
@ -265,13 +265,12 @@ class Rss20 extends Parser
public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed)
{
if (isset($entry->enclosure)) {
$enclosure_url = XmlParser::getXPathResult($entry, 'feedburner:origEnclosureLink', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'enclosure/@url');
$type = XmlParser::getXPathResult($entry, 'enclosure/@type');
$url = XmlParser::getXPathResult($entry, 'feedburner:origEnclosureLink', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'enclosure/@url');
$enclosure_type = XmlParser::getXPathResult($entry, 'enclosure/@type');
$item->enclosure_url = Url::resolve((string) current($enclosure_url), $feed->getSiteUrl());
$item->enclosure_type = (string) current($enclosure_type);
$item->setEnclosureUrl(Url::resolve(XmlParser::getValue($url), $feed->getSiteUrl()));
$item->setEnclosureType(XmlParser::getValue($type));
}
}
@ -285,7 +284,6 @@ class Rss20 extends Parser
public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$language = XmlParser::getXPathResult($entry, 'dc:language', $this->namespaces);
$item->language = (string) current($language) ?: $feed->language;
$item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage());
}
}

View File

@ -4,7 +4,6 @@ namespace PicoFeed\Parser;
use DomDocument;
use SimpleXmlElement;
use Exception;
use ZendXml\Security;
@ -21,9 +20,7 @@ class XmlParser
* Get a SimpleXmlElement instance or return false.
*
* @static
*
* @param string $input XML content
*
* @return mixed
*/
public static function getSimpleXml($input)
@ -35,9 +32,7 @@ class XmlParser
* Get a DomDocument instance or return false.
*
* @static
*
* @param string $input XML content
*
* @return \DOMDocument
*/
public static function getDomDocument($input)
@ -59,6 +54,7 @@ class XmlParser
/**
* Small wrapper around ZendXml to turn their exceptions into picoFeed
* exceptions
*
* @param $input the xml to load
* @param $dom pass in a dom document or use null/omit if simpleXml should
* be used
@ -76,9 +72,7 @@ class XmlParser
* Load HTML document by using a DomDocument instance or return false on failure.
*
* @static
*
* @param string $input XML content
*
* @return \DOMDocument
*/
public static function getHtmlDocument($input)
@ -112,7 +106,6 @@ class XmlParser
public static function htmlToXml($html)
{
$dom = self::getHtmlDocument('<?xml version="1.0" encoding="UTF-8">'.$html);
return $dom->saveXML($dom->getElementsByTagName('body')->item(0));
}
@ -120,7 +113,6 @@ class XmlParser
* Get XML parser errors.
*
* @static
*
* @return string
*/
public static function getErrors()
@ -143,9 +135,7 @@ class XmlParser
* Get the encoding from a xml tag.
*
* @static
*
* @param string $data Input data
*
* @return string
*/
public static function getEncodingFromXmlTag($data)
@ -172,9 +162,7 @@ class XmlParser
* Get the charset from a meta tag.
*
* @static
*
* @param string $data Input data
*
* @return string
*/
public static function getEncodingFromMetaTag($data)
@ -193,7 +181,6 @@ class XmlParser
*
* @param string $query XPath query
* @param array $ns Prefix to namespace URI mapping
*
* @return string
*/
public static function replaceXPathPrefixWithNamespaceURI($query, array $ns)
@ -215,8 +202,7 @@ class XmlParser
* @param \SimpleXMLElement $xml XML element
* @param string $query XPath query
* @param array $ns Prefix to namespace URI mapping
*
* @return \SimpleXMLElement
* @return \SimpleXMLElement[]
*/
public static function getXPathResult(SimpleXMLElement $xml, $query, array $ns = array())
{
@ -226,4 +212,25 @@ class XmlParser
return $xml->xpath($query);
}
/**
* Get the first Xpath result or SimpleXMLElement value
*
* @static
* @access public
* @param mixed $value
* @return string
*/
public static function getValue($value)
{
$result = '';
if (is_array($value) && count($value) > 0) {
$result = (string) $value[0];
} elseif (is_a($value, 'SimpleXMLElement')) {
return $result = (string) $value;
}
return trim($result);
}
}

View File

@ -2,7 +2,7 @@
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://www.sciencemag.org/news/2016/01/could-bright-foamy-wak$
'test_url' => 'http://www.sciencemag.org/news/2016/01/could-bright-foamy-wak$',
'body' => array(
'//div[@class="row--hero"]',
'//article[contains(@class,"primary")]',

View File

@ -1,122 +0,0 @@
<?php
namespace PicoFeed\Serialization;
use SimpleXMLElement;
/**
* OPML export class.
*
* @author Frederic Guillot
*/
class Export
{
/**
* List of feeds to exports.
*
* @var array
*/
private $content = array();
/**
* List of required properties for each feed.
*
* @var array
*/
private $required_fields = array(
'title',
'site_url',
'feed_url',
);
/**
* Constructor.
*
* @param array $content List of feeds
*/
public function __construct(array $content)
{
$this->content = $content;
}
/**
* Get the OPML document.
*
* @return string
*/
public function execute()
{
$xml = new SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><opml/>');
$head = $xml->addChild('head');
$head->addChild('title', 'OPML Export');
$body = $xml->addChild('body');
foreach ($this->content as $category => $values) {
if (is_string($category)) {
$this->createCategory($body, $category, $values);
} else {
$this->createEntry($body, $values);
}
}
return $xml->asXML();
}
/**
* Create a feed entry.
*
* @param SimpleXMLElement $parent Parent Element
* @param array $feed Feed properties
*/
public function createEntry(SimpleXMLElement $parent, array $feed)
{
$valid = true;
foreach ($this->required_fields as $field) {
if (!isset($feed[$field])) {
$valid = false;
break;
}
}
if ($valid) {
$outline = $parent->addChild('outline');
$outline->addAttribute('xmlUrl', $feed['feed_url']);
$outline->addAttribute('htmlUrl', $feed['site_url']);
$outline->addAttribute('title', $feed['title']);
$outline->addAttribute('text', $feed['title']);
$outline->addAttribute('description', isset($feed['description']) ? $feed['description'] : $feed['title']);
$outline->addAttribute('type', 'rss');
$outline->addAttribute('version', 'RSS');
}
}
/**
* Create entries for a feed list.
*
* @param SimpleXMLElement $parent Parent Element
* @param array $feeds Feed list
*/
public function createEntries(SimpleXMLElement $parent, array $feeds)
{
foreach ($feeds as $feed) {
$this->createEntry($parent, $feed);
}
}
/**
* Create a category entry.
*
* @param SimpleXMLElement $parent Parent Element
* @param string $category Category
* @param array $feeds Feed properties
*/
public function createCategory(SimpleXMLElement $parent, $category, array $feeds)
{
$outline = $parent->addChild('outline');
$outline->addAttribute('text', $category);
$this->createEntries($outline, $feeds);
}
}

View File

@ -1,162 +0,0 @@
<?php
namespace PicoFeed\Serialization;
use SimpleXmlElement;
use StdClass;
use PicoFeed\Logging\Logger;
use PicoFeed\Parser\XmlParser;
/**
* OPML Import.
*
* @author Frederic Guillot
*/
class Import
{
/**
* OPML file content.
*
* @var string
*/
private $content = '';
/**
* Subscriptions.
*
* @var array
*/
private $items = array();
/**
* Constructor.
*
* @param string $content OPML file content
*/
public function __construct($content)
{
$this->content = $content;
}
/**
* Parse the OPML file.
*
* @return array|false
*/
public function execute()
{
Logger::setMessage(get_called_class().': start importation');
$xml = XmlParser::getSimpleXml(trim($this->content));
if ($xml === false || $xml->getName() !== 'opml' || !isset($xml->body)) {
Logger::setMessage(get_called_class().': OPML tag not found or malformed XML document');
return false;
}
$this->parseEntries($xml->body);
Logger::setMessage(get_called_class().': '.count($this->items).' subscriptions found');
return $this->items;
}
/**
* Parse each entries of the subscription list.
*
* @param SimpleXMLElement $tree XML node
*/
public function parseEntries(SimpleXMLElement $tree)
{
if (isset($tree->outline)) {
foreach ($tree->outline as $item) {
if (isset($item->outline)) {
$this->parseEntries($item);
} elseif ((isset($item['text']) || isset($item['title'])) && isset($item['xmlUrl'])) {
$entry = new StdClass();
$entry->category = $this->findCategory($tree);
$entry->title = $this->findTitle($item);
$entry->feed_url = $this->findFeedUrl($item);
$entry->site_url = $this->findSiteUrl($item, $entry);
$entry->type = $this->findType($item);
$entry->description = $this->findDescription($item, $entry);
$this->items[] = $entry;
}
}
}
}
/**
* Find category.
*
* @param SimpleXmlElement $tree XML tree
*
* @return string
*/
public function findCategory(SimpleXmlElement $tree)
{
return isset($tree['title']) ? (string) $tree['title'] : (string) $tree['text'];
}
/**
* Find title.
*
* @param SimpleXmlElement $item XML tree
*
* @return string
*/
public function findTitle(SimpleXmlElement $item)
{
return isset($item['title']) ? (string) $item['title'] : (string) $item['text'];
}
/**
* Find feed url.
*
* @param SimpleXmlElement $item XML tree
*
* @return string
*/
public function findFeedUrl(SimpleXmlElement $item)
{
return (string) $item['xmlUrl'];
}
/**
* Find site url.
*
* @param SimpleXmlElement $item XML tree
* @param StdClass $entry Feed entry
*
* @return string
*/
public function findSiteUrl(SimpleXmlElement $item, StdClass $entry)
{
return isset($item['htmlUrl']) ? (string) $item['htmlUrl'] : $entry->feed_url;
}
/**
* Find type.
*
* @param SimpleXmlElement $item XML tree
*
* @return string
*/
public function findType(SimpleXmlElement $item)
{
return isset($item['version']) ? (string) $item['version'] : isset($item['type']) ? (string) $item['type'] : 'rss';
}
/**
* Find description.
*
* @param SimpleXmlElement $item XML tree
* @param StdClass $entry Feed entry
*
* @return string
*/
public function findDescription(SimpleXmlElement $item, StdClass $entry)
{
return isset($item['description']) ? (string) $item['description'] : $entry->title;
}
}

View File

@ -0,0 +1,175 @@
<?php
namespace PicoFeed\Serialization;
/**
* Class Subscription
*
* @package PicoFeed\Serialization
* @author Frederic Guillot
*/
class Subscription
{
protected $title = '';
protected $feedUrl = '';
protected $siteUrl = '';
protected $category = '';
protected $description = '';
protected $type = '';
/**
* Create object instance
*
* @static
* @access public
* @return Subscription
*/
public static function create()
{
return new static();
}
/**
* Set title
*
* @access public
* @param string $title
* @return Subscription
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Get title
*
* @access public
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Set feed URL
*
* @access public
* @param string $feedUrl
* @return Subscription
*/
public function setFeedUrl($feedUrl)
{
$this->feedUrl = $feedUrl;
return $this;
}
/**
* Get feed URL
*
* @access public
* @return string
*/
public function getFeedUrl()
{
return $this->feedUrl;
}
/**
* Set site URL
*
* @access public
* @param string $siteUrl
* @return Subscription
*/
public function setSiteUrl($siteUrl)
{
$this->siteUrl = $siteUrl;
return $this;
}
/**
* Get site URL
*
* @access public
* @return string
*/
public function getSiteUrl()
{
return $this->siteUrl;
}
/**
* Set category
*
* @access public
* @param string $category
* @return Subscription
*/
public function setCategory($category)
{
$this->category = $category;
return $this;
}
/**
* Get category
*
* @access public
* @return string
*/
public function getCategory()
{
return $this->category;
}
/**
* Set description
*
* @access public
* @param string $description
* @return Subscription
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* @access public
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set type
*
* @access public
* @param string $type
* @return Subscription
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* Get type
*
* @access public
* @return string
*/
public function getType()
{
return $this->type;
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace PicoFeed\Serialization;
/**
* Class SubscriptionList
*
* @package PicoFeed\Serialization
* @author Frederic Guillot
*/
class SubscriptionList
{
/**
* OPML entries
*
* @var Subscription[]
*/
public $subscriptions = array();
/**
* Title
*
* @var string
*/
protected $title = '';
/**
* Create object instance
*
* @static
* @access public
* @return SubscriptionList
*/
public static function create()
{
return new static();
}
/**
* Set title
*
* @access public
* @param string $title
* @return SubscriptionList
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Get title
*
* @access public
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Add subscription
*
* @access public
* @param Subscription $subscription
* @return SubscriptionList
*/
public function addSubscription(Subscription $subscription)
{
$this->subscriptions[] = $subscription;
return $this;
}
}

View File

@ -0,0 +1,204 @@
<?php
namespace PicoFeed\Serialization;
use DOMDocument;
use DOMElement;
/**
* Class SubscriptionListBuilder
*
* @package PicoFeed\Serialization
* @author Frederic Guillot
*/
class SubscriptionListBuilder
{
/**
* @var SubscriptionList
*/
protected $subscriptionList;
/**
* @var DOMDocument
*/
protected $document;
/**
* Constructor.
*
* @access public
* @param SubscriptionList $subscriptionList
*/
public function __construct(SubscriptionList $subscriptionList)
{
$this->subscriptionList = $subscriptionList;
}
/**
* Get object instance
*
* @static
* @access public
* @param SubscriptionList $subscriptionList
* @return SubscriptionListBuilder
*/
public static function create(SubscriptionList $subscriptionList)
{
return new static($subscriptionList);
}
/**
* Build OPML feed
*
* @access public
* @param string $filename
* @return string
*/
public function build($filename = '')
{
$this->document = new DomDocument('1.0', 'UTF-8');
$this->document->formatOutput = true;
$opmlElement = $this->document->createElement('opml');
$opmlElement->setAttribute('version', '1.0');
$headElement = $this->document->createElement('head');
if ($this->subscriptionList->getTitle() !== '') {
$titleElement = $this->document->createElement('title');
$titleElement->appendChild($this->document->createTextNode($this->subscriptionList->getTitle()));
$headElement->appendChild($titleElement);
}
$opmlElement->appendChild($headElement);
$opmlElement->appendChild($this->buildBody());
$this->document->appendChild($opmlElement);
if ($filename !== '') {
$this->document->save($filename);
return '';
}
return $this->document->saveXML();
}
/**
* Return true if the list has categories
*
* @access public
* @return bool
*/
public function hasCategories()
{
foreach ($this->subscriptionList->subscriptions as $subscription) {
if ($subscription->getCategory() !== '') {
return true;
}
}
return false;
}
/**
* Build OPML body
*
* @access protected
* @return DOMElement
*/
protected function buildBody()
{
$bodyElement = $this->document->createElement('body');
if ($this->hasCategories()) {
$this->buildCategories($bodyElement);
return $bodyElement;
}
foreach ($this->subscriptionList->subscriptions as $subscription) {
$bodyElement->appendChild($this->buildSubscription($subscription));
}
return $bodyElement;
}
/**
* Build categories section
*
* @access protected
* @param DOMElement $bodyElement
*/
protected function buildCategories(DOMElement $bodyElement)
{
$categories = $this->groupByCategories();
foreach ($categories as $category => $subscriptions) {
$bodyElement->appendChild($this->buildCategory($category, $subscriptions));
}
}
/**
* Build category tag
*
* @access protected
* @param string $category
* @param array $subscriptions
* @return DOMElement
*/
protected function buildCategory($category, array $subscriptions)
{
$outlineElement = $this->document->createElement('outline');
$outlineElement->setAttribute('text', $category);
foreach ($subscriptions as $subscription) {
$outlineElement->appendChild($this->buildSubscription($subscription));
}
return $outlineElement;
}
/**
* Build subscription entry
*
* @access public
* @param Subscription $subscription
* @return DOMElement
*/
protected function buildSubscription(Subscription $subscription)
{
$outlineElement = $this->document->createElement('outline');
$outlineElement->setAttribute('type', $subscription->getType() ?: 'rss');
$outlineElement->setAttribute('text', $subscription->getTitle() ?: $subscription->getFeedUrl());
$outlineElement->setAttribute('xmlUrl', $subscription->getFeedUrl());
if ($subscription->getTitle() !== '') {
$outlineElement->setAttribute('title', $subscription->getTitle());
}
if ($subscription->getDescription() !== '') {
$outlineElement->setAttribute('description', $subscription->getDescription());
}
if ($subscription->getSiteUrl() !== '') {
$outlineElement->setAttribute('htmlUrl', $subscription->getSiteUrl());
}
return $outlineElement;
}
/**
* Group subscriptions by category
*
* @access private
* @return array
*/
private function groupByCategories()
{
$categories = array();
foreach ($this->subscriptionList->subscriptions as $subscription) {
$categories[$subscription->getCategory()][] = $subscription;
}
return $categories;
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace PicoFeed\Serialization;
use PicoFeed\Parser\MalformedXmlException;
use PicoFeed\Parser\XmlParser;
use SimpleXMLElement;
/**
* Class SubscriptionListParser
*
* @package PicoFeed\Serialization
* @author Frederic Guillot
*/
class SubscriptionListParser
{
/**
* @var SubscriptionList
*/
protected $subscriptionList;
/**
* @var string
*/
protected $data;
/**
* Constructor
*
* @access public
* @param string $data
*/
public function __construct($data)
{
$this->subscriptionList = new SubscriptionList();
$this->data = trim($data);
}
/**
* Get object instance
*
* @static
* @access public
* @param string $data
* @return SubscriptionListParser
*/
public static function create($data)
{
return new static($data);
}
/**
* Parse a subscription list entry
*
* @access public
* @throws MalformedXmlException
* @return SubscriptionList
*/
public function parse()
{
$xml = XmlParser::getSimpleXml($this->data);
if (! $xml || !isset($xml->head) || !isset($xml->body)) {
throw new MalformedXmlException('Unable to parse OPML file: invalid XML');
}
$this->parseTitle($xml->head);
$this->parseEntries($xml->body);
return $this->subscriptionList;
}
/**
* Parse title
*
* @access protected
* @param SimpleXMLElement $xml
*/
protected function parseTitle(SimpleXMLElement $xml)
{
$this->subscriptionList->setTitle((string) $xml->title);
}
/**
* Parse entries
*
* @access protected
* @param SimpleXMLElement $body
*/
private function parseEntries(SimpleXMLElement $body)
{
foreach ($body->outline as $outlineElement) {
if (isset($outlineElement->outline)) {
$this->parseEntries($outlineElement);
} else {
$this->subscriptionList->subscriptions[] = SubscriptionParser::create($body, $outlineElement)->parse();
}
}
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace PicoFeed\Serialization;
use SimpleXMLElement;
/**
* Class SubscriptionParser
*
* @package PicoFeed\Serialization
* @author Frederic Guillot
*/
class SubscriptionParser
{
/**
* @var Subscription
*/
protected $subscription;
/**
* @var SimpleXMLElement
*/
private $outlineElement;
/**
* @var SimpleXMLElement
*/
private $parentElement;
/**
* Constructor
*
* @access public
* @param SimpleXMLElement $parentElement
* @param SimpleXMLElement $outlineElement
*/
public function __construct(SimpleXMLElement $parentElement, SimpleXMLElement $outlineElement)
{
$this->parentElement = $parentElement;
$this->outlineElement = $outlineElement;
$this->subscription = new Subscription();
}
/**
* Get object instance
*
* @static
* @access public
* @param SimpleXMLElement $parentElement
* @param SimpleXMLElement $outlineElement
* @return SubscriptionParser
*/
public static function create(SimpleXMLElement $parentElement, SimpleXMLElement $outlineElement)
{
return new static($parentElement, $outlineElement);
}
/**
* Parse subscription entry
*
* @access public
* @return Subscription
*/
public function parse()
{
$this->subscription->setCategory($this->findCategory());
$this->subscription->setTitle($this->findTitle());
$this->subscription->setFeedUrl($this->findFeedUrl());
$this->subscription->setSiteUrl($this->findSiteUrl());
$this->subscription->setType($this->findType());
$this->subscription->setDescription($this->findDescription());
return $this->subscription;
}
/**
* Find category.
*
* @access protected
* @return string
*/
protected function findCategory()
{
return isset($this->parentElement['text']) ? (string) $this->parentElement['text'] : '';
}
/**
* Find title.
*
* @access protected
* @return string
*/
protected function findTitle()
{
return isset($this->outlineElement['title']) ? (string) $this->outlineElement['title'] : (string) $this->outlineElement['text'];
}
/**
* Find feed url.
*
* @access protected
* @return string
*/
protected function findFeedUrl()
{
return (string) $this->outlineElement['xmlUrl'];
}
/**
* Find site url.
*
* @access protected
* @return string
*/
protected function findSiteUrl()
{
return isset($this->outlineElement['htmlUrl']) ? (string) $this->outlineElement['htmlUrl'] : $this->findFeedUrl();
}
/**
* Find type.
*
* @access protected
* @return string
*/
protected function findType()
{
return isset($this->outlineElement['version']) ? (string) $this->outlineElement['version'] :
isset($this->outlineElement['type']) ? (string) $this->outlineElement['type'] : 'rss';
}
/**
* Find description.
*
* @access protected
* @return string
*/
protected function findDescription()
{
return isset($this->outlineElement['description']) ? (string) $this->outlineElement['description'] : $this->findTitle();
}
}

View File

@ -1,215 +0,0 @@
<?php
namespace PicoFeed\Syndication;
use DomDocument;
use DomElement;
use DomAttr;
/**
* Atom writer class.
*
* @author Frederic Guillot
*/
class Atom extends Writer
{
/**
* List of required properties for each feed.
*
* @var array
*/
private $required_feed_properties = array(
'title',
'site_url',
'feed_url',
);
/**
* List of required properties for each item.
*
* @var array
*/
private $required_item_properties = array(
'title',
'url',
);
/**
* Get the Atom document.
*
* @param string $filename Optional filename
*
* @return string
*/
public function execute($filename = '')
{
$this->checkRequiredProperties($this->required_feed_properties, $this);
$this->dom = new DomDocument('1.0', 'UTF-8');
$this->dom->formatOutput = true;
// <feed/>
$feed = $this->dom->createElement('feed');
$feed->setAttributeNodeNS(new DomAttr('xmlns', 'http://www.w3.org/2005/Atom'));
// <generator/>
$generator = $this->dom->createElement('generator', 'PicoFeed');
$generator->setAttribute('uri', 'https://github.com/fguillot/picoFeed');
$feed->appendChild($generator);
// <title/>
$title = $this->dom->createElement('title');
$title->appendChild($this->dom->createTextNode($this->title));
$feed->appendChild($title);
// <id/>
$id = $this->dom->createElement('id');
$id->appendChild($this->dom->createTextNode($this->site_url));
$feed->appendChild($id);
// <updated/>
$this->addUpdated($feed, $this->updated);
// <link rel="alternate" type="text/html" href="http://example.org/"/>
$this->addLink($feed, $this->site_url);
// <link rel="self" type="application/atom+xml" href="http://example.org/feed.atom"/>
$this->addLink($feed, $this->feed_url, 'self', 'application/atom+xml');
// <author/>
if (isset($this->author)) {
$this->addAuthor($feed, $this->author);
}
// <entry/>
foreach ($this->items as $item) {
$this->checkRequiredProperties($this->required_item_properties, $item);
$feed->appendChild($this->createEntry($item));
}
$this->dom->appendChild($feed);
if ($filename) {
$this->dom->save($filename);
} else {
return $this->dom->saveXML();
}
}
/**
* Create item entry.
*
* @param arrray $item Item properties
*
* @return DomElement
*/
public function createEntry(array $item)
{
$entry = $this->dom->createElement('entry');
// <title/>
$title = $this->dom->createElement('title');
$title->appendChild($this->dom->createTextNode($item['title']));
$entry->appendChild($title);
// <id/>
$id = $this->dom->createElement('id');
$id->appendChild($this->dom->createTextNode(isset($item['id']) ? $item['id'] : $item['url']));
$entry->appendChild($id);
// <updated/>
$this->addUpdated($entry, isset($item['updated']) ? $item['updated'] : '');
// <published/>
if (isset($item['published'])) {
$entry->appendChild($this->dom->createElement('published', date(DATE_ATOM, $item['published'])));
}
// <link rel="alternate" type="text/html" href="http://example.org/"/>
$this->addLink($entry, $item['url']);
// <summary/>
if (isset($item['summary'])) {
$summary = $this->dom->createElement('summary');
$summary->appendChild($this->dom->createTextNode($item['summary']));
$entry->appendChild($summary);
}
// <content/>
if (isset($item['content'])) {
$content = $this->dom->createElement('content');
$content->setAttribute('type', 'html');
$content->appendChild($this->dom->createCDATASection($item['content']));
$entry->appendChild($content);
}
// <author/>
if (isset($item['author'])) {
$this->addAuthor($entry, $item['author']);
}
return $entry;
}
/**
* Add Link.
*
* @param DomElement $xml XML node
* @param string $url URL
* @param string $rel Link rel attribute
* @param string $type Link type attribute
*/
public function addLink(DomElement $xml, $url, $rel = 'alternate', $type = 'text/html')
{
$link = $this->dom->createElement('link');
$link->setAttribute('rel', $rel);
$link->setAttribute('type', $type);
$link->setAttribute('href', $url);
$xml->appendChild($link);
}
/**
* Add publication date.
*
* @param DomElement $xml XML node
* @param int $value Timestamp
*/
public function addUpdated(DomElement $xml, $value = 0)
{
$xml->appendChild($this->dom->createElement(
'updated',
date(DATE_ATOM, $value ?: time())
));
}
/**
* Add author.
*
* @param DomElement $xml XML node
* @param array $values Author name and email
*/
public function addAuthor(DomElement $xml, array $values)
{
$author = $this->dom->createElement('author');
if (isset($values['name'])) {
$name = $this->dom->createElement('name');
$name->appendChild($this->dom->createTextNode($values['name']));
$author->appendChild($name);
}
if (isset($values['email'])) {
$email = $this->dom->createElement('email');
$email->appendChild($this->dom->createTextNode($values['email']));
$author->appendChild($email);
}
if (isset($values['url'])) {
$uri = $this->dom->createElement('uri');
$uri->appendChild($this->dom->createTextNode($values['url']));
$author->appendChild($uri);
}
$xml->appendChild($author);
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace PicoFeed\Syndication;
use DOMAttr;
use DOMElement;
/**
* Atom Feed Builder
*
* @package PicoFeed\Syndication
* @author Frederic Guillot
*/
class AtomFeedBuilder extends FeedBuilder
{
/**
* @var DOMElement
*/
protected $feedElement;
/**
* @var AtomHelper
*/
protected $helper;
/**
* Build feed
*
* @access public
* @param string $filename
* @return string
*/
public function build($filename = '')
{
$this->helper = new AtomHelper($this->getDocument());
$this->feedElement = $this->getDocument()->createElement('feed');
$this->feedElement->setAttributeNodeNS(new DomAttr('xmlns', 'http://www.w3.org/2005/Atom'));
$generator = $this->getDocument()->createElement('generator', 'PicoFeed');
$generator->setAttribute('uri', 'https://github.com/fguillot/picoFeed');
$this->feedElement->appendChild($generator);
$this->helper
->buildTitle($this->feedElement, $this->feedTitle)
->buildId($this->feedElement, $this->feedUrl)
->buildDate($this->feedElement, $this->feedDate)
->buildLink($this->feedElement, $this->siteUrl)
->buildLink($this->feedElement, $this->feedUrl, 'self', 'application/atom+xml')
->buildAuthor($this->feedElement, $this->authorName, $this->authorEmail, $this->authorUrl)
;
foreach ($this->items as $item) {
$this->feedElement->appendChild($item->build());
}
$this->getDocument()->appendChild($this->feedElement);
if ($filename !== '') {
$this->getDocument()->save($filename);
}
return $this->getDocument()->saveXML();
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace PicoFeed\Syndication;
use DateTime;
use DOMDocument;
use DOMElement;
/**
* Class AtomHelper
*
* @package PicoFeed\Syndication
* @author Frederic Guillot
*/
class AtomHelper
{
/**
* @var DOMDocument
*/
protected $document;
/**
* Constructor
*
* @param DOMDocument $document
*/
public function __construct(DOMDocument $document)
{
$this->document = $document;
}
/**
* Build node
*
* @access public
* @param DOMElement $element
* @param string $tag
* @param string $value
* @return AtomHelper
*/
public function buildNode(DOMElement $element, $tag, $value)
{
$node = $this->document->createElement($tag);
$node->appendChild($this->document->createTextNode($value));
$element->appendChild($node);
return $this;
}
/**
* Build title
*
* @access public
* @param DOMElement $element
* @param string $title
* @return AtomHelper
*/
public function buildTitle(DOMElement $element, $title)
{
return $this->buildNode($element, 'title', $title);
}
/**
* Build id
*
* @access public
* @param DOMElement $element
* @param string $id
* @return AtomHelper
*/
public function buildId(DOMElement $element, $id)
{
return $this->buildNode($element, 'id', $id);
}
/**
* Build date element
*
* @access public
* @param DOMElement $element
* @param DateTime $date
* @param string $type
* @return AtomHelper
*/
public function buildDate(DOMElement $element, DateTime $date, $type = 'updated')
{
return $this->buildNode($element, $type, $date->format(DateTime::ATOM));
}
/**
* Build link element
*
* @access public
* @param DOMElement $element
* @param string $url
* @param string $rel
* @param string $type
* @return AtomHelper
*/
public function buildLink(DOMElement $element, $url, $rel = 'alternate', $type = 'text/html')
{
$node = $this->document->createElement('link');
$node->setAttribute('rel', $rel);
$node->setAttribute('type', $type);
$node->setAttribute('href', $url);
$element->appendChild($node);
return $this;
}
/**
* Build author element
*
* @access public
* @param DOMElement $element
* @param string $authorName
* @param string $authorEmail
* @param string $authorUrl
* @return AtomHelper
*/
public function buildAuthor(DOMElement $element, $authorName, $authorEmail, $authorUrl)
{
if (!empty($authorName)) {
$author = $this->document->createElement('author');
$this->buildNode($author, 'name', $authorName);
if (!empty($authorEmail)) {
$this->buildNode($author, 'email', $authorEmail);
}
if (!empty($authorUrl)) {
$this->buildNode($author, 'uri', $authorUrl);
}
$element->appendChild($author);
}
return $this;
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace PicoFeed\Syndication;
use DOMElement;
/**
* Atom Item Builder
*
* @package PicoFeed\Syndication
* @author Frederic Guillot
*/
class AtomItemBuilder extends ItemBuilder
{
/**
* @var DOMElement
*/
protected $itemElement;
/**
* @var AtomHelper
*/
protected $helper;
/**
* Build item
*
* @access public
* @return DOMElement
*/
public function build()
{
$this->itemElement = $this->feedBuilder->getDocument()->createElement('entry');
$this->helper = new AtomHelper($this->feedBuilder->getDocument());
if (!empty($this->itemId)) {
$this->helper->buildId($this->itemElement, $this->itemId);
} else {
$this->helper->buildId($this->itemElement, $this->itemUrl);
}
$this->helper
->buildTitle($this->itemElement, $this->itemTitle)
->buildLink($this->itemElement, $this->itemUrl)
->buildDate($this->itemElement, $this->itemUpdatedDate, 'updated')
->buildDate($this->itemElement, $this->itemPublishedDate, 'published')
->buildAuthor($this->itemElement, $this->authorName, $this->authorEmail, $this->authorUrl)
;
if (!empty($this->itemSummary)) {
$this->helper->buildNode($this->itemElement, 'summary', $this->itemSummary);
}
if (!empty($this->itemContent)) {
$node = $this->feedBuilder->getDocument()->createElement('content');
$node->setAttribute('type', 'html');
$node->appendChild($this->feedBuilder->getDocument()->createCDATASection($this->itemContent));
$this->itemElement->appendChild($node);
}
return $this->itemElement;
}
}

View File

@ -0,0 +1,185 @@
<?php
namespace PicoFeed\Syndication;
use DateTime;
use DOMDocument;
/**
* Class FeedBuilder
*
* @package PicoFeed\Syndication
* @author Frederic Guillot
*/
abstract class FeedBuilder
{
/**
* @var DOMDocument
*/
protected $document;
/**
* @var string
*/
protected $feedTitle;
/**
* @var string
*/
protected $feedUrl;
/**
* @var string
*/
protected $siteUrl;
/**
* @var string
*/
protected $authorName;
/**
* @var string
*/
protected $authorEmail;
/**
* @var string
*/
protected $authorUrl;
/**
* @var DateTime
*/
protected $feedDate;
/**
* @var ItemBuilder[]
*/
protected $items;
/**
* Constructor
*
* @access public
*/
public function __construct()
{
$this->document = new DomDocument('1.0', 'UTF-8');
$this->document->formatOutput = true;
}
/**
* Get new object instance
*
* @access public
* @return static
*/
public static function create()
{
return new static();
}
/**
* Add feed title
*
* @access public
* @param string $title
* @return $this
*/
public function withTitle($title)
{
$this->feedTitle = $title;
return $this;
}
/**
* Add feed url
*
* @access public
* @param string $url
* @return $this
*/
public function withFeedUrl($url)
{
$this->feedUrl = $url;
return $this;
}
/**
* Add website url
*
* @access public
* @param string $url
* @return $this
*/
public function withSiteUrl($url)
{
$this->siteUrl = $url;
return $this;
}
/**
* Add feed date
*
* @access public
* @param DateTime $date
* @return $this
*/
public function withDate(DateTime $date)
{
$this->feedDate = $date;
return $this;
}
/**
* Add feed author
*
* @access public
* @param string $name
* @param string $email
* @param string $url
* @return $this
*/
public function withAuthor($name, $email = '', $url ='')
{
$this->authorName = $name;
$this->authorEmail = $email;
$this->authorUrl = $url;
return $this;
}
/**
* Add feed item
*
* @access public
* @param ItemBuilder $item
* @return $this
*/
public function withItem(ItemBuilder $item)
{
$this->items[] = $item;
return $this;
}
/**
* Get DOM document
*
* @access public
* @return DOMDocument
*/
public function getDocument()
{
return $this->document;
}
/**
* Build feed
*
* @abstract
* @access public
* @param string $filename
* @return string
*/
abstract public function build($filename = '');
}

View File

@ -0,0 +1,209 @@
<?php
namespace PicoFeed\Syndication;
use DateTime;
use DOMElement;
/**
* Class ItemBuilder
*
* @package PicoFeed\Syndication
* @author Frederic Guillot
*/
abstract class ItemBuilder
{
/**
* @var string
*/
protected $itemTitle;
/**
* @var string
*/
protected $itemId;
/**
* @var string
*/
protected $itemSummary;
/**
* @var string
*/
protected $authorName;
/**
* @var string
*/
protected $authorEmail;
/**
* @var string
*/
protected $authorUrl;
/**
* @var DateTime
*/
protected $itemPublishedDate;
/**
* @var DateTime
*/
protected $itemUpdatedDate;
/**
* @var string
*/
protected $itemContent;
/**
* @var string
*/
protected $itemUrl;
/**
* @var FeedBuilder
*/
protected $feedBuilder;
/**
* Constructor
*
* @param FeedBuilder $feedBuilder
*/
public function __construct(FeedBuilder $feedBuilder)
{
$this->feedBuilder = $feedBuilder;
}
/**
* Get new object instance
*
* @access public
* @param FeedBuilder $feedBuilder
* @return static
*/
public static function create(FeedBuilder $feedBuilder)
{
return new static($feedBuilder);
}
/**
* Add item title
*
* @access public
* @param string $title
* @return $this
*/
public function withTitle($title)
{
$this->itemTitle = $title;
return $this;
}
/**
* Add item id
*
* @access public
* @param string $id
* @return $this
*/
public function withId($id)
{
$this->itemId = $id;
return $this;
}
/**
* Add item url
*
* @access public
* @param string $url
* @return $this
*/
public function withUrl($url)
{
$this->itemUrl = $url;
return $this;
}
/**
* Add item summary
*
* @access public
* @param string $summary
* @return $this
*/
public function withSummary($summary)
{
$this->itemSummary = $summary;
return $this;
}
/**
* Add item content
*
* @access public
* @param string $content
* @return $this
*/
public function withContent($content)
{
$this->itemContent = $content;
return $this;
}
/**
* Add item updated date
*
* @access public
* @param DateTime $date
* @return $this
*/
public function withUpdatedDate(DateTime $date)
{
$this->itemUpdatedDate = $date;
return $this;
}
/**
* Add item published date
*
* @access public
* @param DateTime $date
* @return $this
*/
public function withPublishedDate(DateTime $date)
{
$this->itemPublishedDate = $date;
return $this;
}
/**
* Add item author
*
* @access public
* @param string $name
* @param string $email
* @param string $url
* @return $this
*/
public function withAuthor($name, $email = '', $url ='')
{
$this->authorName = $name;
$this->authorEmail = $email;
$this->authorUrl = $url;
return $this;
}
/**
* Build item
*
* @abstract
* @access public
* @return DOMElement
*/
abstract public function build();
}

View File

@ -1,206 +0,0 @@
<?php
namespace PicoFeed\Syndication;
use DomDocument;
use DomAttr;
use DomElement;
/**
* Rss 2.0 writer class.
*
* @author Frederic Guillot
*/
class Rss20 extends Writer
{
/**
* List of required properties for each feed.
*
* @var array
*/
private $required_feed_properties = array(
'title',
'site_url',
'feed_url',
);
/**
* List of required properties for each item.
*
* @var array
*/
private $required_item_properties = array(
'title',
'url',
);
/**
* Get the Rss 2.0 document.
*
* @param string $filename Optional filename
*
* @return string
*/
public function execute($filename = '')
{
$this->checkRequiredProperties($this->required_feed_properties, $this);
$this->dom = new DomDocument('1.0', 'UTF-8');
$this->dom->formatOutput = true;
// <rss/>
$rss = $this->dom->createElement('rss');
$rss->setAttribute('version', '2.0');
$rss->setAttributeNodeNS(new DomAttr('xmlns:content', 'http://purl.org/rss/1.0/modules/content/'));
$rss->setAttributeNodeNS(new DomAttr('xmlns:atom', 'http://www.w3.org/2005/Atom'));
$channel = $this->dom->createElement('channel');
// <generator/>
$generator = $this->dom->createElement('generator', 'PicoFeed (https://github.com/fguillot/picoFeed)');
$channel->appendChild($generator);
// <title/>
$title = $this->dom->createElement('title');
$title->appendChild($this->dom->createTextNode($this->title));
$channel->appendChild($title);
// <description/>
$description = $this->dom->createElement('description');
$description->appendChild($this->dom->createTextNode($this->description ?: $this->title));
$channel->appendChild($description);
// <pubDate/>
$this->addPubDate($channel, $this->updated);
// <atom:link/>
$link = $this->dom->createElement('atom:link');
$link->setAttribute('href', $this->feed_url);
$link->setAttribute('rel', 'self');
$link->setAttribute('type', 'application/rss+xml');
$channel->appendChild($link);
// <link/>
$link = $this->dom->createElement('link');
$link->appendChild($this->dom->createTextNode($this->site_url));
$channel->appendChild($link);
// <webMaster/>
if (isset($this->author)) {
$this->addAuthor($channel, 'webMaster', $this->author);
}
// <item/>
foreach ($this->items as $item) {
$this->checkRequiredProperties($this->required_item_properties, $item);
$channel->appendChild($this->createEntry($item));
}
$rss->appendChild($channel);
$this->dom->appendChild($rss);
if ($filename) {
$this->dom->save($filename);
} else {
return $this->dom->saveXML();
}
}
/**
* Create item entry.
*
* @param arrray $item Item properties
*
* @return DomElement
*/
public function createEntry(array $item)
{
$entry = $this->dom->createElement('item');
// <title/>
$title = $this->dom->createElement('title');
$title->appendChild($this->dom->createTextNode($item['title']));
$entry->appendChild($title);
// <link/>
$link = $this->dom->createElement('link');
$link->appendChild($this->dom->createTextNode($item['url']));
$entry->appendChild($link);
// <guid/>
if (isset($item['id'])) {
$guid = $this->dom->createElement('guid');
$guid->setAttribute('isPermaLink', 'false');
$guid->appendChild($this->dom->createTextNode($item['id']));
$entry->appendChild($guid);
} else {
$guid = $this->dom->createElement('guid');
$guid->setAttribute('isPermaLink', 'true');
$guid->appendChild($this->dom->createTextNode($item['url']));
$entry->appendChild($guid);
}
// <pubDate/>
$this->addPubDate($entry, isset($item['updated']) ? $item['updated'] : '');
// <description/>
if (isset($item['summary'])) {
$description = $this->dom->createElement('description');
$description->appendChild($this->dom->createTextNode($item['summary']));
$entry->appendChild($description);
}
// <content/>
if (isset($item['content'])) {
$content = $this->dom->createElement('content:encoded');
$content->appendChild($this->dom->createCDATASection($item['content']));
$entry->appendChild($content);
}
// <author/>
if (isset($item['author'])) {
$this->addAuthor($entry, 'author', $item['author']);
}
return $entry;
}
/**
* Add publication date.
*
* @param DomElement $xml XML node
* @param int $value Timestamp
*/
public function addPubDate(DomElement $xml, $value = 0)
{
$xml->appendChild($this->dom->createElement(
'pubDate',
date(DATE_RSS, $value ?: time())
));
}
/**
* Add author.
*
* @param DomElement $xml XML node
* @param string $tag Tag name
* @param array $values Author name and email
*/
public function addAuthor(DomElement $xml, $tag, array $values)
{
$value = '';
if (isset($values['email'])) {
$value .= $values['email'];
}
if ($value && isset($values['name'])) {
$value .= ' ('.$values['name'].')';
}
if ($value) {
$author = $this->dom->createElement($tag);
$author->appendChild($this->dom->createTextNode($value));
$xml->appendChild($author);
}
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace PicoFeed\Syndication;
use DOMAttr;
use DOMElement;
/**
* Rss20 Feed Builder
*
* @package PicoFeed\Syndication
* @author Frederic Guillot
*/
class Rss20FeedBuilder extends FeedBuilder
{
/**
* @var DOMElement
*/
protected $rssElement;
/**
* @var Rss20Helper
*/
protected $helper;
/**
* @var DOMElement
*/
protected $channelElement;
/**
* Build feed
*
* @access public
* @param string $filename
* @return string
*/
public function build($filename = '')
{
$this->helper = new Rss20Helper($this->getDocument());
$this->rssElement = $this->getDocument()->createElement('rss');
$this->rssElement->setAttribute('version', '2.0');
$this->rssElement->setAttributeNodeNS(new DomAttr('xmlns:content', 'http://purl.org/rss/1.0/modules/content/'));
$this->rssElement->setAttributeNodeNS(new DomAttr('xmlns:atom', 'http://www.w3.org/2005/Atom'));
$this->channelElement = $this->getDocument()->createElement('channel');
$this->helper
->buildNode($this->channelElement, 'generator', 'PicoFeed (https://github.com/fguillot/picoFeed)')
->buildTitle($this->channelElement, $this->feedTitle)
->buildNode($this->channelElement, 'description', $this->feedTitle)
->buildDate($this->channelElement, $this->feedDate)
->buildAuthor($this->channelElement, 'webMaster', $this->authorName, $this->authorEmail)
->buildLink($this->channelElement, $this->siteUrl)
;
$link = $this->getDocument()->createElement('atom:link');
$link->setAttribute('href', $this->feedUrl);
$link->setAttribute('rel', 'self');
$link->setAttribute('type', 'application/rss+xml');
$this->channelElement->appendChild($link);
foreach ($this->items as $item) {
$this->channelElement->appendChild($item->build());
}
$this->rssElement->appendChild($this->channelElement);
$this->getDocument()->appendChild($this->rssElement);
if ($filename !== '') {
$this->getDocument()->save($filename);
}
return $this->getDocument()->saveXML();
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace PicoFeed\Syndication;
use DateTime;
use DOMDocument;
use DOMElement;
/**
* Class Rss20Helper
*
* @package PicoFeed\Syndication
* @author Frederic Guillot
*/
class Rss20Helper
{
/**
* @var DOMDocument
*/
protected $document;
/**
* Constructor
*
* @param DOMDocument $document
*/
public function __construct(DOMDocument $document)
{
$this->document = $document;
}
/**
* Build node
*
* @access public
* @param DOMElement $element
* @param string $tag
* @param string $value
* @return AtomHelper
*/
public function buildNode(DOMElement $element, $tag, $value)
{
$node = $this->document->createElement($tag);
$node->appendChild($this->document->createTextNode($value));
$element->appendChild($node);
return $this;
}
/**
* Build title
*
* @access public
* @param DOMElement $element
* @param string $title
* @return AtomHelper
*/
public function buildTitle(DOMElement $element, $title)
{
return $this->buildNode($element, 'title', $title);
}
/**
* Build date element
*
* @access public
* @param DOMElement $element
* @param DateTime $date
* @param string $type
* @return AtomHelper
*/
public function buildDate(DOMElement $element, DateTime $date, $type = 'pubDate')
{
return $this->buildNode($element, $type, $date->format(DateTime::RSS));
}
/**
* Build link element
*
* @access public
* @param DOMElement $element
* @param string $url
* @return AtomHelper
*/
public function buildLink(DOMElement $element, $url)
{
return $this->buildNode($element, 'link', $url);
}
/**
* Build author element
*
* @access public
* @param DOMElement $element
* @param string $tag
* @param string $authorName
* @param string $authorEmail
* @return AtomHelper
*/
public function buildAuthor(DOMElement $element, $tag, $authorName, $authorEmail)
{
if (!empty($authorName)) {
$value = '';
if (!empty($authorEmail)) {
$value .= $authorEmail.' ('.$authorName.')';
} else {
$value = $authorName;
}
$this->buildNode($element, $tag, $value);
}
return $this;
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace PicoFeed\Syndication;
use DOMElement;
/**
* Rss20 Item Builder
*
* @package PicoFeed\Syndication
* @author Frederic Guillot
*/
class Rss20ItemBuilder extends ItemBuilder
{
/**
* @var DOMElement
*/
protected $itemElement;
/**
* @var Rss20Helper
*/
protected $helper;
/**
* Build item
*
* @access public
* @return DOMElement
*/
public function build()
{
$this->itemElement = $this->feedBuilder->getDocument()->createElement('item');
$this->helper = new Rss20Helper($this->feedBuilder->getDocument());
if (!empty($this->itemId)) {
$guid = $this->feedBuilder->getDocument()->createElement('guid');
$guid->setAttribute('isPermaLink', 'false');
$guid->appendChild($this->feedBuilder->getDocument()->createTextNode($this->itemId));
$this->itemElement->appendChild($guid);
} else {
$guid = $this->feedBuilder->getDocument()->createElement('guid');
$guid->setAttribute('isPermaLink', 'true');
$guid->appendChild($this->feedBuilder->getDocument()->createTextNode($this->itemUrl));
$this->itemElement->appendChild($guid);
}
$this->helper
->buildTitle($this->itemElement, $this->itemTitle)
->buildLink($this->itemElement, $this->itemUrl)
->buildDate($this->itemElement, $this->itemPublishedDate)
->buildAuthor($this->itemElement, 'author', $this->authorName, $this->authorEmail)
;
if (!empty($this->itemSummary)) {
$this->helper->buildNode($this->itemElement, 'description', $this->itemSummary);
}
if (!empty($this->itemContent)) {
$node = $this->feedBuilder->getDocument()->createElement('content:encoded');
$node->appendChild($this->feedBuilder->getDocument()->createCDATASection($this->itemContent));
$this->itemElement->appendChild($node);
}
return $this->itemElement;
}
}

View File

@ -1,95 +0,0 @@
<?php
namespace PicoFeed\Syndication;
use RuntimeException;
/**
* Base writer class.
*
* @author Frederic Guillot
*/
abstract class Writer
{
/**
* Dom object.
*
* @var \DomDocument
*/
protected $dom;
/**
* Items.
*
* @var array
*/
public $items = array();
/**
* Author.
*
* @var array
*/
public $author = array();
/**
* Feed URL.
*
* @var string
*/
public $feed_url = '';
/**
* Website URL.
*
* @var string
*/
public $site_url = '';
/**
* Feed title.
*
* @var string
*/
public $title = '';
/**
* Feed description.
*
* @var string
*/
public $description = '';
/**
* Feed modification date (timestamp).
*
* @var int
*/
public $updated = 0;
/**
* Generate the XML document.
*
* @abstract
*
* @param string $filename Optional filename
*
* @return string
*/
abstract public function execute($filename = '');
/**
* Check required properties to generate the output.
*
* @param array $properties List of properties
* @param mixed $container Object or array container
*/
public function checkRequiredProperties(array $properties, $container)
{
foreach ($properties as $property) {
if ((is_object($container) && !isset($container->$property)) || (is_array($container) && !isset($container[$property]))) {
throw new RuntimeException('Required property missing: '.$property);
}
}
}
}