Move to Composer and update to the last version of PicoFeed

This commit is contained in:
Frédéric Guillot 2014-12-23 21:28:26 -05:00
parent 86743febf6
commit c6ed11c116
278 changed files with 60369 additions and 1630 deletions

1
.gitignore vendored
View File

@ -43,6 +43,5 @@ Thumbs.db
# App specific # # App specific #
################ ################
config.php config.php
!vendor/PicoFeed/*
!models/* !models/*
!controllers/* !controllers/*

View File

@ -59,5 +59,5 @@ if (! is_writable(DATA_DIRECTORY)) {
// Include password_compat for PHP < 5.5 // Include password_compat for PHP < 5.5
if (version_compare(PHP_VERSION, '5.5.0', '<')) { if (version_compare(PHP_VERSION, '5.5.0', '<')) {
require __DIR__.'/vendor/password.php'; require __DIR__.'/lib/password.php';
} }

View File

@ -1,27 +1,6 @@
<?php <?php
require __DIR__.'/lib/Translator.php'; require __DIR__.'/vendor/autoload.php';
require __DIR__.'/vendor/PicoDb/Database.php';
require __DIR__.'/vendor/PicoFeed/PicoFeed.php';
require __DIR__.'/vendor/SimpleValidator/Validator.php';
require __DIR__.'/vendor/SimpleValidator/Base.php';
require __DIR__.'/vendor/SimpleValidator/Validators/Required.php';
require __DIR__.'/vendor/SimpleValidator/Validators/Unique.php';
require __DIR__.'/vendor/SimpleValidator/Validators/MaxLength.php';
require __DIR__.'/vendor/SimpleValidator/Validators/MinLength.php';
require __DIR__.'/vendor/SimpleValidator/Validators/Integer.php';
require __DIR__.'/vendor/SimpleValidator/Validators/Equals.php';
require __DIR__.'/vendor/SimpleValidator/Validators/AlphaNumeric.php';
require __DIR__.'/models/config.php';
require __DIR__.'/models/user.php';
require __DIR__.'/models/feed.php';
require __DIR__.'/models/item.php';
require __DIR__.'/models/schema.php';
require __DIR__.'/models/auto_update.php';
require __DIR__.'/models/database.php';
require __DIR__.'/models/remember_me.php';
if (file_exists(__DIR__.'/config.php')) { if (file_exists(__DIR__.'/config.php')) {
require __DIR__.'/config.php'; require __DIR__.'/config.php';

31
composer.json Normal file
View File

@ -0,0 +1,31 @@
{
"config": {
"preferred-install": "dist"
},
"require": {
"fguillot/simple-validator": "dev-master",
"fguillot/json-rpc": "dev-master",
"fguillot/picodb": "dev-master",
"fguillot/picofeed": "dev-master",
"fguillot/picofarad": "dev-master"
},
"autoload": {
"files": [
"lib/Translator.php",
"models/config.php",
"models/user.php",
"models/feed.php",
"models/item.php",
"models/schema.php",
"models/auto_update.php",
"models/database.php",
"models/remember_me.php"
],
"classmap": [
"vendor/fguillot/json-rpc/src/",
"vendor/fguillot/picodb/lib/",
"vendor/fguillot/picofeed/lib/",
"vendor/fguillot/simple-validator/src/"
]
}
}

View File

@ -52,5 +52,4 @@ define('PROXY_PASSWORD', '');
// ENABLE_AUTO_UPDATE => default is true (enable Miniflux update from the user interface) // ENABLE_AUTO_UPDATE => default is true (enable Miniflux update from the user interface)
define('ENABLE_AUTO_UPDATE', true); define('ENABLE_AUTO_UPDATE', true);
``` ```

View File

@ -20,7 +20,7 @@ However the content grabber doesn't work very well with all websites.
How to write a grabber rules file? How to write a grabber rules file?
---------------------------------- ----------------------------------
Add a PHP file to the directory `PicoFeed\Rules`, the filename must be the domain name: Add a PHP file to the directory `vendor\fguillot\PicoFeed\Rules`, the filename must be the domain name:
Example with the BBC website, `www.bbc.co.uk.php`: Example with the BBC website, `www.bbc.co.uk.php`:

View File

@ -1,11 +1,11 @@
<?php <?php
require __DIR__.'/common.php'; require __DIR__.'/common.php';
require __DIR__.'/vendor/PicoFarad/Template.php'; require __DIR__.'/vendor/fguillot/picofarad/lib/PicoFarad/Template.php';
require __DIR__.'/vendor/PicoFarad/Response.php'; require __DIR__.'/vendor/fguillot/picofarad/lib/PicoFarad/Response.php';
require __DIR__.'/vendor/PicoFarad/Request.php'; require __DIR__.'/vendor/fguillot/picofarad/lib/PicoFarad/Request.php';
require __DIR__.'/vendor/PicoFarad/Session.php'; require __DIR__.'/vendor/fguillot/picofarad/lib/PicoFarad/Session.php';
require __DIR__.'/vendor/PicoFarad/Router.php'; require __DIR__.'/vendor/fguillot/picofarad/lib/PicoFarad/Router.php';
require __DIR__.'/lib/helpers.php'; require __DIR__.'/lib/helpers.php';
use PicoFarad\Router; use PicoFarad\Router;

View File

@ -1,7 +1,6 @@
<?php <?php
require __DIR__.'/common.php'; require __DIR__.'/common.php';
require __DIR__.'/vendor/JsonRPC/Server.php';
use JsonRPC\Server; use JsonRPC\Server;

View File

@ -4,7 +4,7 @@ namespace Helper;
function isRTL(array $item) function isRTL(array $item)
{ {
return ! empty($item['rtl']) || \PicoFeed\Parser::isLanguageRTL($item['language']); return ! empty($item['rtl']) || \PicoFeed\Parser\Parser::isLanguageRTL($item['language']);
} }
function css() function css()

View File

@ -6,8 +6,8 @@ use DirectoryIterator;
use SimpleValidator\Validator; use SimpleValidator\Validator;
use SimpleValidator\Validators; use SimpleValidator\Validators;
use PicoDb\Database; use PicoDb\Database;
use PicoFeed\Config as ReaderConfig; use PicoFeed\Config\Config as ReaderConfig;
use PicoFeed\Logging; use PicoFeed\Logging\Logger;
const DB_VERSION = 30; const DB_VERSION = 30;
const HTTP_USER_AGENT = 'Miniflux (http://miniflux.net)'; const HTTP_USER_AGENT = 'Miniflux (http://miniflux.net)';
@ -18,17 +18,23 @@ function get_reader_config()
$config = new ReaderConfig; $config = new ReaderConfig;
$config->setTimezone(get('timezone')); $config->setTimezone(get('timezone'));
// Client
$config->setClientTimeout(HTTP_TIMEOUT); $config->setClientTimeout(HTTP_TIMEOUT);
$config->setClientUserAgent(HTTP_USER_AGENT); $config->setClientUserAgent(HTTP_USER_AGENT);
$config->setGrabberUserAgent(HTTP_USER_AGENT); $config->setGrabberUserAgent(HTTP_USER_AGENT);
// Proxy
$config->setProxyHostname(PROXY_HOSTNAME); $config->setProxyHostname(PROXY_HOSTNAME);
$config->setProxyPort(PROXY_PORT); $config->setProxyPort(PROXY_PORT);
$config->setProxyUsername(PROXY_USERNAME); $config->setProxyUsername(PROXY_USERNAME);
$config->setProxyPassword(PROXY_PASSWORD); $config->setProxyPassword(PROXY_PASSWORD);
// Filter
$config->setFilterIframeWhitelist(get_iframe_whitelist()); $config->setFilterIframeWhitelist(get_iframe_whitelist());
// Parser
$config->setParserHashAlgo('crc32b');
return $config; return $config;
} }
@ -47,7 +53,7 @@ function get_iframe_whitelist()
// Send a debug message to the console // Send a debug message to the console
function debug($line) function debug($line)
{ {
Logging::setMessage($line); Logger::setMessage($line);
write_debug(); write_debug();
} }
@ -55,7 +61,7 @@ function debug($line)
function write_debug() function write_debug()
{ {
if (DEBUG) { if (DEBUG) {
file_put_contents(DEBUG_FILENAME, implode(PHP_EOL, Logging::getMessages())); file_put_contents(DEBUG_FILENAME, implode(PHP_EOL, Logger::getMessages()));
} }
} }

View File

@ -5,12 +5,12 @@ namespace Model\Feed;
use SimpleValidator\Validator; use SimpleValidator\Validator;
use SimpleValidator\Validators; use SimpleValidator\Validators;
use PicoDb\Database; use PicoDb\Database;
use PicoFeed\Export;
use PicoFeed\Import;
use PicoFeed\Reader;
use PicoFeed\Logging;
use Model\Config; use Model\Config;
use Model\Item; use Model\Item;
use PicoFeed\Serialization\Export;
use PicoFeed\Serialization\Import;
use PicoFeed\Reader\Reader;
use PicoFeed\PicoFeedException;
const LIMIT_ALL = -1; const LIMIT_ALL = -1;
@ -40,7 +40,6 @@ function export_opml()
// Import OPML file // Import OPML file
function import_opml($content) function import_opml($content)
{ {
Logging::setTimezone(Config\get('timezone'));
$import = new Import($content); $import = new Import($content);
$feeds = $import->execute(); $feeds = $import->execute();
@ -76,12 +75,24 @@ function import_opml($content)
// Add a new feed from an URL // Add a new feed from an URL
function create($url, $enable_grabber = false, $force_rtl = false) function create($url, $enable_grabber = false, $force_rtl = false)
{ {
$reader = new Reader(Config\get_reader_config()); try {
$resource = $reader->download($url); $db = Database::get('db');
$parser = $reader->getParser(); // Discover the feed
$reader = new Reader(Config\get_reader_config());
$resource = $reader->discover($url);
if ($parser !== false) { // Feed already there
if ($db->table('feeds')->eq('feed_url', $resource->getUrl())->count()) {
return false;
}
// Parse the feed
$parser = $reader->getParser(
$resource->getUrl(),
$resource->getContent(),
$resource->getEncoding()
);
if ($enable_grabber) { if ($enable_grabber) {
$parser->enableContentGrabber(); $parser->enableContentGrabber();
@ -89,56 +100,38 @@ function create($url, $enable_grabber = false, $force_rtl = false)
$feed = $parser->execute(); $feed = $parser->execute();
if ($feed === false) { // Save the feed
$result = $db->table('feeds')->save(array(
'title' => $feed->getTitle(),
'site_url' => $feed->getSiteUrl(),
'feed_url' => $feed->getFeedUrl(),
'download_content' => $enable_grabber ? 1 : 0,
'rtl' => $force_rtl ? 1 : 0,
'last_modified' => $resource->getLastModified(),
'last_checked' => time(),
'etag' => $resource->getEtag(),
));
if ($result) {
$feed_id = $db->getConnection()->getLastId();
Item\update_all($feed_id, $feed->getItems());
Config\write_debug(); Config\write_debug();
return false;
}
if (! $feed->getUrl()) { return (int) $feed_id;
$feed->url = $reader->getUrl();
}
if (! $feed->getTitle()) {
Config\write_debug();
return false;
}
$db = Database::get('db');
// Check if the feed is already there
if (! $db->table('feeds')->eq('feed_url', $reader->getUrl())->count()) {
// Etag and LastModified are added the next update
$rs = $db->table('feeds')->save(array(
'title' => $feed->getTitle(),
'site_url' => $feed->getUrl(),
'feed_url' => $reader->getUrl(),
'download_content' => $enable_grabber ? 1 : 0,
'rtl' => $force_rtl ? 1 : 0,
));
if ($rs) {
$feed_id = $db->getConnection()->getLastId();
Item\update_all($feed_id, $feed->getItems(), $enable_grabber);
Config\write_debug();
return (int) $feed_id;
}
} }
} }
catch (PicoFeedException $e) {}
Config\write_debug(); Config\write_debug();
return false; return false;
} }
// Refresh all feeds // Refresh all feeds
function refresh_all($limit = LIMIT_ALL) function refresh_all($limit = LIMIT_ALL)
{ {
$feeds_id = get_ids($limit); foreach (@get_ids($limit) as $feed_id) {
foreach ($feeds_id as $feed_id) {
refresh($feed_id); refresh($feed_id);
} }
@ -151,53 +144,57 @@ function refresh_all($limit = LIMIT_ALL)
// Refresh one feed // Refresh one feed
function refresh($feed_id) function refresh($feed_id)
{ {
$feed = get($feed_id); try {
if (empty($feed)) { $feed = get($feed_id);
return false;
}
$reader = new Reader(Config\get_reader_config()); if (empty($feed)) {
return false;
$resource = $reader->download(
$feed['feed_url'],
$feed['last_modified'],
$feed['etag']
);
// Update the `last_checked` column each time, HTTP cache or not
update_last_checked($feed_id);
if (! $resource->isModified()) {
update_parsing_error($feed_id, 0);
Config\write_debug();
return true;
}
$parser = $reader->getParser();
if ($parser !== false) {
if ($feed['download_content']) {
// Don't fetch previous items, only new one
$parser->enableContentGrabber();
$parser->setGrabberIgnoreUrls(Database::get('db')->table('items')->eq('feed_id', $feed_id)->findAllByColumn('url'));
} }
$result = $parser->execute(); $reader = new Reader(Config\get_reader_config());
if ($result !== false) { $resource = $reader->download(
$feed['feed_url'],
$feed['last_modified'],
$feed['etag']
);
// Update the `last_checked` column each time, HTTP cache or not
update_last_checked($feed_id);
// Feed modified
if ($resource->isModified()) {
$parser = $reader->getParser(
$resource->getUrl(),
$resource->getContent(),
$resource->getEncoding()
);
if ($feed['download_content']) {
$parser->enableContentGrabber();
// Don't fetch previous items, only new one
$parser->setGrabberIgnoreUrls(
Database::get('db')->table('items')->eq('feed_id', $feed_id)->findAllByColumn('url')
);
}
$feed = $parser->execute();
update_parsing_error($feed_id, 0);
update_cache($feed_id, $resource->getLastModified(), $resource->getEtag()); update_cache($feed_id, $resource->getLastModified(), $resource->getEtag());
Item\update_all($feed_id, $result->getItems(), $feed['download_content']); Item\update_all($feed_id, $feed->getItems());
Config\write_debug();
return true;
} }
update_parsing_error($feed_id, 0);
Config\write_debug();
return true;
} }
catch (PicoFeedException $e) {}
update_parsing_error($feed_id, 1); update_parsing_error($feed_id, 1);
Config\write_debug(); Config\write_debug();
@ -208,15 +205,13 @@ function refresh($feed_id)
// Get the list of feeds ID to refresh // Get the list of feeds ID to refresh
function get_ids($limit = LIMIT_ALL) function get_ids($limit = LIMIT_ALL)
{ {
$table_feeds = Database::get('db')->table('feeds') $query = Database::get('db')->table('feeds')->eq('enabled', 1)->asc('last_checked');
->eq('enabled', 1)
->asc('last_checked');
if ($limit !== LIMIT_ALL) { if ($limit !== LIMIT_ALL) {
$table_feeds->limit((int) $limit); $query->limit((int) $limit);
} }
return $table_feeds->listing('id', 'id'); return $query->listing('id', 'id');
} }
// Get feeds with no item // Get feeds with no item
@ -231,7 +226,6 @@ function get_all_empty()
->findAll(); ->findAll();
foreach ($feeds as $key => &$feed) { foreach ($feeds as $key => &$feed) {
if ($feed['nb_items'] > 0) { if ($feed['nb_items'] > 0) {
unset($feeds[$key]); unset($feeds[$key]);
} }

View File

@ -4,10 +4,9 @@ namespace Model\Item;
use Model\Config; use Model\Config;
use PicoDb\Database; use PicoDb\Database;
use PicoFeed\Logging; use PicoFeed\Logging\Logger;
use PicoFeed\Grabber; use PicoFeed\Client\Grabber;
use PicoFeed\Client; use PicoFeed\Filter\Filter;
use PicoFeed\Filter;
// Get all items without filtering // Get all items without filtering
function get_everything() function get_everything()
@ -424,7 +423,7 @@ function autoflush_unread()
} }
// Update all items // Update all items
function update_all($feed_id, array $items, $enable_grabber = false) function update_all($feed_id, array $items)
{ {
$nocontent = (bool) Config\get('nocontent'); $nocontent = (bool) Config\get('nocontent');
@ -435,12 +434,12 @@ function update_all($feed_id, array $items, $enable_grabber = false)
foreach ($items as $item) { foreach ($items as $item) {
Logging::setMessage('Item => '.$item->getId().' '.$item->getUrl()); Logger::setMessage('Item => '.$item->getId().' '.$item->getUrl());
// Item parsed correctly? // Item parsed correctly?
if ($item->getId() && $item->getUrl()) { if ($item->getId() && $item->getUrl()) {
Logging::setMessage('Item parsed correctly'); Logger::setMessage('Item parsed correctly');
// Get item record in database, if any // Get item record in database, if any
$itemrec = $db $itemrec = $db
@ -452,11 +451,7 @@ function update_all($feed_id, array $items, $enable_grabber = false)
// Insert a new item // Insert a new item
if ($itemrec === null) { if ($itemrec === null) {
Logging::setMessage('Item added to the database'); Logger::setMessage('Item added to the database');
if ($enable_grabber && ! $nocontent && ! $item->getContent()) {
$item->content = download_content_url($item->getUrl());
}
$db->table('items')->save(array( $db->table('items')->save(array(
'id' => $item->getId(), 'id' => $item->getId(),
@ -474,7 +469,7 @@ function update_all($feed_id, array $items, $enable_grabber = false)
} }
else if (! $itemrec['enclosure'] && $item->getEnclosureUrl()) { else if (! $itemrec['enclosure'] && $item->getEnclosureUrl()) {
Logging::setMessage('Update item enclosure'); Logger::setMessage('Update item enclosure');
$db->table('items')->eq('id', $item->getId())->save(array( $db->table('items')->eq('id', $item->getId())->save(array(
'status' => 'unread', 'status' => 'unread',
@ -483,7 +478,7 @@ function update_all($feed_id, array $items, $enable_grabber = false)
)); ));
} }
else { else {
Logging::setMessage('Item already in the database'); Logger::setMessage('Item already in the database');
} }
// Items inside this feed // Items inside this feed
@ -523,7 +518,7 @@ function cleanup($feed_id, array $items_in_feed)
if (! empty($items_to_remove)) { if (! empty($items_to_remove)) {
$nb_items = count($items_to_remove); $nb_items = count($items_to_remove);
Logging::setMessage('There is '.$nb_items.' items to remove'); Logger::setMessage('There is '.$nb_items.' items to remove');
// Handle the case when there is a huge number of items to remove // Handle the case when there is a huge number of items to remove
// Sqlite have a limit of 1000 sql variables by default // Sqlite have a limit of 1000 sql variables by default
@ -554,7 +549,7 @@ function download_content_url($url)
$grabber->download(); $grabber->download();
if ($grabber->parse()) { if ($grabber->parse()) {
$content = $grabber->getcontent(); $content = $grabber->getFilteredcontent();
} }
if (! empty($content)) { if (! empty($content)) {

View File

@ -10,12 +10,18 @@ git clone --depth 1 https://github.com/fguillot/$APP.git
rm -rf $APP/data/*.sqlite \ rm -rf $APP/data/*.sqlite \
$APP/.git \ $APP/.git \
$APP/.gitignore \
$APP/scripts \ $APP/scripts \
$APP/docs \ $APP/Dockerfile \
$APP/README.* \ $APP/vendor/composer/installed.json
$APP/Dockerfile
find $APP -name docs -type d -exec rm -rf {} +;
find $APP -name tests -type d -exec rm -rf {} +;
find $APP -name composer.json -delete
find $APP -name phpunit.xml -delete
find $APP -name .travis.yml -delete
find $APP -name README.* -delete
find $APP -name .gitignore -delete
find $APP -name *.less -delete find $APP -name *.less -delete
find $APP -name *.scss -delete find $APP -name *.scss -delete
find $APP -name *.rb -delete find $APP -name *.rb -delete

1
vendor/.htaccess vendored
View File

@ -1 +0,0 @@
Deny from all

View File

@ -1,352 +0,0 @@
<?php
namespace JsonRPC;
use ReflectionFunction;
use Closure;
/**
* JsonRPC server class
*
* @package JsonRPC
* @author Frederic Guillot
* @license Unlicense http://unlicense.org/
*/
class Server
{
/**
* Data received from the client
*
* @access private
* @var string
*/
private $payload;
/**
* List of procedures
*
* @static
* @access private
* @var array
*/
static private $procedures = array();
/**
* Constructor
*
* @access public
* @param string $payload Client data
*/
public function __construct($payload = '')
{
$this->payload = $payload;
}
/**
* IP based client restrictions
*
* Return an HTTP error 403 if the client is not allowed
*
* @access public
* @param array $hosts List of hosts
*/
public function allowHosts(array $hosts) {
if (! in_array($_SERVER['REMOTE_ADDR'], $hosts)) {
header('Content-Type: application/json');
header('HTTP/1.0 403 Forbidden');
echo '["Access Forbidden"]';
exit;
}
}
/**
* HTTP Basic authentication
*
* Return an HTTP error 401 if the client is not allowed
*
* @access public
* @param array $users Map of username/password
*/
public function authentication(array $users)
{
// OVH workaround
if (isset($_SERVER['REMOTE_USER'])) {
list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = explode(':', base64_decode(substr($_SERVER['REMOTE_USER'], 6)));
}
if (! isset($_SERVER['PHP_AUTH_USER']) ||
! isset($users[$_SERVER['PHP_AUTH_USER']]) ||
$users[$_SERVER['PHP_AUTH_USER']] !== $_SERVER['PHP_AUTH_PW']) {
header('WWW-Authenticate: Basic realm="JsonRPC"');
header('Content-Type: application/json');
header('HTTP/1.0 401 Unauthorized');
echo '["Authentication failed"]';
exit;
}
}
/**
* Register a new procedure
*
* @access public
* @param string $name Procedure name
* @param closure $callback Callback
*/
public function register($name, Closure $callback)
{
self::$procedures[$name] = $callback;
}
/**
* Unregister a procedure
*
* @access public
* @param string $name Procedure name
*/
public function unregister($name)
{
if (isset(self::$procedures[$name])) {
unset(self::$procedures[$name]);
}
}
/**
* Unregister all procedures
*
* @access public
*/
public function unregisterAll()
{
self::$procedures = array();
}
/**
* Return the response to the client
*
* @access public
* @param array $data Data to send to the client
* @param array $payload Incoming data
* @return string
*/
public function getResponse(array $data, array $payload = array())
{
if (! array_key_exists('id', $payload)) {
return '';
}
$response = array(
'jsonrpc' => '2.0',
'id' => $payload['id']
);
$response = array_merge($response, $data);
@header('Content-Type: application/json');
return json_encode($response);
}
/**
* Map arguments to the procedure
*
* @access public
* @param array $request_params Incoming arguments
* @param array $method_params Procedure arguments
* @param array $params Arguments to pass to the callback
* @param integer $nb_required_params Number of required parameters
* @return bool
*/
public function mapParameters(array $request_params, array $method_params, array &$params, $nb_required_params)
{
if (count($request_params) < $nb_required_params) {
return false;
}
// Positional parameters
if (array_keys($request_params) === range(0, count($request_params) - 1)) {
$params = $request_params;
return true;
}
// Named parameters
foreach ($method_params as $p) {
$name = $p->getName();
if (isset($request_params[$name])) {
$params[$name] = $request_params[$name];
}
else if ($p->isDefaultValueAvailable()) {
$params[$name] = $p->getDefaultValue();
}
else {
return false;
}
}
return true;
}
/**
* Parse the payload and test if the parsed JSON is ok
*
* @access public
* @return boolean
*/
public function isValidJsonFormat()
{
if (empty($this->payload)) {
$this->payload = file_get_contents('php://input');
}
if (is_string($this->payload)) {
$this->payload = json_decode($this->payload, true);
}
return is_array($this->payload);
}
/**
* Test if all required JSON-RPC parameters are here
*
* @access public
* @return boolean
*/
public function isValidJsonRpcFormat()
{
if (! isset($this->payload['jsonrpc']) ||
! isset($this->payload['method']) ||
! is_string($this->payload['method']) ||
$this->payload['jsonrpc'] !== '2.0' ||
(isset($this->payload['params']) && ! is_array($this->payload['params']))) {
return false;
}
return true;
}
/**
* Return true if we have a batch request
*
* @access public
* @return boolean
*/
private function isBatchRequest()
{
return array_keys($this->payload) === range(0, count($this->payload) - 1);
}
/**
* Handle batch request
*
* @access private
* @return string
*/
private function handleBatchRequest()
{
$responses = array();
foreach ($this->payload as $payload) {
if (! is_array($payload)) {
$responses[] = $this->getResponse(array(
'error' => array(
'code' => -32600,
'message' => 'Invalid Request'
)),
array('id' => null)
);
}
else {
$server = new Server($payload);
$response = $server->execute();
if ($response) {
$responses[] = $response;
}
}
}
return empty($responses) ? '' : '['.implode(',', $responses).']';
}
/**
* Parse incoming requests
*
* @access public
* @return string
*/
public function execute()
{
// Invalid Json
if (! $this->isValidJsonFormat()) {
return $this->getResponse(array(
'error' => array(
'code' => -32700,
'message' => 'Parse error'
)),
array('id' => null)
);
}
// Handle batch request
if ($this->isBatchRequest()){
return $this->handleBatchRequest();
}
// Invalid JSON-RPC format
if (! $this->isValidJsonRpcFormat()) {
return $this->getResponse(array(
'error' => array(
'code' => -32600,
'message' => 'Invalid Request'
)),
array('id' => null)
);
}
// Procedure not found
if (! isset(self::$procedures[$this->payload['method']])) {
return $this->getResponse(array(
'error' => array(
'code' => -32601,
'message' => 'Method not found'
)),
$this->payload
);
}
// Execute the procedure
$callback = self::$procedures[$this->payload['method']];
$params = array();
$reflection = new ReflectionFunction($callback);
if (isset($this->payload['params'])) {
$parameters = $reflection->getParameters();
if (! $this->mapParameters($this->payload['params'], $parameters, $params, $reflection->getNumberOfRequiredParameters())) {
return $this->getResponse(array(
'error' => array(
'code' => -32602,
'message' => 'Invalid params'
)),
$this->payload
);
}
}
$result = $reflection->invokeArgs($params);
return $this->getResponse(array('result' => $result), $this->payload);
}
}

View File

@ -1,155 +0,0 @@
<?php
namespace PicoDb;
class Database
{
private static $instances = array();
private $logs = array();
private $pdo;
public function __construct(array $settings)
{
if (! isset($settings['driver'])) {
throw new \LogicException('You must define a database driver.');
}
switch ($settings['driver']) {
case 'sqlite':
require_once __DIR__.'/Drivers/Sqlite.php';
$this->pdo = new Sqlite($settings);
break;
case 'mysql':
require_once __DIR__.'/Drivers/Mysql.php';
$this->pdo = new Mysql($settings);
break;
case 'postgres':
require_once __DIR__.'/Drivers/Postgres.php';
$this->pdo = new Postgres($settings);
break;
default:
throw new \LogicException('This database driver is not supported.');
}
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
}
public static function bootstrap($name, \Closure $callback)
{
self::$instances[$name] = $callback;
}
public static function get($name)
{
if (! isset(self::$instances[$name])) {
throw new \LogicException('No database instance created with that name.');
}
if (is_callable(self::$instances[$name])) {
self::$instances[$name] = call_user_func(self::$instances[$name]);
}
return self::$instances[$name];
}
public function setLogMessage($message)
{
$this->logs[] = $message;
}
public function getLogMessages()
{
return $this->logs;
}
public function getConnection()
{
return $this->pdo;
}
public function closeConnection()
{
$this->pdo = null;
}
public function escapeIdentifier($value)
{
// Do not escape custom query
if (strpos($value, '.') !== false || strpos($value, ' ') !== false) {
return $value;
}
return $this->pdo->escapeIdentifier($value);
}
public function execute($sql, array $values = array())
{
try {
$this->setLogMessage($sql);
$this->setLogMessage(implode(', ', $values));
$rq = $this->pdo->prepare($sql);
$rq->execute($values);
return $rq;
}
catch (\PDOException $e) {
if ($this->pdo->inTransaction()) $this->pdo->rollback();
$this->setLogMessage($e->getMessage());
return false;
}
}
public function startTransaction()
{
if (! $this->pdo->inTransaction()) {
$this->pdo->beginTransaction();
}
}
public function closeTransaction()
{
if ($this->pdo->inTransaction()) {
$this->pdo->commit();
}
}
public function cancelTransaction()
{
if ($this->pdo->inTransaction()) {
$this->pdo->rollback();
}
}
public function table($table_name)
{
require_once __DIR__.'/Table.php';
return new Table($this, $table_name);
}
public function schema()
{
require_once __DIR__.'/Schema.php';
return new Schema($this);
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace PicoFeed\Parsers;
require_once __DIR__.'/Rss20.php';
use PicoFeed\Parsers\Rss20;
/**
* RSS 0.91 Parser
*
* @author Frederic Guillot
* @package parser
*/
class Rss91 extends Rss20
{
}

View File

@ -1,17 +0,0 @@
<?php
namespace PicoFeed\Parsers;
require_once __DIR__.'/Rss20.php';
use PicoFeed\Parsers\Rss20;
/**
* RSS 0.92 Parser
*
* @author Frederic Guillot
* @package parser
*/
class Rss92 extends Rss20
{
}

View File

@ -1,25 +0,0 @@
<?php
// Include this file if you don't want to use an autoloader
require __DIR__.'/Config.php';
require __DIR__.'/Logging.php';
require __DIR__.'/Url.php';
require __DIR__.'/Item.php';
require __DIR__.'/Feed.php';
require __DIR__.'/Client.php';
require __DIR__.'/Filter.php';
require __DIR__.'/Filter/Attribute.php';
require __DIR__.'/Filter/Tag.php';
require __DIR__.'/Filter/Html.php';
require __DIR__.'/XmlParser.php';
require __DIR__.'/Encoding.php';
require __DIR__.'/Grabber.php';
require __DIR__.'/Reader.php';
require __DIR__.'/Import.php';
require __DIR__.'/Export.php';
require __DIR__.'/Writer.php';
require __DIR__.'/Writers/Rss20.php';
require __DIR__.'/Writers/Atom.php';
require __DIR__.'/Parser.php';
require __DIR__.'/Favicon.php';

View File

@ -1,310 +0,0 @@
<?php
namespace PicoFeed;
use DOMXPath;
use PicoFeed\Config;
use PicoFeed\XmlParser;
use PicoFeed\Logging;
use PicoFeed\Filter;
use PicoFeed\Client;
use PicoFeed\Parser;
use PicoFeed\Url;
/**
* Reader class
*
* @author Frederic Guillot
* @package picofeed
*/
class Reader
{
/**
* Feed or site URL
*
* @access private
* @var string
*/
private $url = '';
/**
* Feed content
*
* @access private
* @var string
*/
private $content = '';
/**
* HTTP encoding
*
* @access private
* @var string
*/
private $encoding = '';
/**
* Config class instance
*
* @access private
* @var \PicoFeed\Config
*/
private $config = null;
/**
* Constructor
*
* @access public
* @param \PicoFeed\Config $config Config class instance
*/
public function __construct(Config $config = null)
{
$this->config = $config ?: new Config;
Logging::setTimezone($this->config->getTimezone());
}
/**
* Download a feed
*
* @access public
* @param string $url Feed content
* @param string $last_modified Last modified HTTP header
* @param string $etag Etag HTTP header
* @return \PicoFeed\Client
*/
public function download($url, $last_modified = '', $etag = '')
{
if (strpos($url, 'http') !== 0) {
$url = 'http://'.$url;
}
$client = Client::getInstance();
$client->setConfig($this->config)
->setLastModified($last_modified)
->setEtag($etag);
if ($client->execute($url)) {
$this->content = $client->getContent();
$this->url = $client->getUrl();
$this->encoding = $client->getEncoding();
}
return $client;
}
/**
* Get a parser instance with a custom config
*
* @access public
* @param string $name Parser name
* @return \PicoFeed\Parser
*/
public function getParserInstance($name)
{
require_once __DIR__.'/Parsers/'.ucfirst($name).'.php';
$name = '\PicoFeed\Parsers\\'.$name;
$parser = new $name($this->content, $this->encoding, $this->getUrl());
$parser->setHashAlgo($this->config->getParserHashAlgo());
$parser->setTimezone($this->config->getTimezone());
$parser->setConfig($this->config);
return $parser;
}
/**
* Get the first XML tag
*
* @access public
* @param string $data Feed content
* @return string
*/
public function getFirstTag($data)
{
// Strip HTML comments (max of 5,000 characters long to prevent crashing)
$data = preg_replace('/<!--(.{0,5000}?)-->/Uis', '', $data);
/* Strip Doctype:
* Doctype needs to be within the first 100 characters. (Ideally the first!)
* If it's not found by then, we need to stop looking to prevent PREG
* from reaching max backtrack depth and crashing.
*/
$data = preg_replace('/^.{0,100}<!DOCTYPE([^>]*)>/Uis', '', $data);
// Strip <?xml version....
$data = Filter::stripXmlTag($data);
// Find the first tag
$open_tag = strpos($data, '<');
$close_tag = strpos($data, '>');
return substr($data, $open_tag, $close_tag);
}
/**
* Detect the feed format
*
* @access public
* @param string $parser_name Parser name
* @param string $haystack First XML tag
* @param array $needles List of strings that need to be there
* @return mixed False on failure or Parser instance
*/
public function detectFormat($parser_name, $haystack, array $needles)
{
$results = array();
foreach ($needles as $needle) {
$results[] = strpos($haystack, $needle) !== false;
}
if (! in_array(false, $results, true)) {
Logging::setMessage(get_called_class().': Format detected => '.$parser_name);
return $this->getParserInstance($parser_name);
}
return false;
}
/**
* Discover feed format and return a parser instance
*
* @access public
* @param boolean $discover Enable feed autodiscovery in HTML document
* @return mixed False on failure or Parser instance
*/
public function getParser($discover = false)
{
$formats = array(
array('parser' => 'Atom', 'needles' => array('<feed')),
array('parser' => 'Rss20', 'needles' => array('<rss', '2.0')),
array('parser' => 'Rss92', 'needles' => array('<rss', '0.92')),
array('parser' => 'Rss91', 'needles' => array('<rss', '0.91')),
array('parser' => 'Rss10', 'needles' => array('<rdf:'/*, 'xmlns="http://purl.org/rss/1.0/"'*/)),
);
$first_tag = $this->getFirstTag($this->content);
foreach ($formats as $format) {
$parser = $this->detectFormat($format['parser'], $first_tag, $format['needles']);
if ($parser !== false) {
return $parser;
}
}
if ($discover === true) {
Logging::setMessage(get_called_class().': Format not supported or feed malformed');
Logging::setMessage(get_called_class().': Content => '.PHP_EOL.$this->content);
return false;
}
else if ($this->discover()) {
return $this->getParser(true);
}
Logging::setMessage(get_called_class().': Subscription not found');
Logging::setMessage(get_called_class().': Content => '.PHP_EOL.$this->content);
return false;
}
/**
* Discover the feed url inside a HTML document and download the feed
*
* @access public
* @return boolean
*/
public function discover()
{
if (! $this->content) {
return false;
}
Logging::setMessage(get_called_class().': Try to discover a subscription');
$dom = XmlParser::getHtmlDocument($this->content);
$xpath = new DOMXPath($dom);
$queries = array(
'//link[@type="application/rss+xml"]',
'//link[@type="application/atom+xml"]',
);
foreach ($queries as $query) {
$nodes = $xpath->query($query);
if ($nodes->length !== 0) {
$link = $nodes->item(0)->getAttribute('href');
if (! empty($link)) {
$feedUrl = new Url($link);
$siteUrl = new Url($this->url);
$link = $feedUrl->getAbsoluteUrl($feedUrl->isRelativeUrl() ? $siteUrl->getBaseUrl() : '');
Logging::setMessage(get_called_class().': Find subscription link: '.$link);
$this->download($link);
return true;
}
}
}
return false;
}
/**
* Get the downloaded content
*
* @access public
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* Set the page content
*
* @access public
* @param string $content Page content
* @return \PicoFeed\Reader
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* Get final URL
*
* @access public
* @return string
*/
public function getUrl()
{
return $this->url;
}
/**
* Set the URL
*
* @access public
* @param string $url URL
* @return \PicoFeed\Reader
*/
public function setUrl($url)
{
$this->url = $url;
return $this;
}
}

7
vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInit1f0385c01a6e1fee49b051180b96fd66::getLoader();

383
vendor/composer/ClassLoader.php vendored Normal file
View File

@ -0,0 +1,383 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0 class loader
*
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
public function getPrefixes()
{
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-0 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if ($file === null && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if ($file === null) {
// Remember that this class does not exist.
return $this->classMap[$class] = false;
}
return $file;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
if (0 === strpos($class, $prefix)) {
foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}

81
vendor/composer/autoload_classmap.php vendored Normal file
View File

@ -0,0 +1,81 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'JsonRPC\\Client' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Client.php',
'JsonRPC\\InvalidJsonFormat' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Server.php',
'JsonRPC\\InvalidJsonRpcFormat' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Server.php',
'JsonRPC\\Server' => $vendorDir . '/fguillot/json-rpc/src/JsonRPC/Server.php',
'PicoDb\\Database' => $vendorDir . '/fguillot/picodb/lib/PicoDb/Database.php',
'PicoDb\\Driver\\Mysql' => $vendorDir . '/fguillot/picodb/lib/PicoDb/Driver/Mysql.php',
'PicoDb\\Driver\\Postgres' => $vendorDir . '/fguillot/picodb/lib/PicoDb/Driver/Postgres.php',
'PicoDb\\Driver\\Sqlite' => $vendorDir . '/fguillot/picodb/lib/PicoDb/Driver/Sqlite.php',
'PicoDb\\Schema' => $vendorDir . '/fguillot/picodb/lib/PicoDb/Schema.php',
'PicoDb\\Table' => $vendorDir . '/fguillot/picodb/lib/PicoDb/Table.php',
'PicoFeed\\Client\\Client' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/Client.php',
'PicoFeed\\Client\\ClientException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/ClientException.php',
'PicoFeed\\Client\\Curl' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/Curl.php',
'PicoFeed\\Client\\Grabber' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/Grabber.php',
'PicoFeed\\Client\\HttpHeaders' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/HttpHeaders.php',
'PicoFeed\\Client\\InvalidCertificateException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/InvalidCertificateException.php',
'PicoFeed\\Client\\InvalidUrlException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/InvalidUrlException.php',
'PicoFeed\\Client\\MaxRedirectException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/MaxRedirectException.php',
'PicoFeed\\Client\\MaxSizeException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/MaxSizeException.php',
'PicoFeed\\Client\\Stream' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/Stream.php',
'PicoFeed\\Client\\TimeoutException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/TimeoutException.php',
'PicoFeed\\Client\\Url' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Client/Url.php',
'PicoFeed\\Config\\Config' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Config/Config.php',
'PicoFeed\\Encoding\\Encoding' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Encoding/Encoding.php',
'PicoFeed\\Filter\\Attribute' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Filter/Attribute.php',
'PicoFeed\\Filter\\Filter' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Filter/Filter.php',
'PicoFeed\\Filter\\Html' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Filter/Html.php',
'PicoFeed\\Filter\\Tag' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Filter/Tag.php',
'PicoFeed\\Logging\\Logger' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Logging/Logger.php',
'PicoFeed\\Parser\\Atom' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Parser/Atom.php',
'PicoFeed\\Parser\\Feed' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Parser/Feed.php',
'PicoFeed\\Parser\\Item' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Parser/Item.php',
'PicoFeed\\Parser\\MalformedXmlException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Parser/MalformedXmlException.php',
'PicoFeed\\Parser\\Parser' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Parser/Parser.php',
'PicoFeed\\Parser\\ParserException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Parser/ParserException.php',
'PicoFeed\\Parser\\Rss10' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Parser/Rss10.php',
'PicoFeed\\Parser\\Rss20' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Parser/Rss20.php',
'PicoFeed\\Parser\\Rss91' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Parser/Rss91.php',
'PicoFeed\\Parser\\Rss92' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Parser/Rss92.php',
'PicoFeed\\Parser\\XmlParser' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Parser/XmlParser.php',
'PicoFeed\\PicoFeedException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/PicoFeedException.php',
'PicoFeed\\Reader\\Favicon' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Reader/Favicon.php',
'PicoFeed\\Reader\\Reader' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Reader/Reader.php',
'PicoFeed\\Reader\\ReaderException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Reader/ReaderException.php',
'PicoFeed\\Reader\\SubscriptionNotFoundException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Reader/SubscriptionNotFoundException.php',
'PicoFeed\\Reader\\UnsupportedFeedFormatException' => $vendorDir . '/fguillot/picofeed/lib/PicoFeed/Reader/UnsupportedFeedFormatException.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',
'SimpleValidator\\Base' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Base.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',
'SimpleValidator\\Validators\\Date' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Date.php',
'SimpleValidator\\Validators\\Email' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Email.php',
'SimpleValidator\\Validators\\Equals' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Equals.php',
'SimpleValidator\\Validators\\GreaterThan' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/GreaterThan.php',
'SimpleValidator\\Validators\\InArray' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/InArray.php',
'SimpleValidator\\Validators\\Integer' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Integer.php',
'SimpleValidator\\Validators\\Ip' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Ip.php',
'SimpleValidator\\Validators\\Length' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Length.php',
'SimpleValidator\\Validators\\MacAddress' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/MacAddress.php',
'SimpleValidator\\Validators\\MaxLength' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/MaxLength.php',
'SimpleValidator\\Validators\\MinLength' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/MinLength.php',
'SimpleValidator\\Validators\\NotInArray' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/NotInArray.php',
'SimpleValidator\\Validators\\Numeric' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Numeric.php',
'SimpleValidator\\Validators\\Range' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Range.php',
'SimpleValidator\\Validators\\Required' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Required.php',
'SimpleValidator\\Validators\\Unique' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Unique.php',
'SimpleValidator\\Validators\\Version' => $vendorDir . '/fguillot/simple-validator/src/SimpleValidator/Validators/Version.php',
);

18
vendor/composer/autoload_files.php vendored Normal file
View File

@ -0,0 +1,18 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
$baseDir . '/lib/Translator.php',
$baseDir . '/models/config.php',
$baseDir . '/models/user.php',
$baseDir . '/models/feed.php',
$baseDir . '/models/item.php',
$baseDir . '/models/schema.php',
$baseDir . '/models/auto_update.php',
$baseDir . '/models/database.php',
$baseDir . '/models/remember_me.php',
);

14
vendor/composer/autoload_namespaces.php vendored Normal file
View File

@ -0,0 +1,14 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'SimpleValidator' => array($vendorDir . '/fguillot/simple-validator/src'),
'PicoFeed' => array($vendorDir . '/fguillot/picofeed/lib'),
'PicoFarad' => array($vendorDir . '/fguillot/picofarad/lib'),
'PicoDb' => array($vendorDir . '/fguillot/picodb/lib'),
'JsonRPC' => array($vendorDir . '/fguillot/json-rpc/src'),
);

9
vendor/composer/autoload_psr4.php vendored Normal file
View File

@ -0,0 +1,9 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

55
vendor/composer/autoload_real.php vendored Normal file
View File

@ -0,0 +1,55 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit1f0385c01a6e1fee49b051180b96fd66
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit1f0385c01a6e1fee49b051180b96fd66', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit1f0385c01a6e1fee49b051180b96fd66', 'loadClassLoader'));
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
$loader->register(true);
$includeFiles = require __DIR__ . '/autoload_files.php';
foreach ($includeFiles as $file) {
composerRequire1f0385c01a6e1fee49b051180b96fd66($file);
}
return $loader;
}
}
function composerRequire1f0385c01a6e1fee49b051180b96fd66($file)
{
require $file;
}

197
vendor/composer/installed.json vendored Normal file
View File

@ -0,0 +1,197 @@
[
{
"name": "fguillot/simple-validator",
"version": "dev-master",
"version_normalized": "9999999-dev",
"source": {
"type": "git",
"url": "https://github.com/fguillot/simpleValidator.git",
"reference": "3bfa1ef0062906c83824ce8db1219914996d9bd4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fguillot/simpleValidator/zipball/3bfa1ef0062906c83824ce8db1219914996d9bd4",
"reference": "3bfa1ef0062906c83824ce8db1219914996d9bd4",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"time": "2014-11-25 22:58:14",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"SimpleValidator": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frédéric Guillot",
"homepage": "http://fredericguillot.com"
}
],
"description": "The most easy to use validator library for PHP :)",
"homepage": "https://github.com/fguillot/simpleValidator"
},
{
"name": "fguillot/json-rpc",
"version": "dev-master",
"version_normalized": "9999999-dev",
"source": {
"type": "git",
"url": "https://github.com/fguillot/JsonRPC.git",
"reference": "86e8339205616ad9b09d581957cc084a99c0ed27"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fguillot/JsonRPC/zipball/86e8339205616ad9b09d581957cc084a99c0ed27",
"reference": "86e8339205616ad9b09d581957cc084a99c0ed27",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"time": "2014-11-22 20:32:14",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"JsonRPC": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Unlicense"
],
"authors": [
{
"name": "Frédéric Guillot",
"homepage": "http://fredericguillot.com"
}
],
"description": "A simple Json-RPC client/server library that just works",
"homepage": "https://github.com/fguillot/JsonRPC"
},
{
"name": "fguillot/picodb",
"version": "dev-master",
"version_normalized": "9999999-dev",
"source": {
"type": "git",
"url": "https://github.com/fguillot/picoDb.git",
"reference": "ebe721de0002b7ff86b7f66df0065224bf896eb2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fguillot/picoDb/zipball/ebe721de0002b7ff86b7f66df0065224bf896eb2",
"reference": "ebe721de0002b7ff86b7f66df0065224bf896eb2",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"time": "2014-11-22 04:15:43",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"PicoDb": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"WTFPL"
],
"authors": [
{
"name": "Frédéric Guillot",
"homepage": "http://fredericguillot.com"
}
],
"description": "Minimalist database query builder",
"homepage": "https://github.com/fguillot/picoDb"
},
{
"name": "fguillot/picofeed",
"version": "dev-master",
"version_normalized": "9999999-dev",
"source": {
"type": "git",
"url": "https://github.com/fguillot/picoFeed.git",
"reference": "11589851f91cc3f04c84ba873484486d1457e638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fguillot/picoFeed/zipball/11589851f91cc3f04c84ba873484486d1457e638",
"reference": "11589851f91cc3f04c84ba873484486d1457e638",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"time": "2014-12-22 03:23:04",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"PicoFeed": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Unlicense"
],
"authors": [
{
"name": "Frédéric Guillot",
"homepage": "http://fredericguillot.com"
}
],
"description": "Modern library to write or read feeds (RSS/Atom)",
"homepage": "http://fguillot.github.io/picoFeed"
},
{
"name": "fguillot/picofarad",
"version": "dev-master",
"version_normalized": "9999999-dev",
"source": {
"type": "git",
"url": "https://github.com/fguillot/picoFarad.git",
"reference": "df30333d5bf3b02f8f654c988c7c43305d5e6662"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fguillot/picoFarad/zipball/df30333d5bf3b02f8f654c988c7c43305d5e6662",
"reference": "df30333d5bf3b02f8f654c988c7c43305d5e6662",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"time": "2014-11-01 15:01:02",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"PicoFarad": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Unlicense"
],
"authors": [
{
"name": "Frédéric Guillot",
"homepage": "http://fredericguillot.com"
}
],
"description": "Minimalist micro-framework",
"homepage": "https://github.com/fguillot/picoFarad"
}
]

261
vendor/fguillot/json-rpc/README.markdown vendored Normal file
View File

@ -0,0 +1,261 @@
JsonRPC PHP Client and Server
=============================
A simple Json-RPC client/server that just works.
Features
--------
- JSON-RPC 2.0 protocol only
- The server support batch requests and notifications
- Authentication and IP based client restrictions
- Minimalist: there is only 2 files
- Fully unit tested
- License: Unlicense http://unlicense.org/
Requirements
------------
- The only dependency is the cURL extension
- PHP >= 5.3
Author
------
[Frédéric Guillot](http://fredericguillot.com)
Installation with Composer
--------------------------
```bash
composer require fguillot/json-rpc dev-master
```
Examples
--------
### Server
Callback binding:
```php
<?php
use JsonRPC\Server;
$server = new Server;
// Procedures registration
$server->register('addition', function ($a, $b) {
return $a + $b;
});
$server->register('random', function ($start, $end) {
return mt_rand($start, $end);
});
// Return the response to the client
echo $server->execute();
```
Class/Method binding:
```php
<?php
use JsonRPC\Server;
class Api
{
public function doSomething($arg1, $arg2 = 3)
{
return $arg1 + $arg2;
}
}
$server = new Server;
// Bind the method Api::doSomething() to the procedure myProcedure
$server->bind('myProcedure', 'Api', 'doSomething');
// Use a class instance instead of the class name
$server->bind('mySecondProcedure', new Api, 'doSomething');
echo $server->execute();
```
### Client
Example with positional parameters:
```php
<?php
use JsonRPC\Client;
$client = new Client('http://localhost/server.php');
$result = $client->execute('addition', [3, 5]);
var_dump($result);
```
Example with named arguments:
```php
<?php
require 'JsonRPC/Client.php';
use JsonRPC\Client;
$client = new Client('http://localhost/server.php');
$result = $client->execute('random', ['end' => 10, 'start' => 1]);
var_dump($result);
```
Arguments are called in the right order.
Examples with shortcut methods:
```php
<?php
use JsonRPC\Client;
$client = new Client('http://localhost/server.php');
$result = $client->random(50, 100);
var_dump($result);
```
The example above use positional arguments for the request and this one use named arguments:
```php
$result = $client->random(['end' => 10, 'start' => 1]);
```
### Client batch requests
Call several procedures in a single HTTP request:
```php
<?php
use JsonRPC\Client;
$client = new Client('http://localhost/server.php');
$results = $client->batch();
->foo(['arg1' => 'bar'])
->random(1, 100);
->add(4, 3);
->execute('add', [2, 5])
->send();
print_r($results);
```
All results are stored at the same position of the call.
### Client exceptions
- `BadFunctionCallException`: Procedure not found on the server
- `InvalidArgumentException`: Wrong procedure arguments
- `RuntimeException`: Protocol error
### Enable client debugging
You can enable the debug to see the JSON request and response:
```php
<?php
use JsonRPC\Client;
$client = new Client('http://localhost/server.php');
$client->debug = true;
```
The debug output is sent to the PHP's system logger.
You can configure the log destination in your `php.ini`.
Output example:
```json
==> Request:
{
"jsonrpc": "2.0",
"method": "removeCategory",
"id": 486782327,
"params": [
1
]
}
==> Response:
{
"jsonrpc": "2.0",
"id": 486782327,
"result": true
}
```
### IP based client restrictions
The server can allow only some IP adresses:
```php
<?php
use JsonRPC\Server;
$server = new Server;
// IP client restrictions
$server->allowHosts(['192.168.0.1', '127.0.0.1']);
// Procedures registration
[...]
// Return the response to the client
echo $server->execute();
```
If the client is blocked, you got a 403 Forbidden HTTP response.
### HTTP Basic Authentication
If you use HTTPS, you can allow client by using a username/password.
```php
<?php
use JsonRPC\Server;
$server = new Server;
// List of users to allow
$server->authentication(['jsonrpc' => 'toto']);
// Procedures registration
[...]
// Return the response to the client
echo $server->execute();
```
On the client, set credentials like that:
```php
<?php
use JsonRPC\Client;
$client = new Client('http://localhost/server.php');
$client->authentication('jsonrpc', 'toto');
```
If the authentication failed, the client throw a RuntimeException.

19
vendor/fguillot/json-rpc/composer.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
"name": "fguillot/json-rpc",
"description": "A simple Json-RPC client/server library that just works",
"homepage": "https://github.com/fguillot/JsonRPC",
"type": "library",
"license": "Unlicense",
"authors": [
{
"name": "Frédéric Guillot",
"homepage": "http://fredericguillot.com"
}
],
"require": {
"php": ">=5.3.0"
},
"autoload": {
"psr-0": {"JsonRPC": "src/"}
}
}

View File

@ -2,7 +2,9 @@
namespace JsonRPC; namespace JsonRPC;
use RuntimeException;
use BadFunctionCallException; use BadFunctionCallException;
use InvalidArgumentException;
/** /**
* JsonRPC client class * JsonRPC client class
@ -45,6 +47,22 @@ class Client
*/ */
private $password; private $password;
/**
* True for a batch request
*
* @access public
* @var boolean
*/
public $is_batch = false;
/**
* Batch payload
*
* @access public
* @var array
*/
public $batch = array();
/** /**
* Enable debug output to the php error log * Enable debug output to the php error log
* *
@ -88,8 +106,13 @@ class Client
* @param array $params Procedure arguments * @param array $params Procedure arguments
* @return mixed * @return mixed
*/ */
public function __call($method, $params) public function __call($method, array $params)
{ {
// Allow to pass an array and use named arguments
if (count($params) === 1 && is_array($params[0])) {
$params = $params[0];
}
return $this->execute($method, $params); return $this->execute($method, $params);
} }
@ -107,35 +130,144 @@ class Client
} }
/** /**
* Execute * Start a batch request
*
* @access public
* @return Client
*/
public function batch()
{
$this->is_batch = true;
$this->batch = array();
return $this;
}
/**
* Send a batch request
*
* @access public
* @return array
*/
public function send()
{
$this->is_batch = false;
return $this->parseResponse(
$this->doRequest($this->batch)
);
}
/**
* Execute a procedure
* *
* @access public * @access public
* @throws BadFunctionCallException Exception thrown when a bad request is made (missing argument/procedure)
* @param string $procedure Procedure name * @param string $procedure Procedure name
* @param array $params Procedure arguments * @param array $params Procedure arguments
* @return mixed * @return mixed
*/ */
public function execute($procedure, array $params = array()) public function execute($procedure, array $params = array())
{ {
$id = mt_rand(); if ($this->is_batch) {
$this->batch[] = $this->prepareRequest($procedure, $params);
return $this;
}
return $this->parseResponse(
$this->doRequest($this->prepareRequest($procedure, $params))
);
}
/**
* Prepare the payload
*
* @access public
* @param string $procedure Procedure name
* @param array $params Procedure arguments
* @return array
*/
public function prepareRequest($procedure, array $params = array())
{
$payload = array( $payload = array(
'jsonrpc' => '2.0', 'jsonrpc' => '2.0',
'method' => $procedure, 'method' => $procedure,
'id' => $id 'id' => mt_rand()
); );
if (! empty($params)) { if (! empty($params)) {
$payload['params'] = $params; $payload['params'] = $params;
} }
$result = $this->doRequest($payload); return $payload;
}
if (isset($result['id']) && $result['id'] == $id && array_key_exists('result', $result)) { /**
return $result['result']; * Parse the response and return the procedure result
*
* @access public
* @param array $payload
* @return mixed
*/
public function parseResponse(array $payload)
{
if ($this->isBatchResponse($payload)) {
$results = array();
foreach ($payload as $response) {
$results[] = $this->getResult($response);
}
return $results;
} }
throw new BadFunctionCallException('Bad Request'); return $this->getResult($payload);
}
/**
* Return true if we have a batch response
*
* @access public
* @param array $payload
* @return boolean
*/
private function isBatchResponse(array $payload)
{
return array_keys($payload) === range(0, count($payload) - 1);
}
/**
* Get a RPC call result
*
* @access public
* @param array $payload
* @return mixed
*/
public function getResult(array $payload)
{
if (isset($payload['error']['code'])) {
$this->handleRpcErrors($payload['error']['code']);
}
return isset($payload['result']) ? $payload['result'] : null;
}
/**
* Throw an exception according the RPC error
*
* @access public
* @param integer $code
*/
public function handleRpcErrors($code)
{
switch ($code) {
case -32601:
throw new BadFunctionCallException('Procedure not found');
case -32602:
throw new InvalidArgumentException('Invalid arguments');
default:
throw new RuntimeException('Invalid request/response');
}
} }
/** /**
@ -162,8 +294,14 @@ class Client
curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password); curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password);
} }
$result = curl_exec($ch); $http_body = curl_exec($ch);
$response = json_decode($result, true); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http_code === 401 || $http_code === 403) {
throw new RuntimeException('Access denied');
}
$response = json_decode($http_body, true);
if ($this->debug) { if ($this->debug) {
error_log('==> Request: '.PHP_EOL.json_encode($payload, JSON_PRETTY_PRINT)); error_log('==> Request: '.PHP_EOL.json_encode($payload, JSON_PRETTY_PRINT));

View File

@ -0,0 +1,447 @@
<?php
namespace JsonRPC;
use Closure;
use BadFunctionCallException;
use Exception;
use InvalidArgumentException;
use LogicException;
use ReflectionFunction;
use ReflectionMethod;
class InvalidJsonRpcFormat extends Exception {};
class InvalidJsonFormat extends Exception {};
/**
* JsonRPC server class
*
* @package JsonRPC
* @author Frederic Guillot
* @license Unlicense http://unlicense.org/
*/
class Server
{
/**
* Data received from the client
*
* @access private
* @var string
*/
private $payload;
/**
* List of procedures
*
* @static
* @access private
* @var array
*/
private $callbacks = array();
/**
* List of classes
*
* @static
* @access private
* @var array
*/
private $classes = array();
/**
* Constructor
*
* @access public
* @param string $payload Client data
* @param array $callbacks Callbacks
* @param array $classes Classes
*/
public function __construct($payload = '', array $callbacks = array(), array $classes = array())
{
$this->payload = $payload;
$this->callbacks = $callbacks;
$this->classes = $classes;
}
/**
* IP based client restrictions
*
* Return an HTTP error 403 if the client is not allowed
*
* @access public
* @param array $hosts List of hosts
*/
public function allowHosts(array $hosts) {
if (! in_array($_SERVER['REMOTE_ADDR'], $hosts)) {
header('Content-Type: application/json');
header('HTTP/1.0 403 Forbidden');
echo '{"error": "Access Forbidden"}';
exit;
}
}
/**
* HTTP Basic authentication
*
* Return an HTTP error 401 if the client is not allowed
*
* @access public
* @param array $users Map of username/password
*/
public function authentication(array $users)
{
if (! isset($_SERVER['PHP_AUTH_USER']) ||
! isset($users[$_SERVER['PHP_AUTH_USER']]) ||
$users[$_SERVER['PHP_AUTH_USER']] !== $_SERVER['PHP_AUTH_PW']) {
header('WWW-Authenticate: Basic realm="JsonRPC"');
header('Content-Type: application/json');
header('HTTP/1.0 401 Unauthorized');
echo '{"error": "Authentication failed"}';
exit;
}
}
/**
* Register a new procedure
*
* @access public
* @param string $procedure Procedure name
* @param closure $callback Callback
*/
public function register($name, Closure $callback)
{
$this->callbacks[$name] = $callback;
}
/**
* Bind a procedure to a class
*
* @access public
* @param string $procedure Procedure name
* @param mixed $class Class name or instance
* @param string $method Procedure name
*/
public function bind($procedure, $class, $method)
{
$this->classes[$procedure] = array($class, $method);
}
/**
* Return the response to the client
*
* @access public
* @param array $data Data to send to the client
* @param array $payload Incoming data
* @return string
*/
public function getResponse(array $data, array $payload = array())
{
if (! array_key_exists('id', $payload)) {
return '';
}
$response = array(
'jsonrpc' => '2.0',
'id' => $payload['id']
);
$response = array_merge($response, $data);
@header('Content-Type: application/json');
return json_encode($response);
}
/**
* Parse the payload and test if the parsed JSON is ok
*
* @access public
*/
public function checkJsonFormat()
{
if (empty($this->payload)) {
$this->payload = file_get_contents('php://input');
}
if (is_string($this->payload)) {
$this->payload = json_decode($this->payload, true);
}
if (! is_array($this->payload)) {
throw new InvalidJsonFormat('Malformed payload');
}
}
/**
* Test if all required JSON-RPC parameters are here
*
* @access public
*/
public function checkRpcFormat()
{
if (! isset($this->payload['jsonrpc']) ||
! isset($this->payload['method']) ||
! is_string($this->payload['method']) ||
$this->payload['jsonrpc'] !== '2.0' ||
(isset($this->payload['params']) && ! is_array($this->payload['params']))) {
throw new InvalidJsonRpcFormat('Invalid JSON RPC payload');
}
}
/**
* Return true if we have a batch request
*
* @access public
* @return boolean
*/
private function isBatchRequest()
{
return array_keys($this->payload) === range(0, count($this->payload) - 1);
}
/**
* Handle batch request
*
* @access private
* @return string
*/
private function handleBatchRequest()
{
$responses = array();
foreach ($this->payload as $payload) {
if (! is_array($payload)) {
$responses[] = $this->getResponse(array(
'error' => array(
'code' => -32600,
'message' => 'Invalid Request'
)),
array('id' => null)
);
}
else {
$server = new Server($payload, $this->callbacks, $this->classes);
$response = $server->execute();
if ($response) {
$responses[] = $response;
}
}
}
return empty($responses) ? '' : '['.implode(',', $responses).']';
}
/**
* Parse incoming requests
*
* @access public
* @return string
*/
public function execute()
{
try {
$this->checkJsonFormat();
if ($this->isBatchRequest()){
return $this->handleBatchRequest();
}
$this->checkRpcFormat();
$result = $this->executeProcedure(
$this->payload['method'],
empty($this->payload['params']) ? array() : $this->payload['params']
);
return $this->getResponse(array('result' => $result), $this->payload);
}
catch (InvalidJsonFormat $e) {
return $this->getResponse(array(
'error' => array(
'code' => -32700,
'message' => 'Parse error'
)),
array('id' => null)
);
}
catch (InvalidJsonRpcFormat $e) {
return $this->getResponse(array(
'error' => array(
'code' => -32600,
'message' => 'Invalid Request'
)),
array('id' => null)
);
}
catch (BadFunctionCallException $e) {
return $this->getResponse(array(
'error' => array(
'code' => -32601,
'message' => 'Method not found'
)),
$this->payload
);
}
catch (InvalidArgumentException $e) {
return $this->getResponse(array(
'error' => array(
'code' => -32602,
'message' => 'Invalid params'
)),
$this->payload
);
}
}
/**
* Execute the procedure
*
* @access public
* @param string $procedure Procedure name
* @param array $params Procedure params
* @return mixed
*/
public function executeProcedure($procedure, array $params = array())
{
if (isset($this->callbacks[$procedure])) {
return $this->executeCallback($this->callbacks[$procedure], $params);
}
else if (isset($this->classes[$procedure])) {
return $this->executeMethod($this->classes[$procedure][0], $this->classes[$procedure][1], $params);
}
throw new BadFunctionCallException('Unable to find the procedure');
}
/**
* Execute a callback
*
* @access public
* @param Closure $callback Callback
* @param array $params Procedure params
* @return mixed
*/
public function executeCallback(Closure $callback, $params)
{
$reflection = new ReflectionFunction($callback);
$arguments = $this->getArguments(
$params,
$reflection->getParameters(),
$reflection->getNumberOfRequiredParameters(),
$reflection->getNumberOfParameters()
);
return $reflection->invokeArgs($arguments);
}
/**
* Execute a method
*
* @access public
* @param mixed $class Class name or instance
* @param string $method Method name
* @param array $params Procedure params
* @return mixed
*/
public function executeMethod($class, $method, $params)
{
$reflection = new ReflectionMethod($class, $method);
$arguments = $this->getArguments(
$params,
$reflection->getParameters(),
$reflection->getNumberOfRequiredParameters(),
$reflection->getNumberOfParameters()
);
return $reflection->invokeArgs(
is_string($class) ? new $class : $class,
$arguments
);
}
/**
* Get procedure arguments
*
* @access public
* @param array $request_params Incoming arguments
* @param array $method_params Procedure arguments
* @param integer $nb_required_params Number of required parameters
* @param integer $nb_max_params Maximum number of parameters
* @return array
*/
public function getArguments(array $request_params, array $method_params, $nb_required_params, $nb_max_params)
{
$nb_params = count($request_params);
if ($nb_params < $nb_required_params) {
throw new InvalidArgumentException('Wrong number of arguments');
}
if ($nb_params > $nb_max_params) {
throw new InvalidArgumentException('Too many arguments');
}
if ($this->isPositionalArguments($request_params, $method_params)) {
return $request_params;
}
return $this->getNamedArguments($request_params, $method_params);
}
/**
* Return true if we have positional parametes
*
* @access public
* @param array $request_params Incoming arguments
* @param array $method_params Procedure arguments
* @return bool
*/
public function isPositionalArguments(array $request_params, array $method_params)
{
return array_keys($request_params) === range(0, count($request_params) - 1);
}
/**
* Get named arguments
*
* @access public
* @param array $request_params Incoming arguments
* @param array $method_params Procedure arguments
* @return array
*/
public function getNamedArguments(array $request_params, array $method_params)
{
$params = array();
foreach ($method_params as $p) {
$name = $p->getName();
if (isset($request_params[$name])) {
$params[$name] = $request_params[$name];
}
else if ($p->isDefaultValueAvailable()) {
$params[$name] = $p->getDefaultValue();
}
else {
throw new InvalidArgumentException('Missing argument: '.$name);
}
}
return $params;
}
}

View File

@ -0,0 +1,114 @@
<?php
require_once 'src/JsonRPC/Client.php';
use JsonRPC\Client;
class ClientTest extends PHPUnit_Framework_TestCase
{
public function testParseReponse()
{
$client = new Client('http://localhost/');
$this->assertEquals(
-19,
$client->parseResponse(json_decode('{"jsonrpc": "2.0", "result": -19, "id": 1}', true))
);
$this->assertEquals(
null,
$client->parseResponse(json_decode('{"jsonrpc": "2.0", "id": 1}', true))
);
}
/**
* @expectedException BadFunctionCallException
*/
public function testBadProcedure()
{
$client = new Client('http://localhost/');
$client->parseResponse(json_decode('{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}', true));
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidArgs()
{
$client = new Client('http://localhost/');
$client->parseResponse(json_decode('{"jsonrpc": "2.0", "error": {"code": -32602, "message": "Invalid params"}, "id": "1"}', true));
}
/**
* @expectedException RuntimeException
*/
public function testInvalidRequest()
{
$client = new Client('http://localhost/');
$client->parseResponse(json_decode('{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}', true));
}
/**
* @expectedException RuntimeException
*/
public function testParseError()
{
$client = new Client('http://localhost/');
$client->parseResponse(json_decode('{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}', true));
}
public function testPrepareRequest()
{
$client = new Client('http://localhost/');
$payload = $client->prepareRequest('myProcedure');
$this->assertNotEmpty($payload);
$this->assertArrayHasKey('jsonrpc', $payload);
$this->assertEquals('2.0', $payload['jsonrpc']);
$this->assertArrayHasKey('method', $payload);
$this->assertEquals('myProcedure', $payload['method']);
$this->assertArrayHasKey('id', $payload);
$this->assertArrayNotHasKey('params', $payload);
$payload = $client->prepareRequest('myProcedure', array('p1' => 3));
$this->assertNotEmpty($payload);
$this->assertArrayHasKey('jsonrpc', $payload);
$this->assertEquals('2.0', $payload['jsonrpc']);
$this->assertArrayHasKey('method', $payload);
$this->assertEquals('myProcedure', $payload['method']);
$this->assertArrayHasKey('id', $payload);
$this->assertArrayHasKey('params', $payload);
$this->assertEquals(array('p1' => 3), $payload['params']);
}
public function testBatchRequest()
{
$client = new Client('http://localhost/');
$batch = $client->batch();
$this->assertInstanceOf('JsonRpc\Client', $batch);
$this->assertTrue($client->is_batch);
$batch->random(1, 30);
$batch->add(3, 5);
$batch->execute('foo', array('p1' => 42, 'p3' => 3));
$this->assertNotEmpty($client->batch);
$this->assertEquals(3, count($client->batch));
$this->assertEquals('random', $client->batch[0]['method']);
$this->assertEquals('add', $client->batch[1]['method']);
$this->assertEquals('foo', $client->batch[2]['method']);
$this->assertEquals(array(1, 30), $client->batch[0]['params']);
$this->assertEquals(array(3, 5), $client->batch[1]['params']);
$this->assertEquals(array('p1' => 42, 'p3' => 3), $client->batch[2]['params']);
$batch = $client->batch();
$this->assertInstanceOf('JsonRpc\Client', $batch);
$this->assertTrue($client->is_batch);
$this->assertEmpty($client->batch);
}
}

View File

@ -0,0 +1,142 @@
<?php
require_once 'src/JsonRPC/Server.php';
use JsonRPC\Server;
class A
{
public function getAll($p1, $p2, $p3 = 4)
{
return $p1 + $p2 + $p3;
}
}
class B
{
public function getAll($p1)
{
return $p1 + 2;
}
}
class ServerProcedureTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException BadFunctionCallException
*/
public function testProcedureNotFound()
{
$server = new Server;
$server->executeProcedure('a');
}
/**
* @expectedException BadFunctionCallException
*/
public function testCallbackNotFound()
{
$server = new Server;
$server->register('b', function() {});
$server->executeProcedure('a');
}
/**
* @expectedException ReflectionException
*/
public function testClassNotFound()
{
$server = new Server;
$server->bind('getAllTasks', 'c', 'getAll');
$server->executeProcedure('getAllTasks');
}
/**
* @expectedException ReflectionException
*/
public function testMethodNotFound()
{
$server = new Server;
$server->bind('getAllTasks', 'A', 'getNothing');
$server->executeProcedure('getAllTasks');
}
public function testIsPositionalArguments()
{
$server = new Server;
$this->assertFalse($server->isPositionalArguments(
array('a' => 'b', 'c' => 'd'),
array('a' => 'b', 'c' => 'd')
));
$server = new Server;
$this->assertTrue($server->isPositionalArguments(
array('a', 'b', 'c'),
array('a' => 'b', 'c' => 'd')
));
}
public function testBindNamedArguments()
{
$server = new Server;
$server->bind('getAllA', 'A', 'getAll');
$server->bind('getAllB', 'B', 'getAll');
$server->bind('getAllC', new B, 'getAll');
$this->assertEquals(6, $server->executeProcedure('getAllA', array('p2' => 4, 'p1' => -2)));
$this->assertEquals(10, $server->executeProcedure('getAllA', array('p2' => 4, 'p3' => 8, 'p1' => -2)));
$this->assertEquals(6, $server->executeProcedure('getAllB', array('p1' => 4)));
$this->assertEquals(5, $server->executeProcedure('getAllC', array('p1' => 3)));
}
public function testBindPositionalArguments()
{
$server = new Server;
$server->bind('getAllA', 'A', 'getAll');
$server->bind('getAllB', 'B', 'getAll');
$this->assertEquals(6, $server->executeProcedure('getAllA', array(4, -2)));
$this->assertEquals(2, $server->executeProcedure('getAllA', array(4, 0, -2)));
$this->assertEquals(4, $server->executeProcedure('getAllB', array(2)));
}
public function testRegisterNamedArguments()
{
$server = new Server;
$server->register('getAllA', function($p1, $p2, $p3 = 4) {
return $p1 + $p2 + $p3;
});
$this->assertEquals(6, $server->executeProcedure('getAllA', array('p2' => 4, 'p1' => -2)));
$this->assertEquals(10, $server->executeProcedure('getAllA', array('p2' => 4, 'p3' => 8, 'p1' => -2)));
}
public function testRegisterPositionalArguments()
{
$server = new Server;
$server->register('getAllA', function($p1, $p2, $p3 = 4) {
return $p1 + $p2 + $p3;
});
$this->assertEquals(6, $server->executeProcedure('getAllA', array(4, -2)));
$this->assertEquals(2, $server->executeProcedure('getAllA', array(4, 0, -2)));
}
/**
* @expectedException InvalidArgumentException
*/
public function testTooManyArguments()
{
$server = new Server;
$server->bind('getAllC', new B, 'getAll');
$server->executeProcedure('getAllC', array('p1' => 3, 'p2' => 5));
}
/**
* @expectedException InvalidArgumentException
*/
public function testNotEnoughArguments()
{
$server = new Server;
$server->bind('getAllC', new B, 'getAll');
$server->executeProcedure('getAllC');
}
}

View File

@ -0,0 +1,217 @@
<?php
require_once 'src/JsonRPC/Server.php';
use JsonRPC\Server;
class ServerProtocolTest extends PHPUnit_Framework_TestCase
{
public function testPositionalParameters()
{
$subtract = function($minuend, $subtrahend) {
return $minuend - $subtrahend;
};
$server = new Server('{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}');
$server->register('subtract', $subtract);
$this->assertEquals(
json_decode('{"jsonrpc": "2.0", "result": 19, "id": 1}', true),
json_decode($server->execute(), true)
);
$server = new Server('{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 1}');
$server->register('subtract', $subtract);
$this->assertEquals(
json_decode('{"jsonrpc": "2.0", "result": -19, "id": 1}', true),
json_decode($server->execute(), true)
);
}
public function testNamedParameters()
{
$subtract = function($minuend, $subtrahend) {
return $minuend - $subtrahend;
};
$server = new Server('{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}');
$server->register('subtract', $subtract);
$this->assertEquals(
json_decode('{"jsonrpc": "2.0", "result": 19, "id": 3}', true),
json_decode($server->execute(), true)
);
$server = new Server('{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}');
$server->register('subtract', $subtract);
$this->assertEquals(
json_decode('{"jsonrpc": "2.0", "result": 19, "id": 4}', true),
json_decode($server->execute(), true)
);
}
public function testNotification()
{
$update = function($p1, $p2, $p3, $p4, $p5) {};
$foobar = function() {};
$server = new Server('{"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}');
$server->register('update', $update);
$server->register('foobar', $foobar);
$this->assertEquals('', $server->execute());
$server = new Server('{"jsonrpc": "2.0", "method": "foobar"}');
$server->register('update', $update);
$server->register('foobar', $foobar);
$this->assertEquals('', $server->execute());
}
public function testNoMethod()
{
$server = new Server('{"jsonrpc": "2.0", "method": "foobar", "id": "1"}');
$this->assertEquals(
json_decode('{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}', true),
json_decode($server->execute(), true)
);
}
public function testInvalidJson()
{
$server = new Server('{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]');
$this->assertEquals(
json_decode('{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}', true),
json_decode($server->execute(), true)
);
}
public function testInvalidRequest()
{
$server = new Server('{"jsonrpc": "2.0", "method": 1, "params": "bar"}');
$this->assertEquals(
json_decode('{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}', true),
json_decode($server->execute(), true)
);
}
public function testBatchInvalidJson()
{
$server = new Server('[
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
{"jsonrpc": "2.0", "method"
]');
$this->assertEquals(
json_decode('{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}', true),
json_decode($server->execute(), true)
);
}
public function testBatchEmptyArray()
{
$server = new Server('[]');
$this->assertEquals(
json_decode('{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}', true),
json_decode($server->execute(), true)
);
}
public function testBatchNotEmptyButInvalid()
{
$server = new Server('[1]');
$this->assertEquals(
json_decode('[{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}]', true),
json_decode($server->execute(), true)
);
}
public function testBatchInvalid()
{
$server = new Server('[1,2,3]');
$this->assertEquals(
json_decode('[
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
]', true),
json_decode($server->execute(), true)
);
}
public function testBatchOk()
{
$server = new Server('[
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
{"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
{"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
{"foo": "boo"},
{"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
{"jsonrpc": "2.0", "method": "get_data", "id": "9"}
]');
$server->register('sum', function($a, $b, $c) {
return $a + $b + $c;
});
$server->register('subtract', function($minuend, $subtrahend) {
return $minuend - $subtrahend;
});
$server->register('get_data', function() {
return array('hello', 5);
});
$response = $server->execute();
$this->assertEquals(
json_decode('[
{"jsonrpc": "2.0", "result": 7, "id": "1"},
{"jsonrpc": "2.0", "result": 19, "id": "2"},
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
{"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
]', true),
json_decode($response, true)
);
}
public function testBatchNotifications()
{
$server = new Server('[
{"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]},
{"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}
]');
$server->register('notify_sum', function($a, $b, $c) {
});
$server->register('notify_hello', function($id) {
});
$this->assertEquals('', $server->execute());
}
}

42
vendor/fguillot/picodb/.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Logs and databases #
######################
*.log
*.sql
*.sqlite
# OS generated files #
######################
.DS_Store
.DS_Store?
ehthumbs.db
Icon?
Thumbs.db
*.swp
*~
*.lock
# App specific #
################
example.php

275
vendor/fguillot/picodb/README.md vendored Normal file
View File

@ -0,0 +1,275 @@
PicoDb
======
PicoDb is a minimalist database query builder for PHP
**It's not an ORM**.
Features
--------
- No dependency
- Easy to use, fast and very lightweight
- Use prepared statements
- Handle schema versions (migrations)
- License: [WTFPL](http://www.wtfpl.net)
Requirements
------------
- PHP >= 5.3
- PDO
- A database: Sqlite, Mysql or Postgresql
Todo
----
- Add driver for Postgresql
- Add support for Distinct...
Documentation
-------------
## Connect to your database
use PicoDb\Database;
// Sqlite driver
$db = new Database(['driver' => 'sqlite', 'filename' => ':memory:']);
// Mysql driver
// Optional options: "schema_table" (the default table name is "schema_version")
$db = new Database(array(
'driver' => 'mysql',
'hostname' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'my_db_name',
'charset' => 'utf8',
));
## Execute a SQL request
$db->execute('CREATE TABLE toto (column1 TEXT)');
## Insert some data
$db->table('toto')->save(['column1' => 'hey']);
## Transations
$db->transaction(function($db) {
$db->table('toto')->save(['column1' => 'foo']);
$db->table('toto')->save(['column1' => 'bar']);
});
## Fetch all data
$records = $db->table('toto')->findAll();
foreach ($records as $record) {
var_dump($record['column1']);
}
## Update something
$db->table('toto')->eq('id', 1)->save(['column1' => 'hey']);
You just need to add a condition to perform an update.
## Remove rows
$db->table('toto')->lowerThan('column1', 10)->remove();
## Sorting
$db->table('toto')->asc('column1')->findAll();
or
$db->table('toto')->desc('column1')->findAll();
## Limit and offset
$db->table('toto')->limit(10)->offset(5)->findAll();
## Fetch only some columns
$db->table('toto')->columns('column1', 'column2')->findAll();
## Conditions
### Equals condition
$db->table('toto')
->equals('column1', 'hey')
->findAll();
or
$db->table('toto')
->eq('column1', 'hey')
->findAll();
Yout got: 'SELECT * FROM toto WHERE column1=?'
### IN condition
$db->table('toto')
->in('column1', ['hey', 'bla'])
->findAll();
### Like condition
$db->table('toto')
->like('column1', '%hey%')
->findAll();
### Lower than
$db->table('toto')
->lowerThan('column1', 2)
->findAll();
or
$db->table('toto')
->lt('column1', 2)
->findAll();
### Lower than or equals
$db->table('toto')
->lowerThanOrEquals('column1', 2)
->findAll();
or
$db->table('toto')
->lte('column1', 2)
->findAll();
### Greater than
$db->table('toto')
->greaterThan('column1', 3)
->findAll();
or
$db->table('toto')
->gt('column1', 3)
->findAll();
### Greater than or equals
$db->table('toto')
->greaterThanOrEquals('column1', 3)
->findAll();
or
$db->table('toto')
->gte('column1', 3)
->findAll();
### Multiple conditions
Each condition is joined by a AND.
$db->table('toto')
->like('column2', '%toto')
->gte('column1', 3)
->findAll();
How to make a OR condition:
$db->table('toto')
->beginOr()
->like('column2', '%toto')
->gte('column1', 3)
->closeOr()
->eq('column5', 'titi')
->findAll();
## Schema migrations
### Define a migration
- Migrations are defined in simple functions inside a namespace named "Schema".
- An instance of PDO is passed to first argument of the function.
- Function names has the version number at the end.
Example:
namespace Schema;
function version_1($pdo)
{
$pdo->exec('
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE,
email TEXT UNIQUE,
password TEXT
)
');
}
function version_2($pdo)
{
$pdo->exec('
CREATE TABLE tags (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE
)
');
}
### Run schema update automatically
- The method "check()" executes all migrations until to reach the correct version number.
- If we are already on the last version nothing will happen.
- The schema version for the driver Sqlite is stored inside a variable (PRAGMA user_version)
- You can use that with a dependency injection controller.
Example:
$last_schema_version = 5;
$db = new PicoDb\Database(array(
'driver' => 'sqlite',
'filename' => '/tmp/mydb.sqlite'
));
if ($db->schema()->check($last_schema_version)) {
// Do something...
}
else {
die('Unable to migrate database schema.');
}
### Use a singleton to handle database instances
Setup a new instance:
PicoDb\Database::bootstrap('myinstance', function() {
$db = new PicoDb\Database(array(
'driver' => 'sqlite',
'filename' => DB_FILENAME
));
if ($db->schema()->check(DB_VERSION)) {
return $db;
}
else {
die('Unable to migrate database schema.');
}
});
Get this instance anywhere in your code:
PicoDb\Database::get('myinstance')->table(...)

19
vendor/fguillot/picodb/composer.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
"name": "fguillot/picodb",
"description": "Minimalist database query builder",
"homepage": "https://github.com/fguillot/picoDb",
"type": "library",
"license": "WTFPL",
"authors": [
{
"name": "Frédéric Guillot",
"homepage": "http://fredericguillot.com"
}
],
"require": {
"php": ">=5.3.0"
},
"autoload": {
"psr-0": {"PicoDb": "lib/"}
}
}

View File

@ -0,0 +1,292 @@
<?php
namespace PicoDb;
use Closure;
use PDO;
use PDOException;
use LogicException;
use Picodb\Driver\Sqlite;
use Picodb\Driver\Mysql;
use Picodb\Driver\Postgres;
class Database
{
/**
* Database instances
*
* @access private
* @static
* @var array
*/
private static $instances = array();
/**
* Queries logs
*
* @access private
* @var array
*/
private $logs = array();
/**
* PDO instance
*
* @access private
* @var PDO
*/
private $pdo;
/**
* Constructor, iniatlize a PDO driver
*
* @access public
* @param array $settings Connection settings
*/
public function __construct(array $settings)
{
if (! isset($settings['driver'])) {
throw new LogicException('You must define a database driver.');
}
switch ($settings['driver']) {
case 'sqlite':
require_once __DIR__.'/Driver/Sqlite.php';
$this->pdo = new Sqlite($settings);
break;
case 'mysql':
require_once __DIR__.'/Driver/Mysql.php';
$this->pdo = new Mysql($settings);
break;
case 'postgres':
require_once __DIR__.'/Driver/Postgres.php';
$this->pdo = new Postgres($settings);
break;
default:
throw new LogicException('This database driver is not supported.');
}
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
/**
* Destructor
*
* @access public
*/
public function __destruct()
{
$this->closeConnection();
}
/**
* Register a new database instance
*
* @static
* @access public
* @param string $name Instance name
* @param Closure $callback Callback
*/
public static function bootstrap($name, Closure $callback)
{
self::$instances[$name] = $callback;
}
/**
* Get a database instance
*
* @static
* @access public
* @param string $name Instance name
* @return Database
*/
public static function get($name)
{
if (! isset(self::$instances[$name])) {
throw new LogicException('No database instance created with that name.');
}
if (is_callable(self::$instances[$name])) {
self::$instances[$name] = call_user_func(self::$instances[$name]);
}
return self::$instances[$name];
}
/**
* Add a log message
*
* @access public
* @param string $message Message
*/
public function setLogMessage($message)
{
$this->logs[] = $message;
}
/**
* Get all queries logs
*
* @access public
* @return array
*/
public function getLogMessages()
{
return $this->logs;
}
/**
* Get the PDO connection
*
* @access public
* @return PDO
*/
public function getConnection()
{
return $this->pdo;
}
/**
* Release the PDO connection
*
* @access public
*/
public function closeConnection()
{
$this->pdo = null;
}
/**
* Escape an identifier (column, table name...)
*
* @access public
* @param string $value Value
* @return string
*/
public function escapeIdentifier($value)
{
// Do not escape custom query
if (strpos($value, '.') !== false || strpos($value, ' ') !== false) {
return $value;
}
return $this->pdo->escapeIdentifier($value);
}
/**
* Execute a prepared statement
*
* @access public
* @param string $sql SQL query
* @param array $values Values
* @return PDOStatement
*/
public function execute($sql, array $values = array())
{
try {
$this->setLogMessage($sql);
$rq = $this->pdo->prepare($sql);
$rq->execute($values);
return $rq;
}
catch (PDOException $e) {
$this->setLogMessage($e->getMessage());
return false;
}
}
/**
* Run a transaction
*
* @access public
* @param Closure $callback Callback
* @return mixed
*/
public function transaction(Closure $callback)
{
try {
$this->pdo->beginTransaction();
$result = $callback($this);
if ($result === false) {
$this->pdo->rollback();
}
else {
$this->pdo->commit();
}
}
catch (PDOException $e) {
$this->pdo->rollback();
$this->setLogMessage($e->getMessage());
$result = false;
}
return $result === null ? true : $result;
}
/**
* Begin a transaction
*
* @access public
*/
public function startTransaction()
{
if (! $this->pdo->inTransaction()) {
$this->pdo->beginTransaction();
}
}
/**
* Commit a transaction
*
* @access public
*/
public function closeTransaction()
{
if ($this->pdo->inTransaction()) {
$this->pdo->commit();
}
}
/**
* Rollback a transaction
*
* @access public
*/
public function cancelTransaction()
{
if ($this->pdo->inTransaction()) {
$this->pdo->rollback();
}
}
/**
* Get a table instance
*
* @access public
* @return Picodb\Table
*/
public function table($table_name)
{
require_once __DIR__.'/Table.php';
return new Table($this, $table_name);
}
/**
* Get a schema instance
*
* @access public
* @return Picodb\Schema
*/
public function schema()
{
require_once __DIR__.'/Schema.php';
return new Schema($this);
}
}

View File

@ -1,12 +1,14 @@
<?php <?php
namespace PicoDb; namespace PicoDb\Driver;
class Mysql extends \PDO { use PDO;
use LogicException;
class Mysql extends PDO
{
private $schema_table = 'schema_version'; private $schema_table = 'schema_version';
public function __construct(array $settings) public function __construct(array $settings)
{ {
$required_atttributes = array( $required_atttributes = array(
@ -19,13 +21,14 @@ class Mysql extends \PDO {
foreach ($required_atttributes as $attribute) { foreach ($required_atttributes as $attribute) {
if (! isset($settings[$attribute])) { if (! isset($settings[$attribute])) {
throw new \LogicException('This configuration parameter is missing: "'.$attribute.'"'); throw new LogicException('This configuration parameter is missing: "'.$attribute.'"');
} }
} }
$dsn = 'mysql:host='.$settings['hostname'].';dbname='.$settings['database']; $dsn = 'mysql:host='.$settings['hostname'].';dbname='.$settings['database'].';charset='.$settings['charset'];
$options = array( $options = array(
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES '.$settings['charset'] PDO::MYSQL_ATTR_INIT_COMMAND => 'SET sql_mode = STRICT_ALL_TABLES',
); );
parent::__construct($dsn, $settings['username'], $settings['password'], $options); parent::__construct($dsn, $settings['username'], $settings['password'], $options);
@ -42,7 +45,7 @@ class Mysql extends \PDO {
$rq = $this->prepare('SELECT `version` FROM `'.$this->schema_table.'`'); $rq = $this->prepare('SELECT `version` FROM `'.$this->schema_table.'`');
$rq->execute(); $rq->execute();
$result = $rq->fetch(\PDO::FETCH_ASSOC); $result = $rq->fetch(PDO::FETCH_ASSOC);
if (isset($result['version'])) { if (isset($result['version'])) {
return (int) $result['version']; return (int) $result['version'];

View File

@ -1,9 +1,12 @@
<?php <?php
namespace PicoDb; namespace PicoDb\Driver;
class Postgres extends \PDO { use PDO;
use LogicException;
class Postgres extends PDO
{
private $schema_table = 'schema_version'; private $schema_table = 'schema_version';
@ -18,7 +21,7 @@ class Postgres extends \PDO {
foreach ($required_atttributes as $attribute) { foreach ($required_atttributes as $attribute) {
if (! isset($settings[$attribute])) { if (! isset($settings[$attribute])) {
throw new \LogicException('This configuration parameter is missing: "'.$attribute.'"'); throw new LogicException('This configuration parameter is missing: "'.$attribute.'"');
} }
} }
@ -38,7 +41,7 @@ class Postgres extends \PDO {
$rq = $this->prepare('SELECT version FROM '.$this->schema_table.''); $rq = $this->prepare('SELECT version FROM '.$this->schema_table.'');
$rq->execute(); $rq->execute();
$result = $rq->fetch(\PDO::FETCH_ASSOC); $result = $rq->fetch(PDO::FETCH_ASSOC);
if (isset($result['version'])) { if (isset($result['version'])) {
return (int) $result['version']; return (int) $result['version'];

View File

@ -1,10 +1,12 @@
<?php <?php
namespace PicoDb; namespace PicoDb\Driver;
class Sqlite extends \PDO {
use PDO;
use LogicException;
class Sqlite extends PDO
{
public function __construct(array $settings) public function __construct(array $settings)
{ {
$required_atttributes = array( $required_atttributes = array(
@ -13,7 +15,7 @@ class Sqlite extends \PDO {
foreach ($required_atttributes as $attribute) { foreach ($required_atttributes as $attribute) {
if (! isset($settings[$attribute])) { if (! isset($settings[$attribute])) {
throw new \LogicException('This configuration parameter is missing: "'.$attribute.'"'); throw new LogicException('This configuration parameter is missing: "'.$attribute.'"');
} }
} }
@ -27,7 +29,7 @@ class Sqlite extends \PDO {
{ {
$rq = $this->prepare('PRAGMA user_version'); $rq = $this->prepare('PRAGMA user_version');
$rq->execute(); $rq->execute();
$result = $rq->fetch(\PDO::FETCH_ASSOC); $result = $rq->fetch(PDO::FETCH_ASSOC);
if (isset($result['user_version'])) { if (isset($result['user_version'])) {
return (int) $result['user_version']; return (int) $result['user_version'];

View File

@ -2,17 +2,17 @@
namespace PicoDb; namespace PicoDb;
use PDOException;
class Schema class Schema
{ {
protected $db = null; protected $db = null;
public function __construct(Database $db) public function __construct(Database $db)
{ {
$this->db = $db; $this->db = $db;
} }
public function check($last_version = 1) public function check($last_version = 1)
{ {
$current_version = $this->db->getConnection()->getSchemaVersion(); $current_version = $this->db->getConnection()->getSchemaVersion();
@ -24,7 +24,6 @@ class Schema
return true; return true;
} }
public function migrateTo($current_version, $next_version) public function migrateTo($current_version, $next_version)
{ {
try { try {
@ -43,7 +42,7 @@ class Schema
$this->db->closeTransaction(); $this->db->closeTransaction();
} }
catch (\PDOException $e) { catch (PDOException $e) {
$this->db->setLogMessage($function_name.' => '.$e->getMessage()); $this->db->setLogMessage($function_name.' => '.$e->getMessage());
$this->db->cancelTransaction(); $this->db->cancelTransaction();
return false; return false;

View File

@ -207,13 +207,13 @@ class Table
} }
public function join($table, $foreign_column, $local_column) public function join($table, $foreign_column, $local_column, $local_table = null)
{ {
$this->joins[] = sprintf( $this->joins[] = sprintf(
'LEFT JOIN %s ON %s=%s', 'LEFT JOIN %s ON %s=%s',
$this->db->escapeIdentifier($table), $this->db->escapeIdentifier($table),
$this->db->escapeIdentifier($table).'.'.$this->db->escapeIdentifier($foreign_column), $this->db->escapeIdentifier($table).'.'.$this->db->escapeIdentifier($foreign_column),
$this->db->escapeIdentifier($this->table_name).'.'.$this->db->escapeIdentifier($local_column) $this->db->escapeIdentifier($local_table ?: $this->table_name).'.'.$this->db->escapeIdentifier($local_column)
); );
return $this; return $this;

38
vendor/fguillot/picofarad/.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
# Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Logs and databases #
######################
*.log
*.sql
*.sqlite
# OS generated files #
######################
.DS_Store
.DS_Store?
ehthumbs.db
Icon?
Thumbs.db
*.swp
*~
*.lock

306
vendor/fguillot/picofarad/README.md vendored Normal file
View File

@ -0,0 +1,306 @@
PicoFarad
=========
PicoFarad is a minimalist micro-framework for PHP.
Perfect to build a REST API or a small webapp.
Features
--------
- No dependency
- Easy to use, fast and very lightweight
- Only 4 files: Request, Response, Router and Session
- License: Do what the fuck you want with that
Requirements
------------
- PHP >= 5.3
Router
------
### Example for a single file webapp:
```php
<?php
use PicoFarad\Router;
use PicoFarad\Response;
use PicoFarad\Request;
use PicoFarad\Session;
// Called before each action
Router\before(function($action) {
// Open a session only for the specified directory
Session\open(dirname($_SERVER['PHP_SELF']));
// HTTP secure headers
Response\csp();
Response\xframe();
Response\xss();
Response\nosniff();
});
// GET ?action=show-help
Router\get_action('show-help', function() {
Response\text('help me!');
});
// POST ?action=hello (with a form value "name")
Router\post_action('show-help', function() {
Response\text('Hello '.Request\value('name'));
});
// Default action executed
Router\notfound(function() {
Response\text('Sorry, page not found!');
})
```
### Split your webapp in different files:
```php
<?php
use PicoFarad\Router;
use PicoFarad\Response;
// Include automatically those files:
// __DIR__.'/controllers/controller1.php'
// __DIR__.'/controllers/controller2.php'
Router\bootstrap(__DIR__.'/controllers', 'controller1', 'controller2');
// Page not found
Router\notfound(function() {
Response\redirect('?action=unread');
});
```
### Example for a REST API:
```php
<?php
// POST /foo
Router\post('/foo', function() {
$values = Request\values();
...
Response\json(['result' => true], 201);
});
// GET /foo/123
Router\get('/foo/:id', function($id) {
Response\json(['result' => true]);
});
// PUT /foo/123
Router\put('/foo/:id', function($id) {
$values = Request\values();
...
Response\json(['result' => true]);
});
// DELETE /foo/123
Router\delete('/foo/:id', function($id) {
Response\json(['result' => true]);
});
```
Response
--------
### Send a JSON response
```php
<?php
use PicoFarad\Response;
$data = array(....);
// Output the encoded JSON data with a HTTP status 200 Ok
Response\json($data);
// Change the default HTTP status code by a 400 Bad Request
Response\json($data, 400);
```
### Send text response
```php
Response\text('my text data');
```
### Send HTML response
```php
Response\html('<html...>');
```
### Send XML response
```php
Response\xml('<xml ... >');
```
### Send a binary response
```php
Response\binary($my_file_content);
```
### Force browser download
```php
Response\force_download('The name of the ouput file');
```
### Modify the HTTP status code
```php
Response\status(403);
```
### Temporary redirection
```php
Response\redirect('http://....');
```
### Permanent redirection
```php
Response\redirect('http://....', 301);
```
### Secure headers
```php
// Send the header X-Content-Type-Options: nosniff
Response\nosniff();
// Send the header X-XSS-Protection: 1; mode=block
Response\xss()
// Send the header Strict-Transport-Security: max-age=31536000
Response\hsts();
// Send the header X-Frame-Options: DENY
Response\xframe();
```
### Content Security Policies
```php
Response\csp(array(
'img-src' => '*'
));
// Send these headers:
Content-Security-Policy: img-src *; default-src 'self';
X-Content-Security-Policy: img-src *; default-src 'self';
X-WebKit-CSP: img-src *; default-src 'self';
```
Request
-------
### Get querystring variables
```php
use PicoFarad\Request;
// Get from the URL: ?toto=value
echo Request\param('toto');
// Get only integer value: ?toto=2
echo Request\int_param('toto');
```
### Get the raw body
```php
echo Request\body();
```
### Get decoded JSON body or form values
If a form is submited, you got an array of values.
If the body is a JSON encoded string you got an array of the decoded JSON.
```php
print_r(Request\values());
```
### Get a form variable
```php
echo Request\value('myvariable');
```
### Get the content of a uploaded file
```php
echo Request\file_content('field_form_name');
```
### Check if the request is a POST
```php
if (Request\is_post()) {
...
}
```
### Check if the request is a GET
```php
if (Request\is_get()) {
...
}
```
### Get the request uri
```php
echo Request\uri();
```
Session
-------
### Open and close a session
The session cookie have the following settings:
- Cookie lifetime: 2678400 seconds (31 days)
- Limited to a specified path (http://domain/mywebapp/) or not (http://domain/)
- If the connection is HTTPS, the cookie use the secure flag
- The cookie is HttpOnly, not available from Javascript
Example:
```php
use PicoFarad\Session;
// Session start
Session\open('mywebappdirectory');
// Destroy the session
Session\close();
```
### Flash messages
Set the session variables: `$_SESSION['flash_message']` and `$_SESSION['flash_error_message']`.
In your template, use a helper to display and delete these messages.
```php
// Standard message
Session\flash('My message');
// Error message
Session\flash_error('My error message');
```

19
vendor/fguillot/picofarad/composer.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
"name": "fguillot/picofarad",
"description": "Minimalist micro-framework",
"homepage": "https://github.com/fguillot/picoFarad",
"type": "library",
"license": "Unlicense",
"authors": [
{
"name": "Frédéric Guillot",
"homepage": "http://fredericguillot.com"
}
],
"require": {
"php": ">=5.3.0"
},
"autoload": {
"psr-0": {"PicoFarad": "lib/"}
}
}

View File

@ -78,7 +78,19 @@ function file_move($field, $destination)
} }
function uri()
{
return $_SERVER['REQUEST_URI'];
}
function is_post() function is_post()
{ {
return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST'; return $_SERVER['REQUEST_METHOD'] === 'POST';
}
function is_get()
{
return $_SERVER['REQUEST_METHOD'] === 'GET';
} }

View File

@ -28,9 +28,9 @@ function status($status_code)
} }
function redirect($url) function redirect($url, $status_code = 302)
{ {
header('Location: '.$url); header('Location: '.$url, true, $status_code);
exit; exit;
} }

View File

@ -2,7 +2,7 @@
namespace PicoFarad\Session; namespace PicoFarad\Session;
const SESSION_LIFETIME = 0; const SESSION_LIFETIME = 2678400;
function open($base_path = '/', $save_path = '') function open($base_path = '/', $save_path = '')
@ -41,25 +41,6 @@ function open($base_path = '/', $save_path = '')
function close() function close()
{ {
// Flush all sessions variables
$_SESSION = array();
// Destroy the session cookie
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
// Destroy session data
session_destroy(); session_destroy();
} }

2
vendor/fguillot/picofeed/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.DS_Store
vendor/

12
vendor/fguillot/picofeed/.travis.yml vendored Normal file
View File

@ -0,0 +1,12 @@
language: php
php:
- "5.6"
- "5.5"
- "5.4"
- "5.3"
before_script: wget https://phar.phpunit.de/phpunit.phar
script:
- composer dump-autoload
- php phpunit.phar

View File

@ -0,0 +1,67 @@
PicoFeed
========
PicoFeed was originally developed for [Miniflux](http://miniflux.net), a minimalist and open source news reader.
However, this library can be used inside any project.
PicoFeed is tested with a lot of different feeds and it's simple and easy to use.
[![Build Status](https://travis-ci.org/fguillot/picoFeed.svg?branch=master)](https://travis-ci.org/fguillot/picoFeed)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/fguillot/picoFeed/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/fguillot/picoFeed/?branch=master)
Features
--------
- Simple and fast
- Feed parser for Atom 1.0 and RSS 0.91, 0.92, 1.0 and 2.0
- Feed writer for Atom 1.0 and RSS 2.0
- Favicon fetcher
- Import/Export OPML subscriptions
- Content filter: HTML cleanup, remove pixel trackers and Ads
- Multiple HTTP client adapters: cURL or Stream Context
- Proxy support
- Content grabber: download from the original website the full content
- Enclosure detection
- RTL languages support
- License: Unlicense <http://unlicense.org/>
Requirements
------------
- PHP >= 5.3
- libxml >= 2.7
- XML PHP extensions: DOM and SimpleXML
- cURL or Stream Context (`allow_url_fopen=On`)
Authors
-------
- Original author: [Frédéric Guillot](http://fredericguillot.com/)
- Major Contributors:
- [Bernhard Posselt](https://github.com/Raydiation)
- [David Pennington](https://github.com/Xeoncross)
- [Mathias Kresin](https://github.com/mkresin)
Real world usage
----------------
- [AnythingNew](http://anythingnew.co)
- [Miniflux](http://miniflux.net)
- [Owncloud News](https://github.com/owncloud/news)
Documentation
-------------
- [Installation](docs/installation.markdown)
- [Running unit tests](docs/tests.markdown)
- [Feed parsing](docs/feed-parsing.markdown)
- [Feed creation](docs/feed-creation.markdown)
- [Favicon fetcher](docs/favicon.markdown)
- [OPML file importation](docs/opml-import.markdown)
- [OPML file exportation](docs/opml-export.markdown)
- [Image proxy](docs/image-proxy.markdown) (avoid SSL mixed content warnings)
- [Web scraping](docs/grabber.markdown)
- [Exceptions](docs/exceptions.markdown)
- [Debugging](docs/debugging.markdown)
- [Configuration](docs/config.markdown)

24
vendor/fguillot/picofeed/UNLICENSE vendored Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

19
vendor/fguillot/picofeed/composer.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
"name": "fguillot/picofeed",
"description": "Modern library to write or read feeds (RSS/Atom)",
"homepage": "http://fguillot.github.io/picoFeed",
"type": "library",
"license": "Unlicense",
"authors": [
{
"name": "Frédéric Guillot",
"homepage": "http://fredericguillot.com"
}
],
"require": {
"php": ">=5.3.0"
},
"autoload": {
"psr-0": {"PicoFeed": "lib/"}
}
}

View File

@ -0,0 +1,286 @@
Configuration
=============
How to use the Config object
----------------------------
To change the default parameters, you have to use the Config class.
Create a new instance and pass it to the Reader object like that:
```php
use PicoFeed\Reader\Reader;
use PicoFeed\Config\Config;
$config = new Config;
$config->setClientUserAgent('My custom RSS Reader')
->setProxyHostname('127.0.0.1')
->setProxyPort(8118);
$reader = new Reader($config);
...
```
HTTP Client parameters
----------------------
### Connection timeout
- Method name: `setClientTimeout()`
- Default value: 10 seconds
- Argument value: number of seconds (integer)
```php
$config->setClientTimeout(20); // 20 seconds
```
### User Agent
- Method name: `setClientUserAgent()`
- Default value: `PicoFeed (https://github.com/fguillot/picoFeed)`
- Argument value: string
```php
$config->setClientUserAgent('My RSS reader');
```
### Maximum HTTP redirections
- Method name: `setMaxRedirections()`
- Default value: 5
- Argument value: integer
```php
$config->setMaxRedirections(10);
```
### Maximum HTTP body response size
- Method name: `setMaxBodySize()`
- Default value: 2097152 (2MB)
- Argument value: value in bytes (integer)
```php
$config->setMaxBodySize(10485760); // 10MB
```
### Proxy hostname
- Method name: `setProxyHostname()`
- Default value: empty
- Argument value: string
```php
$config->setProxyHostname('proxy.example.org');
```
### Proxy port
- Method name: `setProxyPort()`
- Default value: 3128
- Argument value: port number (integer)
```php
$config->setProxyPort(8118);
```
### Proxy username
- Method name: `setProxyUsername()`
- Default value: empty
- Argument value: string
```php
$config->setProxyUsername('myuser');
```
### Proxy password
- Method name: `setProxyPassword()`
- Default value: empty
- Argument value: string
```php
$config->setProxyPassword('mysecret');
```
Content grabber
---------------
### Connection timeout
- Method name: `setGrabberTimeout()`
- Default value: 10 seconds
- Argument value: number of seconds (integer)
```php
$config->setGrabberTimeout(20); // 20 seconds
```
### User Agent
- Method name: `setGrabberUserAgent()`
- Default value: `PicoFeed (https://github.com/fguillot/picoFeed)`
- Argument value: string
```php
$config->setGrabberUserAgent('My content scraper');
```
Parser
------
### Hash algorithm used for item id generation
- Method name: `setParserHashAlgo()`
- Default value: `sha256`
- Argument value: any value returned by the function `hash_algos()` (string)
- See: http://php.net/hash_algos
```php
$config->setParserHashAlgo('sha1');
```
### Disable item content filtering
- Method name: `setContentFiltering()`
- Default value: true (filtering is enabled by default)
- Argument value: boolean
```php
$config->setContentFiltering(false);
```
### Timezone
- Method name: `setTimezone()`
- Default value: UTC
- Argument value: See https://php.net/manual/en/timezones.php (string)
- Note: define the timezone for items/feeds
```php
$config->setTimezone('Europe/Paris');
```
Logging
-------
### Timezone
- Method name: `setTimezone()`
- Default value: UTC
- Argument value: See https://php.net/manual/en/timezones.php (string)
- Note: define the timezone for the logging class
```php
$config->setTimezone('Europe/Paris');
```
Filter
------
### Set the iframe whitelist (allowed iframe sources)
- Method name: `setFilterIframeWhitelist()`
- Default value: See the Filter class source code
- Argument value: array
```php
$config->setFilterIframeWhitelist(['http://www.youtube.com', 'http://www.vimeo.com']);
```
### Define HTML integer attributes
- Method name: `setFilterIntegerAttributes()`
- Default value: See the Filter class source code
- Argument value: array
```php
$config->setFilterIntegerAttributes(['width', 'height']);
```
### Add HTML attributes automatically
- Method name: `setFilterAttributeOverrides()`
- Default value: See the Filter class source code
- Argument value: array
```php
$config->setFilterAttributeOverrides(['a' => ['target' => '_blank']);
```
### Set the list of required attributes for tags
- Method name: `setFilterRequiredAttributes()`
- Default value: See the Filter class source code
- Argument value: array
- Note: If the required attributes are not there, the tag is stripped
```php
$config->setFilterRequiredAttributes(['a' => 'href', 'img' => 'src']);
```
### Set the resource blacklist (Ads blocker)
- Method name: `setFilterMediaBlacklist()`
- Default value: See the Filter class source code
- Argument value: array
- Note: Tags are stripped if they have those URLs
```php
$config->setFilterMediaBlacklist(['feeds.feedburner.com', 'share.feedsportal.com']);
```
### Define which attributes are used for external resources
- Method name: `setFilterMediaAttributes()`
- Default value: See the Filter class source code
- Argument value: array
```php
$config->setFilterMediaAttributes(['src', 'href']);
```
### Define the scheme whitelist
- Method name: `setFilterSchemeWhitelist()`
- Default value: See the Filter class source code
- Argument value: array
- See: http://en.wikipedia.org/wiki/URI_scheme
```php
$config->setFilterSchemeWhitelist(['http://', 'ftp://']);
```
### Define the tags and attributes whitelist
- Method name: `setFilterWhitelistedTags()`
- Default value: See the Filter class source code
- Argument value: array
- Note: Only those tags are allowed everything else is stripped
```php
$config->setFilterWhitelistedTags(['a' => ['href'], 'img' => ['src', 'title']]);
```
### Define a image proxy url
- Method name: `setFilterImageProxyUrl()`
- Default value: Empty
- Argument value: string
```php
$config->setFilterImageProxyUrl('http://myproxy.example.org/?url=%s');
```
### Define a image proxy callback
- Method name: `setFilterImageProxyCallback()`
- Default value: null
- Argument value: Closure
```php
$config->setFilterImageProxyCallback(function ($image_url) {
$key = hash_hmac('sha1', $image_url, 'secret');
return 'https://mypublicproxy/'.$key.'/'.urlencode($image_url);
});
```

View File

@ -0,0 +1,86 @@
Debugging
=========
Logging
-------
PicoFeed log in memory the execution flow, if a feed doesn't work correctly it's easy to see what is wrong.
### Reading messages
```php
use PicoFeed\Logging\Logger;
// All messages are stored inside an Array
print_r(Logger::getMessages());
```
You will got an output like that:
```php
Array
(
[0] => Fetch URL: http://petitcodeur.fr/feed.xml
[1] => Etag:
[2] => Last-Modified:
[3] => cURL total time: 0.711378
[4] => cURL dns lookup time: 0.001064
[5] => cURL connect time: 0.100733
[6] => cURL speed download: 74825
[7] => HTTP status code: 200
[8] => HTTP headers: Set-Cookie => start=R2701971637; path=/; expires=Sat, 06-Jul-2013 05:16:33 GMT
[9] => HTTP headers: Date => Sat, 06 Jul 2013 03:55:52 GMT
[10] => HTTP headers: Content-Type => application/xml
[11] => HTTP headers: Content-Length => 53229
[12] => HTTP headers: Connection => close
[13] => HTTP headers: Server => Apache
[14] => HTTP headers: Last-Modified => Tue, 02 Jul 2013 03:26:02 GMT
[15] => HTTP headers: ETag => "393e79c-cfed-4e07ee78b2680"
[16] => HTTP headers: Accept-Ranges => bytes
....
)
```
### Remove messages
All messages are stored in memory, if you need to clear them just call the method `Logger::deleteMessages()`:
```php
Logger::deleteMessages();
```
Command line utility
====================
PicoFeed provides a basic command line tool to debug feeds quickly.
The tool is located in the root directory project.
### Usage
```bash
$ ./picofeed
Usage:
./picofeed feed <feed-url> # Parse a feed a dump the ouput on stdout
./picofeed debug <feed-url> # Display all logging messages for a feed
./picofeed item <feed-url> <item-id> # Fetch only one item
./picofeed nofilter <feed-url> <item-id> # Fetch an item but with no content filtering
```
### Example
```bash
$ ./picofeed debug https://linuxfr.org/
Exception thrown ===> "Invalid SSL certificate"
Array
(
[0] => [2014-11-08 14:04:14] PicoFeed\Client\Curl Fetch URL: https://linuxfr.org/
[1] => [2014-11-08 14:04:14] PicoFeed\Client\Curl Etag provided:
[2] => [2014-11-08 14:04:14] PicoFeed\Client\Curl Last-Modified provided:
[3] => [2014-11-08 14:04:16] PicoFeed\Client\Curl cURL total time: 1.850634
[4] => [2014-11-08 14:04:16] PicoFeed\Client\Curl cURL dns lookup time: 0.00093
[5] => [2014-11-08 14:04:16] PicoFeed\Client\Curl cURL connect time: 0.115213
[6] => [2014-11-08 14:04:16] PicoFeed\Client\Curl cURL speed download: 0
[7] => [2014-11-08 14:04:16] PicoFeed\Client\Curl cURL effective url: https://linuxfr.org/
[8] => [2014-11-08 14:04:16] PicoFeed\Client\Curl cURL error: SSL certificate problem: Invalid certificate chain
)
```

View File

@ -0,0 +1,28 @@
Exceptions
==========
All exceptions inherits from the standard `Exception` class.
### Library Exceptions
- `PicoFeed\PicoFeedException`: Base class exception for the library
### Client Exceptions
- `PicoFeed\Client\ClientException`: Base exception class for the Client class
- `PicoFeed\Client\InvalidCertificateException`: Invalid SSL certificate
- `PicoFeed\Client\InvalidUrlException`: Malformed URL, page not found (404), unable to establish a connection
- `PicoFeed\Client\MaxRedirectException`: Maximum of HTTP redirections reached
- `PicoFeed\Client\MaxSizeException`: The response size exceeds to maximum allowed
- `PicoFeed\Client\TimeoutException`: Connection timeout
### Parser Exceptions
- `PicoFeed\Parser\ParserException`: Base exception class for the Parser class
- `PicoFeed\Parser\MalformedXmlException`: XML Parser error
### Reader Exceptions
- `PicoFeed\Reader\ReaderException`: Base exception class for the Reader
- `PicoFeed\Reader\SubscriptionNotFoundException`: Unable to find a feed for the given website
- `PicoFeed\Reader\UnsupportedFeedFormatException`: Unable to detect the feed format

View File

@ -0,0 +1,81 @@
Favicon fetcher
===============
Find and download the favicon
-----------------------------
```php
use PicoFeed\Reader\Favicon;
$favicon = new Favicon;
// The icon link is https://bits.wikimedia.org/favicon/wikipedia.ico
$icon_link = $favicon->find('https://en.wikipedia.org/');
$icon_content = $favicon->getContent();
```
PicoFeed will try first to find the favicon from the meta tags and fallback to the `favicon.ico` located in the website's root if nothing is found.
- `Favicon::find()` returns the favicon absolute url or an empty string if nothing is found.
- `Favicon::getContent()` returns the favicon file content (binary content)
When the HTML page is parsed, relative links and protocol relative links are converted to absolute url.
Get Favicon file type
---------------------
It's possible to fetch the image type, this information come from the Content-Type HTTP header:
```php
$favicon = new Favicon;
$favicon->find('http://example.net/');
echo $favicon->getType();
// Will output the content type, by example "image/png"
```
Get the Favicon as Data URI
---------------------------
You can also get the whole image as Data URI.
It's useful if you want to store the icon in your database and avoid too many HTTP requests.
```php
$favicon = new Favicon;
$favicon->find('http://example.net/');
echo $favicon->getDataUri();
// Output something like that: data:image/png;base64,iVBORw0KGgoAAAANSUh.....
```
See: http://en.wikipedia.org/wiki/Data_URI_scheme
Check if a favicon link exists
------------------------------
```php
use PicoFeed\Reader\Favicon;
$favicon = new Favicon;
// Return true if the file exists
var_dump($favicon->exists('http://php.net/favicon.ico'));
```
Use personalized HTTP settings
------------------------------
Like other classes, the Favicon class support the Config object as constructor argument:
```php
use PicoFeed\Config\Config;
use PicoFeed\Reader\Favicon;
$config = new Config;
$config->setClientUserAgent('My RSS Reader');
$favicon = new Favicon($config);
$favicon->find('https://github.com');
```

View File

@ -0,0 +1,74 @@
Feed creation
=============
PicoFeed can also generate Atom and RSS feeds.
Generate RSS 2.0 feed
----------------------
```php
use PicoFeed\Syndication\Rss20;
$writer = new Rss20();
$writer->title = 'My site';
$writer->site_url = 'http://boo/';
$writer->feed_url = 'http://boo/feed.atom';
$writer->author = array(
'name' => 'Me',
'url' => 'http://me',
'email' => 'me@here'
);
$writer->items[] = array(
'title' => 'My article 1',
'updated' => strtotime('-2 days'),
'url' => 'http://foo/bar',
'summary' => 'Super summary',
'content' => '<p>content</p>'
);
$writer->items[] = array(
'title' => 'My article 2',
'updated' => strtotime('-1 day'),
'url' => 'http://foo/bar2',
'summary' => 'Super summary 2',
'content' => '<p>content 2 &nbsp; &copy; 2015</p>',
'author' => array(
'name' => 'Me too',
)
);
$writer->items[] = array(
'title' => 'My article 3',
'url' => 'http://foo/bar3'
);
echo $writer->execute();
```
Generate Atom feed
------------------
```php
use PicoFeed\Syndication\Atom;
$writer = new Atom();
$writer->title = 'My site';
$writer->site_url = 'http://boo/';
$writer->feed_url = 'http://boo/feed.atom';
$writer->author = array(
'name' => 'Me',
'url' => 'http://me',
'email' => 'me@here'
);
$writer->items[] = array(
'title' => 'My article 1',
'updated' => strtotime('-2 days'),
'url' => 'http://foo/bar',
'summary' => 'Super summary',
'content' => '<p>content</p>'
);
echo $writer->execute();
```

View File

@ -0,0 +1,226 @@
Feed parsing
============
Parsing a subscription
----------------------
```php
use PicoFeed\Reader\Reader;
use PicoFeed\PicoFeedException;
try {
$reader = new Reader;
// Return a resource
$resource = $reader->download('http://linuxfr.org/news.atom');
// Return the right parser instance according to the feed format
$parser = $reader->getParser(
$resource->getUrl(),
$resource->getContent(),
$resource->getEncoding()
);
// Return a Feed object
$feed = $parser->execute();
// Print the feed properties with the magic method __toString()
echo $feed;
}
catch (PicoFeedException $e) {
// Do Something...
}
```
- The Reader class is the entry point for feed reading
- The method `download()` fetch the remote content and return a resource, an instance of `PicoFeed\Client\Client`
- The method `getParser()` returns a Parser instance according to the feed format Atom, Rss 2.0...
- The parser itself returns a `Feed` object that contains feed and item properties
Output:
```bash
Feed::id = tag:linuxfr.org,2005:/news
Feed::title = LinuxFr.org : les dépêches
Feed::feed_url = http://linuxfr.org/news.atom
Feed::site_url = http://linuxfr.org/news
Feed::date = 1415138079
Feed::language = en-US
Feed::description =
Feed::logo =
Feed::items = 15 items
Feed::isRTL() = false
----
Item::id = 38d8f48284fb03940cbb3aff9101089b81e44efb1281641bdd7c3e7e4bf3b0cd
Item::title = openSUSE 13.2 : nouvelle version du caméléon disponible !
Item::url = http://linuxfr.org/news/opensuse-13-2-nouvelle-version-du-cameleon-disponible
Item::date = 1415122640
Item::language = en-US
Item::author = Syvolc
Item::enclosure_url =
Item::enclosure_type =
Item::isRTL() = false
Item::content = 18307 bytes
....
```
Get the list of available subscriptions for a website
-----------------------------------------------------
The example below will returns all available subscriptions for the website:
```php
use PicoFeed\Reader\Reader;
try {
$reader = new Reader;
$resource = $reader->download('http://www.cnn.com');
$feeds = $reader->find(
$resource->getUrl(),
$resource->getContent()
);
print_r($feeds);
}
catch (PicoFeedException $e) {
// Do something...
}
```
Output:
```php
Array
(
[0] => http://rss.cnn.com/rss/cnn_topstories.rss
[1] => http://rss.cnn.com/rss/cnn_latest.rss
)
```
Feed discovery and parsing
--------------------------
This example will discover automatically the subscription and parse the feed:
```php
try {
$reader = new Reader;
$resource = $reader->discover('http://linuxfr.org');
$parser = $reader->getParser(
$resource->getUrl(),
$resource->getContent(),
$resource->getEncoding()
);
$feed = $parser->execute();
echo $feed;
}
catch (PicoFeedException $e) {
}
```
HTTP caching
------------
PicoFeed supports HTTP caching to avoid unnecessary processing.
1. After the first download, save in your database the values of the Etag and LastModified HTTP headers
2. For the next requests, provide those values to the `download()` method and check if the feed was modified or not
Here an example:
```php
try {
// Fetch from your database the previous values of the Etag and LastModified headers
$etag = '...';
$last_modified = '...';
$reader = new Reader;
// Provide those values to the download method
$resource = $reader->download('http://linuxfr.org/news.atom', $last_modified, $etag);
// Return true if the remote content has changed
if ($resource->isModified()) {
$parser = $reader->getParser(
$resource->getUrl(),
$resource->getContent(),
$resource->getEncoding()
);
$feed = $parser->execute();
// Save your feed in your database
// ...
// Store the Etag and the LastModified headers in your database for the next requests
$etag = $resource->getEtag();
$last_modified = $resource->getLastModified();
// ...
}
else {
echo 'Not modified, nothing to do!';
}
}
catch (PicoFeedException $e) {
// Do something...
}
```
Feed and item properties
------------------------
```php
// Feed object
$feed->getId(); // Unique feed id
$feed->getTitle(); // Feed title
$feed->getFeedUrl(); // Feed url
$feed->getSiteUrl(); // Website url
$feed->getDate(); // Feed last updated date
$feed->getLanguage(); // Feed language
$feed->getDescription(); // Feed description
$feed->getLogo(); // Feed logo (can be a large image, different from icon)
$feed->getItems(); // List of item objects
// Item object
$feed->items[0]->getId(); // Item unique id (hash)
$feed->items[0]->getTitle(); // Item title
$feed->items[0]->getUrl(); // Item url
$feed->items[0]->getDate(); // Item published date (timestamp)
$feed->items[0]->getLanguage(); // Item language
$feed->items[0]->getAuthor(); // Item author
$feed->items[0]->getEnclosureUrl(); // Enclosure url
$feed->items[0]->getEnclosureType(); // Enclosure mime-type (audio/mp3, image/png...)
$feed->items[0]->getContent(); // Item content (filtered or raw)
$feed->items[0]->isRTL(); // Return true if the item language is Right-To-Left
```
RTL language detection
----------------------
Use the method `Item::isRTL()` to test if an item is RTL or not:
```php
var_dump($item->isRTL()); // true or false
```
Known RTL languages are:
- Arabic (ar-**)
- Farsi (fa-**)
- Urdu (ur-**)
- Pashtu (ps-**)
- Syriac (syr-**)
- Divehi (dv-**)
- Hebrew (he-**)
- Yiddish (yi-**)

View File

@ -0,0 +1,136 @@
Web scraper
===========
The web scraper is useful for feeds that display only a summary of articles, the scraper can download and parse the full content from the original website.
How the content grabber works?
------------------------------
1. Try with rules first (XPath queries) for the domain name (see `PicoFeed\Rules\`)
2. Try to find the text content by using common attributes for class and id
3. Finally, if nothing is found, the feed content is displayed
**The best results are obtained with XPath rules file.**
Standalone usage
----------------
```php
<?php
use PicoFeed\Client\Grabber;
$grabber = new Grabber($item_url);
$grabber->download();
$grabber->parse();
// Get raw HTML content
echo $grabber->getRawContent();
// Get relevant content
echo $grabber->getContent();
// Get filtered relevant content
echo $grabber->getFilteredContent();
```
Fetch full item contents during feed parsing
--------------------------------------------
Before parsing all items, just call the method `$parser->enableContentGrabber()`:
```php
<?php
use PicoFeed\Reader\Reader;
use PicoFeed\PicoFeedException;
try {
$reader = new Reader;
// Return a resource
$resource = $reader->download('http://www.egscomics.com/rss.php');
// Return the right parser instance according to the feed format
$parser = $reader->getParser(
$resource->getUrl(),
$resource->getContent(),
$resource->getEncoding()
);
// Enable content grabber before parsing items
$parser->enableContentGrabber();
// Return a Feed object
$feed = $parser->execute();
}
catch (PicoFeedException $e) {
// Do Something...
}
```
When the content scraper is enabled, everything will be slower.
**For each item a new HTTP request is made** and the HTML downloaded is parsed with XML/XPath.
Configuration
-------------
### Enable content grabber for items
- Method name: `enableContentGrabber()`
- Default value: false (content grabber is disabled by default)
- Argument value: none
```php
$parser->enableContentGrabber();
```
### Ignore item urls for the content grabber
- Method name: `setGrabberIgnoreUrls()`
- Default value: empty (fetch all item urls)
- Argument value: array (list of item urls to ignore)
```php
$parser->setGrabberIgnoreUrls(['http://foo', 'http://bar']);
```
How to write a grabber rules file?
----------------------------------
Add a PHP file to the directory `PicoFeed\Rules`, the filename must be the same as the domain name:
Example with the BBC website, `www.bbc.co.uk.php`:
```php
<?php
return array(
'test_url' => 'http://www.bbc.co.uk/news/world-middle-east-23911833',
'body' => array(
'//div[@class="story-body"]',
),
'strip' => array(
'//script',
'//form',
'//style',
'//*[@class="story-date"]',
'//*[@class="story-header"]',
'//*[@class="story-related"]',
'//*[contains(@class, "byline")]',
'//*[contains(@class, "story-feature")]',
'//*[@id="video-carousel-container"]',
'//*[@id="also-related-links"]',
'//*[contains(@class, "share") or contains(@class, "hidden") or contains(@class, "hyper")]',
)
);
```
Actually, only `body`, `strip` and `test_url` are supported.
Don't forget to send a pull request or a ticket to share your contribution with everybody,
List of content grabber rules
-----------------------------
Rules are stored inside the directory [lib/PicoFeed/Rules](https://github.com/fguillot/picoFeed/tree/master/lib/PicoFeed/Rules)

View File

@ -0,0 +1,66 @@
Image Proxy
===========
To prevent mixed content warnings on SSL pages served from your RSS reader you might want to use an assets proxy.
Images url will be rewritten to be downloaded through the proxy.
Example:
```html
<img src="http://example.org/image.png"/>
```
Can be rewritten like that:
```html
<img src="http://myproxy.example.org/?url=http%3A%2F%2Fexample.org%2Fimage.png"/>
```
Currently this feature is only compatible with images.
There is several open source SSL image proxy available like [Camo](https://github.com/atmos/camo).
You can also write your own proxy.
Usage
-----
There two different ways to use this feature, define a proxy url or a callback.
### Define a proxy url
A proxy url must be defined with a placeholder `%s`.
The placeholder will be replaced by the image source urlencoded.
```php
$config = new Config;
$config->setFilterImageProxyUrl('http://myproxy.example.org/?url=%s');
```
Will rewrite the image source like that:
```html
<img src="http://myproxy.example.org/?url=http%3A%2F%2Fexample.org%2Fimage.png"/>
```
### Define a callback
Your callback will be called each time an image url need to be rewritten.
The first argument is the original image url and your function must returns the new image url.
Here an example if your proxy need a shared secret key:
```php
$config = new Config;
$config->setFilterImageProxyCallback(function ($image_url) {
$key = hash_hmac('sha1', $image_url, 'secret');
return 'https://mypublicproxy/'.$key.'/'.urlencode($image_url);
});
```
Will generate an image url like that:
```html
<img src="https://mypublicproxy/4924964043f3119b3cf2b07b1922d491bcc20092/http%3A%2F%2Ffoo%2Fimage.png"/>
```

View File

@ -0,0 +1,67 @@
Installation
============
Versions
--------
- Development version: master
- Available versions:
- v0.1.0 (stable)
- v0.0.2
- v0.0.1
Note: The public API has changed between 0.0.x and 0.1.0
Installation with Composer
--------------------------
Configure your `composer.json`:
```json
{
"require": {
"fguillot/picofeed": "0.1.0"
}
}
```
Or simply:
```bash
composer require fguillot/picofeed:0.1.0
```
And download the code:
```bash
composer install # or update
```
Usage example with the Composer autoloader:
```php
<?php
require 'vendor/autoload.php';
use PicoFeed\Reader\Reader;
try {
$reader = new Reader;
$resource = $reader->download('http://linuxfr.org/news.atom');
$parser = $reader->getParser(
$resource->getUrl(),
$resource->getContent(),
$resource->getEncoding()
);
$feed = $parser->execute();
echo $feed;
}
catch (Exception $e) {
// Do something...
}
```

View File

@ -0,0 +1,46 @@
OPML export
===========
Example with no categories
--------------------------
```php
use PicoFeed\Serialization\Export;
$feeds = array(
array(
'title' => 'Site title',
'description' => 'Optional description',
'site_url' => 'http://petitcodeur.fr/',
'site_feed' => 'http://petitcodeur.fr/feed.xml'
)
);
$export = new Export($feeds);
$opml = $export->execute();
echo $opml; // XML content
```
Example with categories
-----------------------
```php
use PicoFeed\Serialization\Export;
$feeds = array(
'my category' => array(
array(
'title' => 'Site title',
'description' => 'Optional description',
'site_url' => 'http://petitcodeur.fr/',
'site_feed' => 'http://petitcodeur.fr/feed.xml'
)
)
);
$export = new Export($feeds);
$opml = $export->execute();
echo $opml; // XML content
```

View File

@ -0,0 +1,19 @@
Import OPML file
================
Importing a list of subscriptions is pretty straightforward:
```php
use PicoFeed\Serialization\Import;
$opml = file_get_contents('mySubscriptions.opml');
$import = new Import($opml);
$entries = $import->execute();
if ($entries !== false) {
print_r($entries);
}
```
The method `execute()` return `false` if there is a parsing error.

View File

@ -0,0 +1,14 @@
Running unit tests
==================
If the autoloader is not yet installed run:
```php
composer dump-autoload
```
Then run:
```php
phpunit tests
```

View File

@ -1,10 +1,9 @@
<?php <?php
namespace PicoFeed; namespace PicoFeed\Client;
use LogicException; use LogicException;
use Clients\Curl; use PicoFeed\Logging\Logger;
use Clients\Stream;
/** /**
* Client class * Client class
@ -23,12 +22,12 @@ abstract class Client
private $is_modified = true; private $is_modified = true;
/** /**
* Flag that say if the resource is a 404 * HTTP Content-Type
* *
* @access private * @access private
* @var bool * @var string
*/ */
private $is_not_found = false; private $content_type = '';
/** /**
* HTTP encoding * HTTP encoding
@ -148,19 +147,15 @@ abstract class Client
* *
* @static * @static
* @access public * @access public
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public static function getInstance() public static function getInstance()
{ {
if (function_exists('curl_init')) { if (function_exists('curl_init')) {
return new Curl;
require_once __DIR__.'/Clients/Curl.php';
return new Clients\Curl;
} }
else if (ini_get('allow_url_fopen')) { else if (ini_get('allow_url_fopen')) {
return new Stream;
require_once __DIR__.'/Clients/Stream.php';
return new Clients\Stream;
} }
throw new LogicException('You must have "allow_url_fopen=1" or curl extension installed'); throw new LogicException('You must have "allow_url_fopen=1" or curl extension installed');
@ -171,7 +166,7 @@ abstract class Client
* *
* @access public * @access public
* @param string $url URL * @param string $url URL
* @return bool * @return Client
*/ */
public function execute($url = '') public function execute($url = '')
{ {
@ -179,20 +174,17 @@ abstract class Client
$this->url = $url; $this->url = $url;
} }
Logging::setMessage(get_called_class().' Fetch URL: '.$this->url); Logger::setMessage(get_called_class().' Fetch URL: '.$this->url);
Logging::setMessage(get_called_class().' Etag provided: '.$this->etag); Logger::setMessage(get_called_class().' Etag provided: '.$this->etag);
Logging::setMessage(get_called_class().' Last-Modified provided: '.$this->last_modified); Logger::setMessage(get_called_class().' Last-Modified provided: '.$this->last_modified);
$response = $this->doRequest(); $response = $this->doRequest();
if (is_array($response)) { $this->handleNotModifiedResponse($response);
$this->handleNotModifiedResponse($response); $this->handleNotFoundResponse($response);
$this->handleNotFoundResponse($response); $this->handleNormalResponse($response);
$this->handleNormalResponse($response);
return true;
}
return false; return $this;
} }
/** /**
@ -207,20 +199,13 @@ abstract class Client
$this->is_modified = false; $this->is_modified = false;
} }
else if ($response['status'] == 200) { else if ($response['status'] == 200) {
$this->is_modified = $this->hasBeenModified($response, $this->etag, $this->last_modified);
$etag = $this->getHeader($response, 'ETag'); $this->etag = $this->getHeader($response, 'ETag');
$last_modified = $this->getHeader($response, 'Last-Modified'); $this->last_modified = $this->getHeader($response, 'Last-Modified');
if ($this->isPropertyEquals('etag', $etag) || $this->isPropertyEquals('last_modified', $last_modified)) {
$this->is_modified = false;
}
$this->etag = $etag;
$this->last_modified = $last_modified;
} }
if ($this->is_modified === false) { if ($this->is_modified === false) {
Logging::setMessage(get_called_class().' Resource not modified'); Logger::setMessage(get_called_class().' Resource not modified');
} }
} }
@ -233,8 +218,7 @@ abstract class Client
public function handleNotFoundResponse(array $response) public function handleNotFoundResponse(array $response)
{ {
if ($response['status'] == 404) { if ($response['status'] == 404) {
$this->is_not_found = true; throw new InvalidUrlException('Resource not found');
Logging::setMessage(get_called_class().' Resource not found');
} }
} }
@ -248,32 +232,68 @@ abstract class Client
{ {
if ($response['status'] == 200) { if ($response['status'] == 200) {
$this->content = $response['body']; $this->content = $response['body'];
$this->encoding = $this->findCharset($response); $this->content_type = $this->findContentType($response);
$this->encoding = $this->findCharset();
} }
} }
/** /**
* Check if a class property equals to a value * Check if a request has been modified according to the parameters
* *
* @access public * @access public
* @param string $property Class property * @param array $response
* @param string $value Value * @param string $etag
* @param string $lastModified
* @return boolean * @return boolean
*/ */
private function isPropertyEquals($property, $value) private function hasBeenModified($response, $etag, $lastModified)
{ {
return $this->$property && $this->$property === $value; $headers = array(
'Etag' => $etag,
'Last-Modified' => $lastModified
);
// Compare the values for each header that is present
$presentCacheHeaderCount = 0;
foreach ($headers as $key => $value) {
if (isset($response['headers'][$key])) {
if ($response['headers'][$key] !== $value) {
return true;
}
$presentCacheHeaderCount++;
}
}
// If at least one header is present and the values match, the response
// was not modified
if ($presentCacheHeaderCount > 0) {
return false;
}
return true;
}
/**
* Find content type from response headers
*
* @access public
* @param array $response Client response
* @return string
*/
public function findContentType(array $response)
{
return strtolower($this->getHeader($response, 'Content-Type'));
} }
/** /**
* Find charset from response headers * Find charset from response headers
* *
* @access public * @access public
* @param array $response Client response * @return string
*/ */
public function findCharset(array $response) public function findCharset()
{ {
$result = explode('charset=', strtolower($this->getHeader($response, 'Content-Type'))); $result = explode('charset=', $this->content_type);
return isset($result[1]) ? $result[1] : ''; return isset($result[1]) ? $result[1] : '';
} }
@ -314,13 +334,13 @@ abstract class Client
} }
} }
Logging::setMessage(get_called_class().' HTTP status code: '.$status); Logger::setMessage(get_called_class().' HTTP status code: '.$status);
foreach ($headers as $name => $value) { foreach ($headers as $name => $value) {
Logging::setMessage(get_called_class().' HTTP header: '.$name.' => '.$value); Logger::setMessage(get_called_class().' HTTP header: '.$name.' => '.$value);
} }
return array($status, $headers); return array($status, new HttpHeaders($headers));
} }
/** /**
@ -328,7 +348,7 @@ abstract class Client
* *
* @access public * @access public
* @param string $last_modified Header value * @param string $last_modified Header value
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setLastModified($last_modified) public function setLastModified($last_modified)
{ {
@ -352,7 +372,7 @@ abstract class Client
* *
* @access public * @access public
* @param string $etag Etag HTTP header value * @param string $etag Etag HTTP header value
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setEtag($etag) public function setEtag($etag)
{ {
@ -387,7 +407,7 @@ abstract class Client
* *
* @access public * @access public
* @return string * @return string
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setUrl($url) public function setUrl($url)
{ {
@ -406,6 +426,17 @@ abstract class Client
return $this->content; return $this->content;
} }
/**
* Get the content type value from HTTP headers
*
* @access public
* @return string
*/
public function getContentType()
{
return $this->content_type;
}
/** /**
* Get the encoding value from HTTP headers * Get the encoding value from HTTP headers
* *
@ -428,23 +459,12 @@ abstract class Client
return $this->is_modified; return $this->is_modified;
} }
/**
* Return true if the remote resource is not found
*
* @access public
* @return bool
*/
public function isNotFound()
{
return $this->is_not_found;
}
/** /**
* Set connection timeout * Set connection timeout
* *
* @access public * @access public
* @param integer $timeout Connection timeout * @param integer $timeout Connection timeout
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setTimeout($timeout) public function setTimeout($timeout)
{ {
@ -457,7 +477,7 @@ abstract class Client
* *
* @access public * @access public
* @param string $user_agent User Agent * @param string $user_agent User Agent
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setUserAgent($user_agent) public function setUserAgent($user_agent)
{ {
@ -470,7 +490,7 @@ abstract class Client
* *
* @access public * @access public
* @param integer $max Maximum * @param integer $max Maximum
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setMaxRedirections($max) public function setMaxRedirections($max)
{ {
@ -483,7 +503,7 @@ abstract class Client
* *
* @access public * @access public
* @param integer $max Maximum * @param integer $max Maximum
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setMaxBodySize($max) public function setMaxBodySize($max)
{ {
@ -496,7 +516,7 @@ abstract class Client
* *
* @access public * @access public
* @param string $hostname Proxy hostname * @param string $hostname Proxy hostname
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setProxyHostname($hostname) public function setProxyHostname($hostname)
{ {
@ -509,7 +529,7 @@ abstract class Client
* *
* @access public * @access public
* @param integer $port Proxy port * @param integer $port Proxy port
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setProxyPort($port) public function setProxyPort($port)
{ {
@ -522,7 +542,7 @@ abstract class Client
* *
* @access public * @access public
* @param string $username Proxy username * @param string $username Proxy username
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setProxyUsername($username) public function setProxyUsername($username)
{ {
@ -535,7 +555,7 @@ abstract class Client
* *
* @access public * @access public
* @param string $password Password * @param string $password Password
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setProxyPassword($password) public function setProxyPassword($password)
{ {
@ -547,8 +567,8 @@ abstract class Client
* Set config object * Set config object
* *
* @access public * @access public
* @param \PicoFeed\Config $config Config instance * @param \PicoFeed\Config\Config $config Config instance
* @return \PicoFeed\Client * @return \PicoFeed\Client\Client
*/ */
public function setConfig($config) public function setConfig($config)
{ {

View File

@ -0,0 +1,16 @@
<?php
namespace PicoFeed\Client;
use PicoFeed\PicoFeedException;
/**
* ClientException Exception
*
* @author Frederic Guillot
* @package Client
*/
abstract class ClientException extends PicoFeedException
{
}

View File

@ -1,15 +1,14 @@
<?php <?php
namespace PicoFeed\Clients; namespace PicoFeed\Client;
use \PicoFeed\Logging; use PicoFeed\Logging\Logger;
use \PicoFeed\Client;
/** /**
* cURL HTTP client * cURL HTTP client
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package client * @package Client
*/ */
class Curl extends Client class Curl extends Client
{ {
@ -100,7 +99,7 @@ class Curl extends Client
* Prepare HTTP headers * Prepare HTTP headers
* *
* @access private * @access private
* @return array * @return string[]
*/ */
private function prepareHeaders() private function prepareHeaders()
{ {
@ -124,24 +123,24 @@ class Curl extends Client
* Prepare curl proxy context * Prepare curl proxy context
* *
* @access private * @access private
* @return resource * @return resource $ch
*/ */
private function prepareProxyContext($ch) private function prepareProxyContext($ch)
{ {
if ($this->proxy_hostname) { if ($this->proxy_hostname) {
Logging::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port); Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port);
curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxy_port); curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxy_port);
curl_setopt($ch, CURLOPT_PROXYTYPE, 'HTTP'); curl_setopt($ch, CURLOPT_PROXYTYPE, 'HTTP');
curl_setopt($ch, CURLOPT_PROXY, $this->proxy_hostname); curl_setopt($ch, CURLOPT_PROXY, $this->proxy_hostname);
if ($this->proxy_username) { if ($this->proxy_username) {
Logging::setMessage(get_called_class().' Proxy credentials: Yes'); Logger::setMessage(get_called_class().' Proxy credentials: Yes');
curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxy_username.':'.$this->proxy_password); curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxy_username.':'.$this->proxy_password);
} }
else { else {
Logging::setMessage(get_called_class().' Proxy credentials: No'); Logger::setMessage(get_called_class().' Proxy credentials: No');
} }
} }
@ -161,12 +160,10 @@ class Curl extends Client
curl_setopt($ch, CURLOPT_URL, $this->url); curl_setopt($ch, CURLOPT_URL, $this->url);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->prepareHeaders()); curl_setopt($ch, CURLOPT_HTTPHEADER, $this->prepareHeaders());
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, ini_get('open_basedir') === ''); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, ini_get('open_basedir') === '');
curl_setopt($ch, CURLOPT_MAXREDIRS, $this->max_redirects); curl_setopt($ch, CURLOPT_MAXREDIRS, $this->max_redirects);
curl_setopt($ch, CURLOPT_ENCODING, ''); curl_setopt($ch, CURLOPT_ENCODING, '');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // For auto-signed certificates...
curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($this, 'readBody')); curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($this, 'readBody'));
curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, 'readHeaders')); curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, 'readHeaders'));
curl_setopt($ch, CURLOPT_COOKIEJAR, 'php://memory'); curl_setopt($ch, CURLOPT_COOKIEJAR, 'php://memory');
@ -181,28 +178,31 @@ class Curl extends Client
* Execute curl context * Execute curl context
* *
* @access private * @access private
* @return resource
*/ */
private function executeContext() private function executeContext()
{ {
$ch = $this->prepareContext(); $ch = $this->prepareContext();
curl_exec($ch); curl_exec($ch);
Logging::setMessage(get_called_class().' cURL total time: '.curl_getinfo($ch, CURLINFO_TOTAL_TIME)); Logger::setMessage(get_called_class().' cURL total time: '.curl_getinfo($ch, CURLINFO_TOTAL_TIME));
Logging::setMessage(get_called_class().' cURL dns lookup time: '.curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME)); Logger::setMessage(get_called_class().' cURL dns lookup time: '.curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME));
Logging::setMessage(get_called_class().' cURL connect time: '.curl_getinfo($ch, CURLINFO_CONNECT_TIME)); Logger::setMessage(get_called_class().' cURL connect time: '.curl_getinfo($ch, CURLINFO_CONNECT_TIME));
Logging::setMessage(get_called_class().' cURL speed download: '.curl_getinfo($ch, CURLINFO_SPEED_DOWNLOAD)); Logger::setMessage(get_called_class().' cURL speed download: '.curl_getinfo($ch, CURLINFO_SPEED_DOWNLOAD));
Logging::setMessage(get_called_class().' cURL effective url: '.curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)); Logger::setMessage(get_called_class().' cURL effective url: '.curl_getinfo($ch, CURLINFO_EFFECTIVE_URL));
if (curl_errno($ch)) { $curl_errno = curl_errno($ch);
Logging::setMessage(get_called_class().' cURL error: '.curl_error($ch));
if ($curl_errno) {
Logger::setMessage(get_called_class().' cURL error: '.curl_error($ch));
curl_close($ch); curl_close($ch);
return false;
$this->handleError($curl_errno);
} }
curl_close($ch); // Update the url if there where redirects
$this->url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
return true; curl_close($ch);
} }
/** /**
@ -214,13 +214,11 @@ class Curl extends Client
*/ */
public function doRequest($follow_location = true) public function doRequest($follow_location = true)
{ {
if (! $this->executeContext()) { $this->executeContext();
return false;
}
list($status, $headers) = $this->parseHeaders(explode("\r\n", $this->headers[$this->headers_counter - 1])); list($status, $headers) = $this->parseHeaders(explode("\r\n", $this->headers[$this->headers_counter - 1]));
// When resticted with open_basedir // When restricted with open_basedir
if ($this->needToHandleRedirection($follow_location, $status)) { if ($this->needToHandleRedirection($follow_location, $status)) {
return $this->handleRedirection($headers['Location']); return $this->handleRedirection($headers['Location']);
} }
@ -250,11 +248,12 @@ class Curl extends Client
* *
* @access private * @access private
* @param string $location Redirected URL * @param string $location Redirected URL
* @return boolean|array * @return array
*/ */
private function handleRedirection($location) private function handleRedirection($location)
{ {
$nb_redirects = 0; $nb_redirects = 0;
$result = array();
$this->url = $location; $this->url = $location;
$this->body = ''; $this->body = '';
$this->body_length = 0; $this->body_length = 0;
@ -266,7 +265,7 @@ class Curl extends Client
$nb_redirects++; $nb_redirects++;
if ($nb_redirects >= $this->max_redirects) { if ($nb_redirects >= $this->max_redirects) {
return false; throw new MaxRedirectException('Maximum number of redirections reached');
} }
$result = $this->doRequest(false); $result = $this->doRequest(false);
@ -279,10 +278,50 @@ class Curl extends Client
$this->headers_counter = 0; $this->headers_counter = 0;
} }
else { else {
return $result; break;
} }
} }
return false; return $result;
}
/**
* Handle cURL errors (throw individual exceptions)
*
* We don't use constants because they are not necessary always available
* (depends of the version of libcurl linked to php)
*
* @see http://curl.haxx.se/libcurl/c/libcurl-errors.html
* @access private
* @param integer $errno cURL error code
*/
private function handleError($errno)
{
switch ($errno) {
case 78: // CURLE_REMOTE_FILE_NOT_FOUND
throw new InvalidUrlException('Resource not found');
case 6: // CURLE_COULDNT_RESOLVE_HOST
throw new InvalidUrlException('Unable to resolve hostname');
case 7: // CURLE_COULDNT_CONNECT
throw new InvalidUrlException('Unable to connect to the remote host');
case 28: // CURLE_OPERATION_TIMEDOUT
throw new TimeoutException('Operation timeout');
case 35: // CURLE_SSL_CONNECT_ERROR
case 51: // CURLE_PEER_FAILED_VERIFICATION
case 58: // CURLE_SSL_CERTPROBLEM
case 60: // CURLE_SSL_CACERT
case 59: // CURLE_SSL_CIPHER
case 64: // CURLE_USE_SSL_FAILED
case 66: // CURLE_SSL_ENGINE_INITFAILED
case 77: // CURLE_SSL_CACERT_BADFILE
case 83: // CURLE_SSL_ISSUER_ERROR
throw new InvalidCertificateException('Invalid SSL certificate');
case 47: // CURLE_TOO_MANY_REDIRECTS
throw new MaxRedirectException('Maximum number of redirections reached');
case 63: // CURLE_FILESIZE_EXCEEDED
throw new MaxSizeException('Maximum response size exceeded');
default:
throw new InvalidUrlException('Unable to fetch the URL');
}
} }
} }

View File

@ -1,14 +1,18 @@
<?php <?php
namespace PicoFeed; namespace PicoFeed\Client;
use DOMXPath; use DOMXPath;
use PicoFeed\Encoding\Encoding;
use PicoFeed\Logging\Logger;
use PicoFeed\Filter\Filter;
use PicoFeed\Parser\XmlParser;
/** /**
* Grabber class * Grabber class
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package picofeed * @package Client
*/ */
class Grabber class Grabber
{ {
@ -119,9 +123,9 @@ class Grabber
* Config object * Config object
* *
* @access private * @access private
* @var \PicoFeed\Config * @var \PicoFeed\Config\Config
*/ */
private $config = null; private $config;
/** /**
* Constructor * Constructor
@ -142,8 +146,8 @@ class Grabber
* Set config object * Set config object
* *
* @access public * @access public
* @param \PicoFeed\Config $config Config instance * @param \PicoFeed\Config\Config $config Config instance
* @return \PicoFeed\Grabber * @return Grabber
*/ */
public function setConfig($config) public function setConfig($config)
{ {
@ -173,6 +177,19 @@ class Grabber
return $this->html; return $this->html;
} }
/**
* Get filtered relevant content
*
* @access public
* @return string
*/
public function getFilteredContent()
{
$filter = Filter::html($this->content, $this->url);
$filter->setConfig($this->config);
return $filter->execute();
}
/** /**
* Parse the HTML content * Parse the HTML content
* *
@ -183,30 +200,30 @@ class Grabber
{ {
if ($this->html) { if ($this->html) {
Logging::setMessage(get_called_class().' Fix encoding'); Logger::setMessage(get_called_class().' Fix encoding');
Logging::setMessage(get_called_class().': HTTP Encoding "'.$this->encoding.'"'); Logger::setMessage(get_called_class().': HTTP Encoding "'.$this->encoding.'"');
$this->html = Filter::stripHeadTags($this->html);
$this->html = Encoding::convert($this->html, $this->encoding); $this->html = Encoding::convert($this->html, $this->encoding);
$this->html = Filter::stripHeadTags($this->html);
Logging::setMessage(get_called_class().' Content length: '.strlen($this->html).' bytes'); Logger::setMessage(get_called_class().' Content length: '.strlen($this->html).' bytes');
$rules = $this->getRules(); $rules = $this->getRules();
if (is_array($rules)) { if (is_array($rules)) {
Logging::setMessage(get_called_class().' Parse content with rules'); Logger::setMessage(get_called_class().' Parse content with rules');
$this->parseContentWithRules($rules); $this->parseContentWithRules($rules);
} }
else { else {
Logging::setMessage(get_called_class().' Parse content with candidates'); Logger::setMessage(get_called_class().' Parse content with candidates');
$this->parseContentWithCandidates(); $this->parseContentWithCandidates();
} }
} }
else { else {
Logging::setMessage(get_called_class().' No content fetched'); Logger::setMessage(get_called_class().' No content fetched');
} }
Logging::setMessage(get_called_class().' Content length: '.strlen($this->content).' bytes'); Logger::setMessage(get_called_class().' Content length: '.strlen($this->content).' bytes');
Logging::setMessage(get_called_class().' Grabber done'); Logger::setMessage(get_called_class().' Grabber done');
return $this->content !== ''; return $this->content !== '';
} }
@ -223,6 +240,7 @@ class Grabber
$client->setConfig($this->config); $client->setConfig($this->config);
$client->execute($this->url); $client->execute($this->url);
$this->url = $client->getUrl();
$this->html = $client->getContent(); $this->html = $client->getContent();
$this->encoding = $client->getEncoding(); $this->encoding = $client->getEncoding();
@ -255,14 +273,12 @@ class Grabber
$files[] = substr($hostname, 0, $pos); $files[] = substr($hostname, 0, $pos);
} }
// Logging::setMessage(var_export($files, true));
foreach ($files as $file) { foreach ($files as $file) {
$filename = __DIR__.'/Rules/'.$file.'.php'; $filename = __DIR__.'/../Rules/'.$file.'.php';
if (file_exists($filename)) { if (file_exists($filename)) {
Logging::setMessage(get_called_class().' Load rule: '.$file); Logger::setMessage(get_called_class().' Load rule: '.$file);
return include $filename; return include $filename;
} }
} }
@ -278,7 +294,7 @@ class Grabber
*/ */
public function parseContentWithRules(array $rules) public function parseContentWithRules(array $rules)
{ {
// Logging::setMessage($this->html); // Logger::setMessage($this->html);
$dom = XmlParser::getHtmlDocument('<?xml version="1.0" encoding="UTF-8">'.$this->html); $dom = XmlParser::getHtmlDocument('<?xml version="1.0" encoding="UTF-8">'.$this->html);
$xpath = new DOMXPath($dom); $xpath = new DOMXPath($dom);
@ -324,13 +340,13 @@ class Grabber
// Try to lookup in each tag // Try to lookup in each tag
foreach ($this->candidatesAttributes as $candidate) { foreach ($this->candidatesAttributes as $candidate) {
Logging::setMessage(get_called_class().' Try this candidate: "'.$candidate.'"'); Logger::setMessage(get_called_class().' Try this candidate: "'.$candidate.'"');
$nodes = $xpath->query('//*[(contains(@class, "'.$candidate.'") or @id="'.$candidate.'") and not (contains(@class, "nav") or contains(@class, "page"))]'); $nodes = $xpath->query('//*[(contains(@class, "'.$candidate.'") or @id="'.$candidate.'") and not (contains(@class, "nav") or contains(@class, "page"))]');
if ($nodes !== false && $nodes->length > 0) { if ($nodes !== false && $nodes->length > 0) {
$this->content = $dom->saveXML($nodes->item(0)); $this->content = $dom->saveXML($nodes->item(0));
Logging::setMessage(get_called_class().' Find candidate "'.$candidate.'" ('.strlen($this->content).' bytes)'); Logger::setMessage(get_called_class().' Find candidate "'.$candidate.'" ('.strlen($this->content).' bytes)');
break; break;
} }
} }
@ -342,16 +358,16 @@ class Grabber
if ($nodes !== false && $nodes->length > 0) { if ($nodes !== false && $nodes->length > 0) {
$this->content = $dom->saveXML($nodes->item(0)); $this->content = $dom->saveXML($nodes->item(0));
Logging::setMessage(get_called_class().' Find <article/> tag ('.strlen($this->content).' bytes)'); Logger::setMessage(get_called_class().' Find <article/> tag ('.strlen($this->content).' bytes)');
} }
} }
if (strlen($this->content) < 50) { if (strlen($this->content) < 50) {
Logging::setMessage(get_called_class().' No enought content fetched, get the full body'); Logger::setMessage(get_called_class().' No enought content fetched, get the full body');
$this->content = $dom->saveXML($dom->firstChild); $this->content = $dom->saveXML($dom->firstChild);
} }
Logging::setMessage(get_called_class().' Strip garbage'); Logger::setMessage(get_called_class().' Strip garbage');
$this->stripGarbage(); $this->stripGarbage();
} }
@ -373,7 +389,7 @@ class Grabber
$nodes = $xpath->query('//'.$tag); $nodes = $xpath->query('//'.$tag);
if ($nodes !== false && $nodes->length > 0) { if ($nodes !== false && $nodes->length > 0) {
Logging::setMessage(get_called_class().' Strip tag: "'.$tag.'"'); Logger::setMessage(get_called_class().' Strip tag: "'.$tag.'"');
foreach ($nodes as $node) { foreach ($nodes as $node) {
$node->parentNode->removeChild($node); $node->parentNode->removeChild($node);
} }
@ -385,7 +401,7 @@ class Grabber
$nodes = $xpath->query('//*[contains(@class, "'.$attribute.'") or contains(@id, "'.$attribute.'")]'); $nodes = $xpath->query('//*[contains(@class, "'.$attribute.'") or contains(@id, "'.$attribute.'")]');
if ($nodes !== false && $nodes->length > 0) { if ($nodes !== false && $nodes->length > 0) {
Logging::setMessage(get_called_class().' Strip attribute: "'.$attribute.'"'); Logger::setMessage(get_called_class().' Strip attribute: "'.$attribute.'"');
foreach ($nodes as $node) { foreach ($nodes as $node) {
$node->parentNode->removeChild($node); $node->parentNode->removeChild($node);
} }

View File

@ -0,0 +1,43 @@
<?php
namespace PicoFeed\Client;
use ArrayAccess;
/**
* Class to handle http headers case insensitivity
*
* @author Bernhard Posselt
* @package Client
*/
class HttpHeaders implements ArrayAccess
{
private $headers = array();
public function __construct(array $headers)
{
foreach ($headers as $key => $value) {
$this->headers[strtolower($key)] = $value;
}
}
public function offsetGet($offset)
{
return $this->headers[strtolower($offset)];
}
public function offsetSet($offset, $value)
{
$this->headers[strtolower($offset)] = $value;
}
public function offsetExists($offset)
{
return isset($this->headers[strtolower($offset)]);
}
public function offsetUnset($offset)
{
unset($this->headers[strtolower($offset)]);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Client;
/**
* InvalidCertificateException Exception
*
* @author Frederic Guillot
* @package Client
*/
class InvalidCertificateException extends ClientException
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Client;
/**
* InvalidUrlException Exception
*
* @author Frederic Guillot
* @package Client
*/
class InvalidUrlException extends ClientException
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Client;
/**
* MaxRedirectException Exception
*
* @author Frederic Guillot
* @package Client
*/
class MaxRedirectException extends ClientException
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Client;
/**
* MaxSizeException Exception
*
* @author Frederic Guillot
* @package Client
*/
class MaxSizeException extends ClientException
{
}

View File

@ -1,15 +1,14 @@
<?php <?php
namespace PicoFeed\Clients; namespace PicoFeed\Client;
use \PicoFeed\Logging; use PicoFeed\Logging\Logger;
use \PicoFeed\Client;
/** /**
* Stream context HTTP client * Stream context HTTP client
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package client * @package Client
*/ */
class Stream extends Client class Stream extends Client
{ {
@ -17,7 +16,7 @@ class Stream extends Client
* Prepare HTTP headers * Prepare HTTP headers
* *
* @access private * @access private
* @return array * @return string[]
*/ */
private function prepareHeaders() private function prepareHeaders()
{ {
@ -64,16 +63,16 @@ class Stream extends Client
if ($this->proxy_hostname) { if ($this->proxy_hostname) {
Logging::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port); Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port);
$context['http']['proxy'] = 'tcp://'.$this->proxy_hostname.':'.$this->proxy_port; $context['http']['proxy'] = 'tcp://'.$this->proxy_hostname.':'.$this->proxy_port;
$context['http']['request_fulluri'] = true; $context['http']['request_fulluri'] = true;
if ($this->proxy_username) { if ($this->proxy_username) {
Logging::setMessage(get_called_class().' Proxy credentials: Yes'); Logger::setMessage(get_called_class().' Proxy credentials: Yes');
} }
else { else {
Logging::setMessage(get_called_class().' Proxy credentials: No'); Logger::setMessage(get_called_class().' Proxy credentials: No');
} }
} }
@ -96,7 +95,7 @@ class Stream extends Client
// Make HTTP request // Make HTTP request
$stream = @fopen($this->url, 'r', false, $context); $stream = @fopen($this->url, 'r', false, $context);
if (! is_resource($stream)) { if (! is_resource($stream)) {
return false; throw new InvalidUrlException('Unable to establish a connection');
} }
// Get the entire body until the max size // Get the entire body until the max size
@ -104,12 +103,16 @@ class Stream extends Client
// If the body size is too large abort everything // If the body size is too large abort everything
if (strlen($body) > $this->max_body_size) { if (strlen($body) > $this->max_body_size) {
return false; throw new MaxSizeException('Content size too large');
} }
// Get HTTP headers response // Get HTTP headers response
$metadata = stream_get_meta_data($stream); $metadata = stream_get_meta_data($stream);
if ($metadata['timed_out']) {
throw new TimeoutException('Operation timeout');
}
list($status, $headers) = $this->parseHeaders($metadata['wrapper_data']); list($status, $headers) = $this->parseHeaders($metadata['wrapper_data']);
fclose($stream); fclose($stream);
@ -125,11 +128,11 @@ class Stream extends Client
* Decode body response according to the HTTP headers * Decode body response according to the HTTP headers
* *
* @access public * @access public
* @param string $body Raw body * @param string $body Raw body
* @param array $headers HTTP headers * @param HttpHeaders $headers HTTP headers
* @return string * @return string
*/ */
public function decodeBody($body, array $headers) public function decodeBody($body, HttpHeaders $headers)
{ {
if (isset($headers['Transfer-Encoding']) && $headers['Transfer-Encoding'] === 'chunked') { if (isset($headers['Transfer-Encoding']) && $headers['Transfer-Encoding'] === 'chunked') {
$body = $this->decodeChunked($body); $body = $this->decodeChunked($body);

View File

@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Client;
/**
* TimeoutException Exception
*
* @author Frederic Guillot
* @package Client
*/
class TimeoutException extends ClientException
{
}

View File

@ -1,12 +1,12 @@
<?php <?php
namespace PicoFeed; namespace PicoFeed\Client;
/** /**
* URL class * URL class
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package picofeed * @package Client
*/ */
class Url class Url
{ {
@ -79,6 +79,20 @@ class Url
return $link->getAbsoluteUrl(); return $link->getAbsoluteUrl();
} }
/**
* Shortcut method to get a base url
*
* @static
* @access public
* @param string $url
* @return string
*/
public static function base($url)
{
$link = new Url($url);
return $link->getBaseUrl();
}
/** /**
* Get the base URL * Get the base URL
* *

View File

@ -1,6 +1,6 @@
<?php <?php
namespace PicoFeed; namespace PicoFeed\Config;
/** /**
* Config class * Config class
@ -8,28 +8,30 @@ namespace PicoFeed;
* @author Frederic Guillot * @author Frederic Guillot
* @package picofeed * @package picofeed
* *
* @method \PicoFeed\Config setClientTimeout(integer $value) * @method \PicoFeed\Config\Config setClientTimeout(integer $value)
* @method \PicoFeed\Config setClientUserAgent(string $value) * @method \PicoFeed\Config\Config setClientUserAgent(string $value)
* @method \PicoFeed\Config setMaxRedirections(integer $value) * @method \PicoFeed\Config\Config setMaxRedirections(integer $value)
* @method \PicoFeed\Config setMaxBodySize(integer $value) * @method \PicoFeed\Config\Config setMaxBodySize(integer $value)
* @method \PicoFeed\Config setProxyHostname(string $value) * @method \PicoFeed\Config\Config setProxyHostname(string $value)
* @method \PicoFeed\Config setProxyPort(integer $value) * @method \PicoFeed\Config\Config setProxyPort(integer $value)
* @method \PicoFeed\Config setProxyUsername(string $value) * @method \PicoFeed\Config\Config setProxyUsername(string $value)
* @method \PicoFeed\Config setProxyPassword(string $value) * @method \PicoFeed\Config\Config setProxyPassword(string $value)
* @method \PicoFeed\Config setGrabberTimeout(integer $value) * @method \PicoFeed\Config\Config setGrabberTimeout(integer $value)
* @method \PicoFeed\Config setGrabberUserAgent(string $value) * @method \PicoFeed\Config\Config setGrabberUserAgent(string $value)
* @method \PicoFeed\Config setParserHashAlgo(string $value) * @method \PicoFeed\Config\Config setParserHashAlgo(string $value)
* @method \PicoFeed\Config setContentFiltering(boolean $value) * @method \PicoFeed\Config\Config setContentFiltering(boolean $value)
* @method \PicoFeed\Config setTimezone(string $value) * @method \PicoFeed\Config\Config setTimezone(string $value)
* @method \PicoFeed\Config setFilterIframeWhitelist(array $value) * @method \PicoFeed\Config\Config setFilterIframeWhitelist(array $value)
* @method \PicoFeed\Config setFilterIntegerAttributes(array $value) * @method \PicoFeed\Config\Config setFilterIntegerAttributes(array $value)
* @method \PicoFeed\Config setFilterAttributeOverrides(array $value) * @method \PicoFeed\Config\Config setFilterAttributeOverrides(array $value)
* @method \PicoFeed\Config setFilterRequiredAttributes(array $value) * @method \PicoFeed\Config\Config setFilterRequiredAttributes(array $value)
* @method \PicoFeed\Config setFilterMediaBlacklist(array $value) * @method \PicoFeed\Config\Config setFilterMediaBlacklist(array $value)
* @method \PicoFeed\Config setFilterMediaAttributes(array $value) * @method \PicoFeed\Config\Config setFilterMediaAttributes(array $value)
* @method \PicoFeed\Config setFilterSchemeWhitelist(array $value) * @method \PicoFeed\Config\Config setFilterSchemeWhitelist(array $value)
* @method \PicoFeed\Config setFilterWhitelistedTags(array $value) * @method \PicoFeed\Config\Config setFilterWhitelistedTags(array $value)
* @method \PicoFeed\Config setFilterBlacklistedTags(array $value) * @method \PicoFeed\Config\Config setFilterBlacklistedTags(array $value)
* @method \PicoFeed\Config\Config setFilterImageProxyUrl($value)
* @method \PicoFeed\Config\Config setFilterImageProxyCallback($closure)
* *
* @method integer getClientTimeout() * @method integer getClientTimeout()
* @method string getClientUserAgent() * @method string getClientUserAgent()
@ -53,6 +55,8 @@ namespace PicoFeed;
* @method array getFilterSchemeWhitelist(array $default_value) * @method array getFilterSchemeWhitelist(array $default_value)
* @method array getFilterWhitelistedTags(array $default_value) * @method array getFilterWhitelistedTags(array $default_value)
* @method array getFilterBlacklistedTags(array $default_value) * @method array getFilterBlacklistedTags(array $default_value)
* @method string getFilterImageProxyUrl()
* @method \Closure getFilterImageProxyCallback()
*/ */
class Config class Config
{ {

View File

@ -1,6 +1,6 @@
<?php <?php
namespace PicoFeed; namespace PicoFeed\Encoding;
/** /**
* @author "Sebastián Grignoli" <grignoli@framework2.com.ar> * @author "Sebastián Grignoli" <grignoli@framework2.com.ar>
@ -152,20 +152,16 @@ class Encoding
return $cc1.$cc2; return $cc1.$cc2;
} }
public static function convert_CP_1251($input)
{
return iconv('CP1251', 'UTF-8//TRANSLIT', $input);
}
public static function convert($input, $encoding) public static function convert($input, $encoding)
{ {
if ($encoding === 'windows-1251') { switch ($encoding) {
return self::convert_CP_1251($input); case 'utf-8':
return $input;
case 'windows-1251':
case 'windows-1255':
return iconv($encoding, 'UTF-8//TRANSLIT', $input);
default:
return self::toUTF8($input);
} }
else if ($encoding === '' || $encoding !== 'utf-8') {
return self::toUTF8($input);
}
return $input;
} }
} }

View File

@ -2,17 +2,32 @@
namespace PicoFeed\Filter; namespace PicoFeed\Filter;
use \PicoFeed\Url; use \PicoFeed\Client\Url;
use \PicoFeed\Filter;
/** /**
* Attribute Filter class * Attribute Filter class
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package filter * @package Filter
*/ */
class Attribute class Attribute
{ {
/**
* Image proxy url
*
* @access private
* @var string
*/
private $image_proxy_url = '';
/**
* Image proxy callback
*
* @access private
* @var \Closure|null
*/
private $image_proxy_callback = null;
/** /**
* Tags and attribute whitelist * Tags and attribute whitelist
* *
@ -205,25 +220,26 @@ class Attribute
'filterEmptyAttribute', 'filterEmptyAttribute',
'filterAllowedAttribute', 'filterAllowedAttribute',
'filterIntegerAttribute', 'filterIntegerAttribute',
'filterAbsoluteUrlAttribute', 'rewriteAbsoluteUrl',
'filterIframeAttribute', 'filterIframeAttribute',
'filterBlacklistResourceAttribute', 'filterBlacklistResourceAttribute',
'filterProtocolUrlAttribute', 'filterProtocolUrlAttribute',
'rewriteImageProxyUrl',
); );
/** /**
* Add attributes to specified tags * Add attributes to specified tags
* *
* @access private * @access private
* @var \PicoFeed\Url * @var \PicoFeed\Client\Url
*/ */
private $website = null; private $website;
/** /**
* Constructor * Constructor
* *
* @access public * @access public
* @param \PicoFeed\Url $website Website url instance * @param \PicoFeed\Client\Url $website Website url instance
*/ */
public function __construct(Url $website) public function __construct(Url $website)
{ {
@ -350,7 +366,7 @@ class Attribute
* @param string $value Atttribute value * @param string $value Atttribute value
* @return boolean * @return boolean
*/ */
public function filterAbsoluteUrlAttribute($tag, $attribute, &$value) public function rewriteAbsoluteUrl($tag, $attribute, &$value)
{ {
if ($this->isResource($attribute)) { if ($this->isResource($attribute)) {
$value = Url::resolve($value, $this->website); $value = Url::resolve($value, $this->website);
@ -359,6 +375,30 @@ class Attribute
return true; return true;
} }
/**
* Rewrite image url to use with a proxy
*
* @access public
* @param string $tag Tag name
* @param string $attribute Atttribute name
* @param string $value Atttribute value
* @return boolean
*/
public function rewriteImageProxyUrl($tag, $attribute, &$value)
{
if ($tag === 'img' && $attribute === 'src') {
if ($this->image_proxy_url) {
$value = sprintf($this->image_proxy_url, urlencode($value));
}
else if (is_callable($this->image_proxy_callback)) {
$value = call_user_func($this->image_proxy_callback, $value);
}
}
return true;
}
/** /**
* Return true if the scheme is authorized * Return true if the scheme is authorized
* *
@ -420,7 +460,7 @@ class Attribute
* Check if an attribute name is an external resource * Check if an attribute name is an external resource
* *
* @access public * @access public
* @param string $data Attribute name * @param string $attribute Attribute name
* @return boolean * @return boolean
*/ */
public function isResource($attribute) public function isResource($attribute)
@ -451,7 +491,7 @@ class Attribute
* Detect if an url is blacklisted * Detect if an url is blacklisted
* *
* @access public * @access public
* @param string $resouce Attribute value (URL) * @param string $resource Attribute value (URL)
* @return boolean * @return boolean
*/ */
public function isBlacklistedMedia($resource) public function isBlacklistedMedia($resource)
@ -485,11 +525,11 @@ class Attribute
} }
/** /**
* Set whitelisted tags adn attributes for each tag * Set whitelisted tags and attributes for each tag
* *
* @access public * @access public
* @param array $values List of tags: ['video' => ['src', 'cover'], 'img' => ['src']] * @param array $values List of tags: ['video' => ['src', 'cover'], 'img' => ['src']]
* @return \PicoFeed\Filter * @return Attribute
*/ */
public function setWhitelistedAttributes(array $values) public function setWhitelistedAttributes(array $values)
{ {
@ -502,7 +542,7 @@ class Attribute
* *
* @access public * @access public
* @param array $values List of scheme: ['http://', 'ftp://'] * @param array $values List of scheme: ['http://', 'ftp://']
* @return \PicoFeed\Filter * @return Attribute
*/ */
public function setSchemeWhitelist(array $values) public function setSchemeWhitelist(array $values)
{ {
@ -515,7 +555,7 @@ class Attribute
* *
* @access public * @access public
* @param array $values List of values: ['src', 'href'] * @param array $values List of values: ['src', 'href']
* @return \PicoFeed\Filter * @return Attribute
*/ */
public function setMediaAttributes(array $values) public function setMediaAttributes(array $values)
{ {
@ -528,7 +568,7 @@ class Attribute
* *
* @access public * @access public
* @param array $values List of tags: ['http://google.com/', '...'] * @param array $values List of tags: ['http://google.com/', '...']
* @return \PicoFeed\Filter * @return Attribute
*/ */
public function setMediaBlacklist(array $values) public function setMediaBlacklist(array $values)
{ {
@ -541,7 +581,7 @@ class Attribute
* *
* @access public * @access public
* @param array $values List of tags: ['img' => 'src'] * @param array $values List of tags: ['img' => 'src']
* @return \PicoFeed\Filter * @return Attribute
*/ */
public function setRequiredAttributes(array $values) public function setRequiredAttributes(array $values)
{ {
@ -554,7 +594,7 @@ class Attribute
* *
* @access public * @access public
* @param array $values List of tags: ['a' => 'target="_blank"'] * @param array $values List of tags: ['a' => 'target="_blank"']
* @return \PicoFeed\Filter * @return Attribute
*/ */
public function setAttributeOverrides(array $values) public function setAttributeOverrides(array $values)
{ {
@ -567,7 +607,7 @@ class Attribute
* *
* @access public * @access public
* @param array $values List of tags: ['width', 'height'] * @param array $values List of tags: ['width', 'height']
* @return \PicoFeed\Filter * @return Attribute
*/ */
public function setIntegerAttributes(array $values) public function setIntegerAttributes(array $values)
{ {
@ -580,11 +620,39 @@ class Attribute
* *
* @access public * @access public
* @param array $values List of tags: ['http://www.youtube.com'] * @param array $values List of tags: ['http://www.youtube.com']
* @return \PicoFeed\Filter * @return Attribute
*/ */
public function setIframeWhitelist(array $values) public function setIframeWhitelist(array $values)
{ {
$this->iframe_whitelist = $values ?: $this->iframe_whitelist; $this->iframe_whitelist = $values ?: $this->iframe_whitelist;
return $this; return $this;
} }
/**
* Set image proxy URL
*
* The original image url will be urlencoded
*
* @access public
* @param string $url Proxy URL
* @return Attribute
*/
public function setImageProxyUrl($url)
{
$this->image_proxy_url = $url ?: $this->image_proxy_url;
return $this;
}
/**
* Set image proxy callback
*
* @access public
* @param \Closure $callback
* @return Attribute
*/
public function setImageProxyCallback($callback)
{
$this->image_proxy_callback = $callback ?: $this->image_proxy_callback;
return $this;
}
} }

View File

@ -1,14 +1,12 @@
<?php <?php
namespace PicoFeed; namespace PicoFeed\Filter;
use PicoFeed\Filter\Html;
/** /**
* Filter class * Filter class
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package picofeed * @package Filter
*/ */
class Filter class Filter
{ {
@ -19,7 +17,7 @@ class Filter
* @access public * @access public
* @param string $html HTML content * @param string $html HTML content
* @param string $website Site URL (used to build absolute URL) * @param string $website Site URL (used to build absolute URL)
* @return PicoFeed\Filter\Html * @return Html
*/ */
public static function html($html, $website) public static function html($html, $website)
{ {
@ -88,16 +86,7 @@ class Filter
*/ */
public static function stripHeadTags($data) public static function stripHeadTags($data)
{ {
$start = strpos($data, '<head>'); return preg_replace('@<head[^>]*?>.*?</head>@siu','', $data );
$end = strpos($data, '</head>');
if ($start !== false && $end !== false) {
$before = substr($data, 0, $start);
$after = substr($data, $end + 7);
$data = $before.$after;
}
return $data;
} }
/** /**
@ -113,10 +102,7 @@ class Filter
$value = str_replace("\r", ' ', $value); $value = str_replace("\r", ' ', $value);
$value = str_replace("\t", ' ', $value); $value = str_replace("\t", ' ', $value);
$value = str_replace("\n", ' ', $value); $value = str_replace("\n", ' ', $value);
// $value = preg_replace('/\s+/', ' ', $value); <= break utf-8
// Break UTF-8 strings (TODO: find a better way)
// $value = preg_replace('/\s+/', ' ', $value);
return trim($value); return trim($value);
} }

View File

@ -2,15 +2,14 @@
namespace PicoFeed\Filter; namespace PicoFeed\Filter;
use \PicoFeed\Url; use \PicoFeed\Client\Url;
use \PicoFeed\Filter; use \PicoFeed\Parser\XmlParser;
use \PicoFeed\XmlParser;
/** /**
* HTML Filter class * HTML Filter class
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package filter * @package Filter
*/ */
class Html class Html
{ {
@ -18,9 +17,9 @@ class Html
* Config object * Config object
* *
* @access private * @access private
* @var \PicoFeed\Config * @var \PicoFeed\Config\Config
*/ */
private $config = null; private $config;
/** /**
* Unfiltered XML data * Unfiltered XML data
@ -89,14 +88,16 @@ class Html
* Set config object * Set config object
* *
* @access public * @access public
* @param \PicoFeed\Config $config Config instance * @param \PicoFeed\Config\Config $config Config instance
* @return \PicoFeed\Html * @return \PicoFeed\Filter\Html
*/ */
public function setConfig($config) public function setConfig($config)
{ {
$this->config = $config; $this->config = $config;
if ($this->config !== null) { if ($this->config !== null) {
$this->attribute->setImageProxyCallback($this->config->getFilterImageProxyCallback());
$this->attribute->setImageProxyUrl($this->config->getFilterImageProxyUrl());
$this->attribute->setIframeWhitelist($this->config->getFilterIframeWhitelist(array())); $this->attribute->setIframeWhitelist($this->config->getFilterIframeWhitelist(array()));
$this->attribute->setIntegerAttributes($this->config->getFilterIntegerAttributes(array())); $this->attribute->setIntegerAttributes($this->config->getFilterIntegerAttributes(array()));
$this->attribute->setAttributeOverrides($this->config->getFilterAttributeOverrides(array())); $this->attribute->setAttributeOverrides($this->config->getFilterAttributeOverrides(array()));
@ -133,6 +134,11 @@ class Html
return $this->output; return $this->output;
} }
/**
* Called after XML parsing
*
* @access public
*/
public function postFilter() public function postFilter()
{ {
$this->output = $this->tag->removeEmptyTags($this->output); $this->output = $this->tag->removeEmptyTags($this->output);
@ -144,7 +150,7 @@ class Html
* *
* @access public * @access public
* @param resource $parser XML parser * @param resource $parser XML parser
* @param string $name Tag name * @param string $tag Tag name
* @param array $attributes Tag attributes * @param array $attributes Tag attributes
*/ */
public function startTag($parser, $tag, array $attributes) public function startTag($parser, $tag, array $attributes)
@ -172,7 +178,7 @@ class Html
* *
* @access public * @access public
* @param resource $parser XML parser * @param resource $parser XML parser
* @param string $name Tag name * @param string $tag Tag name
*/ */
public function endTag($parser, $tag) public function endTag($parser, $tag)
{ {

View File

@ -6,7 +6,7 @@ namespace PicoFeed\Filter;
* Tag Filter class * Tag Filter class
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package filter * @package Filter
*/ */
class Tag class Tag
{ {
@ -163,7 +163,7 @@ class Tag
* *
* @access public * @access public
* @param array $values List of tags: ['video' => ['src', 'cover'], 'img' => ['src']] * @param array $values List of tags: ['video' => ['src', 'cover'], 'img' => ['src']]
* @return \PicoFeed\Filter * @return Tag
*/ */
public function setWhitelistedTags(array $values) public function setWhitelistedTags(array $values)
{ {

View File

@ -1,6 +1,6 @@
<?php <?php
namespace PicoFeed; namespace PicoFeed\Logging;
use DateTime; use DateTime;
use DateTimeZone; use DateTimeZone;
@ -9,9 +9,9 @@ use DateTimeZone;
* Logging class * Logging class
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package picofeed * @package Logging
*/ */
class Logging class Logger
{ {
/** /**
* List of messages * List of messages
@ -80,4 +80,16 @@ class Logging
{ {
self::$timezone = $timezone ?: self::$timezone; self::$timezone = $timezone ?: self::$timezone;
} }
/**
* Get all messages serialized into a string
*
* @static
* @access public
* @return string
*/
public static function toString()
{
return implode(PHP_EOL, self::$messages).PHP_EOL;
}
} }

View File

@ -1,21 +1,16 @@
<?php <?php
namespace PicoFeed\Parsers; namespace PicoFeed\Parser;
use SimpleXMLElement; use SimpleXMLElement;
use PicoFeed\Parser; use PicoFeed\Filter\Filter;
use PicoFeed\XmlParser; use PicoFeed\Client\Url;
use PicoFeed\Logging;
use PicoFeed\Feed;
use PicoFeed\Filter;
use PicoFeed\Item;
use PicoFeed\Url;
/** /**
* Atom parser * Atom parser
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package parser * @package Parser
*/ */
class Atom extends Parser class Atom extends Parser
{ {
@ -35,20 +30,32 @@ class Atom extends Parser
* Find the feed url * Find the feed url
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedUrl(SimpleXMLElement $xml, Feed $feed) public function findFeedUrl(SimpleXMLElement $xml, Feed $feed)
{ {
$feed->url = $this->getLink($xml); $feed->feed_url = $this->getUrl($xml, 'self');
}
/**
* Find the site url
*
* @access public
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findSiteUrl(SimpleXMLElement $xml, Feed $feed)
{
$feed->site_url = $this->getUrl($xml, 'alternate', true);
} }
/** /**
* Find the feed description * Find the feed description
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedDescription(SimpleXMLElement $xml, Feed $feed) public function findFeedDescription(SimpleXMLElement $xml, Feed $feed)
{ {
@ -59,8 +66,8 @@ class Atom extends Parser
* Find the feed logo url * Find the feed logo url
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedLogo(SimpleXMLElement $xml, Feed $feed) public function findFeedLogo(SimpleXMLElement $xml, Feed $feed)
{ {
@ -71,20 +78,20 @@ class Atom extends Parser
* Find the feed title * Find the feed title
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedTitle(SimpleXMLElement $xml, Feed $feed) public function findFeedTitle(SimpleXMLElement $xml, Feed $feed)
{ {
$feed->title = Filter::stripWhiteSpace((string) $xml->title) ?: $feed->url; $feed->title = Filter::stripWhiteSpace((string) $xml->title) ?: $feed->getSiteUrl();
} }
/** /**
* Find the feed language * Find the feed language
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed)
{ {
@ -95,8 +102,8 @@ class Atom extends Parser
* Find the feed id * Find the feed id
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedId(SimpleXMLElement $xml, Feed $feed) public function findFeedId(SimpleXMLElement $xml, Feed $feed)
{ {
@ -107,8 +114,8 @@ class Atom extends Parser
* Find the feed date * Find the feed date
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedDate(SimpleXMLElement $xml, Feed $feed) public function findFeedDate(SimpleXMLElement $xml, Feed $feed)
{ {
@ -120,11 +127,14 @@ class Atom extends Parser
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param Item $item Item object * @param Item $item Item object
*/ */
public function findItemDate(SimpleXMLElement $entry, Item $item) public function findItemDate(SimpleXMLElement $entry, Item $item)
{ {
$item->date = $this->parseDate((string) $entry->updated); $published = isset($entry->published) ? $this->parseDate((string) $entry->published) : 0;
$updated = isset($entry->updated) ? $this->parseDate((string) $entry->updated) : 0;
$item->date = max($published, $updated) ?: time();
} }
/** /**
@ -147,9 +157,9 @@ class Atom extends Parser
* Find the item author * Find the item author
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed * @param SimpleXMLElement $xml Feed
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item)
{ {
@ -166,7 +176,7 @@ class Atom extends Parser
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public function findItemContent(SimpleXMLElement $entry, Item $item) public function findItemContent(SimpleXMLElement $entry, Item $item)
{ {
@ -178,11 +188,11 @@ class Atom extends Parser
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public function findItemUrl(SimpleXMLElement $entry, Item $item) public function findItemUrl(SimpleXMLElement $entry, Item $item)
{ {
$item->url = $this->getLink($entry); $item->url = $this->getUrl($entry, 'alternate', true);
} }
/** /**
@ -190,28 +200,21 @@ class Atom extends Parser
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed)
{ {
$id = (string) $entry->id; $id = (string) $entry->id;
if ($id !== $item->url) { if ($id) {
$item_permalink = $id; $item->id = $this->generateId($id);
} }
else { else {
$item_permalink = $item->url; $item->id = $this->generateId(
$item->getTitle(), $item->getUrl(), $item->getContent()
);
} }
if ($this->isExcludedFromId($feed->url)) {
$feed_permalink = '';
}
else {
$feed_permalink = $feed->url;
}
$item->id = $this->generateId($item_permalink, $feed_permalink);
} }
/** /**
@ -219,18 +222,16 @@ class Atom extends Parser
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed)
{ {
foreach ($entry->link as $link) { $enclosure = $this->findLink($entry, 'enclosure');
if ((string) $link['rel'] === 'enclosure') {
$item->enclosure_url = Url::resolve((string) $link['href'], $feed->url); if ($enclosure) {
$item->enclosure_type = (string) $link['type']; $item->enclosure_url = Url::resolve((string) $enclosure['href'], $feed->getSiteUrl());
break; $item->enclosure_type = (string) $enclosure['type'];
}
} }
} }
@ -239,40 +240,71 @@ class Atom extends Parser
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed) public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed)
{ {
$item->language = $feed->language; $language = (string) $entry->attributes('xml', true)->{'lang'};
if ($language === '') {
$language = $feed->language;
}
$item->language = $language;
} }
/** /**
* Get the URL from a link tag * Get the URL from a link tag
* *
* @access public * @access private
* @param SimpleXMLElement $xml XML tag * @param SimpleXMLElement $xml XML tag
* @param string $rel Link relationship: alternate, enclosure, related, self, via
* @return string * @return string
*/ */
public function getLink(SimpleXMLElement $xml) private function getUrl(SimpleXMLElement $xml, $rel, $fallback = false)
{
$link = $this->findLink($xml, $rel);
if ($link) {
return (string) $link['href'];
}
if ($fallback) {
$link = $this->findLink($xml, '');
return $link ? (string) $link['href'] : '';
}
return '';
}
/**
* Get a link tag that match a relationship
*
* @access private
* @param SimpleXMLElement $xml XML tag
* @param string $rel Link relationship: alternate, enclosure, related, self, via
* @return SimpleXMLElement|null
*/
private function findLink(SimpleXMLElement $xml, $rel)
{ {
foreach ($xml->link as $link) { foreach ($xml->link as $link) {
if ((string) $link['type'] === 'text/html' || (string) $link['type'] === 'application/xhtml+xml') { if ($rel === (string) $link['rel']) {
return (string) $link['href']; return $link;
} }
} }
return (string) $xml->link['href']; return null;
} }
/** /**
* Get the entry content * Get the entry content
* *
* @access public * @access private
* @param SimpleXMLElement $entry XML Entry * @param SimpleXMLElement $entry XML Entry
* @return string * @return string
*/ */
public function getContent(SimpleXMLElement $entry) private function getContent(SimpleXMLElement $entry)
{ {
if (isset($entry->content) && ! empty($entry->content)) { if (isset($entry->content) && ! empty($entry->content)) {

View File

@ -1,12 +1,12 @@
<?php <?php
namespace PicoFeed; namespace PicoFeed\Parser;
/** /**
* Feed * Feed
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package picofeed * @package Parser
*/ */
class Feed class Feed
{ {
@ -48,7 +48,15 @@ class Feed
* @access public * @access public
* @var string * @var string
*/ */
public $url = ''; public $feed_url = '';
/**
* Site url
*
* @access public
* @var string
*/
public $site_url = '';
/** /**
* Feed date * Feed date
@ -84,10 +92,11 @@ class Feed
{ {
$output = ''; $output = '';
foreach (array('id', 'title', 'url', 'date', 'language', 'description', 'logo') as $property) { foreach (array('id', 'title', 'feed_url', 'site_url', 'date', 'language', 'description', 'logo') as $property) {
$output .= 'Feed::'.$property.' = '.$this->$property.PHP_EOL; $output .= 'Feed::'.$property.' = '.$this->$property.PHP_EOL;
} }
$output .= 'Feed::isRTL() = '.($this->isRTL() ? 'true' : 'false').PHP_EOL;
$output .= 'Feed::items = '.count($this->items).' items'.PHP_EOL; $output .= 'Feed::items = '.count($this->items).' items'.PHP_EOL;
foreach ($this->items as $item) { foreach ($this->items as $item) {
@ -132,14 +141,25 @@ class Feed
} }
/** /**
* Get url * Get feed url
* *
* @access public * @access public
* $return string * $return string
*/ */
public function getUrl() public function getFeedUrl()
{ {
return $this->url; return $this->feed_url;
}
/**
* Get site url
*
* @access public
* $return string
*/
public function getSiteUrl()
{
return $this->site_url;
} }
/** /**
@ -185,4 +205,15 @@ class Feed
{ {
return $this->items; return $this->items;
} }
/**
* Return true if the feed is "Right to Left"
*
* @access public
* @return bool
*/
public function isRTL()
{
return Parser::isLanguageRTL($this->language);
}
} }

View File

@ -1,15 +1,32 @@
<?php <?php
namespace PicoFeed; namespace PicoFeed\Parser;
/** /**
* Feed Item * Feed Item
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package picofeed * @package Parser
*/ */
class Item class Item
{ {
/**
* List of known RTL languages
*
* @access public
* @var public
*/
public $rtl = array(
'ar', // Arabic (ar-**)
'fa', // Farsi (fa-**)
'ur', // Urdu (ur-**)
'ps', // Pashtu (ps-**)
'syr', // Syriac (syr-**)
'dv', // Divehi (dv-**)
'he', // Hebrew (he-**)
'yi', // Yiddish (yi-**)
);
/** /**
* Item id * Item id
* *
@ -96,6 +113,7 @@ class Item
$output .= 'Item::'.$property.' = '.$this->$property.PHP_EOL; $output .= 'Item::'.$property.' = '.$this->$property.PHP_EOL;
} }
$output .= 'Item::isRTL() = '.($this->isRTL() ? 'true' : 'false').PHP_EOL;
$output .= 'Item::content = '.strlen($this->content).' bytes'.PHP_EOL; $output .= 'Item::content = '.strlen($this->content).' bytes'.PHP_EOL;
return $output; return $output;
@ -199,4 +217,15 @@ class Item
{ {
return $this->author; return $this->author;
} }
/**
* Return true if the item is "Right to Left"
*
* @access public
* @return bool
*/
public function isRTL()
{
return Parser::isLanguageRTL($this->language);
}
} }

View File

@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Parser;
/**
* MalformedXmlException Exception
*
* @author Frederic Guillot
* @package Parser
*/
class MalformedXmlException extends ParserException
{
}

View File

@ -1,16 +1,21 @@
<?php <?php
namespace PicoFeed; namespace PicoFeed\Parser;
use SimpleXMLElement; use SimpleXMLElement;
use DateTime; use DateTime;
use DateTimeZone; use DateTimeZone;
use PicoFeed\Encoding\Encoding;
use PicoFeed\Filter\Filter;
use PicoFeed\Logging\Logger;
use PicoFeed\Client\Url;
use PicoFeed\Client\Grabber;
/** /**
* Base parser class * Base parser class
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package parser * @package Parser
*/ */
abstract class Parser abstract class Parser
{ {
@ -18,9 +23,9 @@ abstract class Parser
* Config object * Config object
* *
* @access private * @access private
* @var \PicoFeed\Config * @var \PicoFeed\Config\Config
*/ */
private $config = null; private $config;
/** /**
* Hash algorithm used to generate item id, any value supported by PHP, see hash_algos() * Hash algorithm used to generate item id, any value supported by PHP, see hash_algos()
@ -28,7 +33,7 @@ abstract class Parser
* @access private * @access private
* @var string * @var string
*/ */
private $hash_algo = 'crc32b'; // crc32b seems to be faster and shorter than other hash algorithms private $hash_algo = 'sha256';
/** /**
* Timezone used to parse feed dates * Timezone used to parse feed dates
@ -90,9 +95,9 @@ abstract class Parser
* Constructor * Constructor
* *
* @access public * @access public
* @param string $content Feed content * @param string $content Feed content
* @param string $http_encoding HTTP encoding (headers) * @param string $http_encoding HTTP encoding (headers)
* @param string $base_url Fallback url when the feed provide relative or broken url * @param string $fallback_url Fallback url when the feed provide relative or broken url
*/ */
public function __construct($content, $http_encoding = '', $fallback_url = '') public function __construct($content, $http_encoding = '', $fallback_url = '')
{ {
@ -103,7 +108,7 @@ abstract class Parser
$this->content = Filter::stripXmlTag($content); $this->content = Filter::stripXmlTag($content);
// Encode everything in UTF-8 // Encode everything in UTF-8
Logging::setMessage(get_called_class().': HTTP Encoding "'.$http_encoding.'" ; XML Encoding "'.$xml_encoding.'"'); Logger::setMessage(get_called_class().': HTTP Encoding "'.$http_encoding.'" ; XML Encoding "'.$xml_encoding.'"');
$this->content = Encoding::convert($this->content, $xml_encoding ?: $http_encoding); $this->content = Encoding::convert($this->content, $xml_encoding ?: $http_encoding);
// Workarounds // Workarounds
@ -114,18 +119,18 @@ abstract class Parser
* Parse the document * Parse the document
* *
* @access public * @access public
* @return mixed \PicoFeed\Feed instance or false * @return \PicoFeed\Parser\Feed
*/ */
public function execute() public function execute()
{ {
Logging::setMessage(get_called_class().': begin parsing'); Logger::setMessage(get_called_class().': begin parsing');
$xml = XmlParser::getSimpleXml($this->content); $xml = XmlParser::getSimpleXml($this->content);
if ($xml === false) { if ($xml === false) {
Logging::setMessage(get_called_class().': XML parsing error'); Logger::setMessage(get_called_class().': XML parsing error');
Logging::setMessage(XmlParser::getErrors()); Logger::setMessage(XmlParser::getErrors());
return false; throw new MalformedXmlException('XML parsing error');
} }
$this->namespaces = $xml->getNamespaces(true); $this->namespaces = $xml->getNamespaces(true);
@ -135,6 +140,9 @@ abstract class Parser
$this->findFeedUrl($xml, $feed); $this->findFeedUrl($xml, $feed);
$this->checkFeedUrl($feed); $this->checkFeedUrl($feed);
$this->findSiteUrl($xml, $feed);
$this->checkSiteUrl($feed);
$this->findFeedTitle($xml, $feed); $this->findFeedTitle($xml, $feed);
$this->findFeedDescription($xml, $feed); $this->findFeedDescription($xml, $feed);
$this->findFeedLanguage($xml, $feed); $this->findFeedLanguage($xml, $feed);
@ -151,9 +159,12 @@ abstract class Parser
$this->checkItemUrl($feed, $item); $this->checkItemUrl($feed, $item);
$this->findItemTitle($entry, $item); $this->findItemTitle($entry, $item);
$this->findItemId($entry, $item, $feed);
$this->findItemDate($entry, $item);
$this->findItemContent($entry, $item); $this->findItemContent($entry, $item);
// Id generation can use the item url/title/content (order is important)
$this->findItemId($entry, $item, $feed);
$this->findItemDate($entry, $item);
$this->findItemEnclosure($entry, $item, $feed); $this->findItemEnclosure($entry, $item, $feed);
$this->findItemLanguage($entry, $item, $feed); $this->findItemLanguage($entry, $item, $feed);
@ -163,7 +174,7 @@ abstract class Parser
$feed->items[] = $item; $feed->items[] = $item;
} }
Logging::setMessage(get_called_class().PHP_EOL.$feed); Logger::setMessage(get_called_class().PHP_EOL.$feed);
return $feed; return $feed;
} }
@ -176,10 +187,27 @@ abstract class Parser
*/ */
public function checkFeedUrl(Feed $feed) public function checkFeedUrl(Feed $feed)
{ {
$url = new Url($feed->getUrl()); if ($feed->getFeedUrl() === '') {
$feed->feed_url = $this->fallback_url;
}
else {
$feed->feed_url = Url::resolve($feed->getFeedUrl(), $this->fallback_url);
}
}
if ($url->isRelativeUrl()) { /**
$feed->url = $this->fallback_url; * Check if the site url is correct
*
* @access public
* @param Feed $feed Feed object
*/
public function checkSiteUrl(Feed $feed)
{
if ($feed->getSiteUrl() === '') {
$feed->site_url = Url::base($feed->getFeedUrl());
}
else {
$feed->site_url = Url::resolve($feed->getSiteUrl(), $this->fallback_url);
} }
} }
@ -192,11 +220,7 @@ abstract class Parser
*/ */
public function checkItemUrl(Feed $feed, Item $item) public function checkItemUrl(Feed $feed, Item $item)
{ {
$url = new Url($item->getUrl()); $item->url = Url::resolve($item->getUrl(), $feed->getSiteUrl());
if ($url->isRelativeUrl()) {
$item->url = Url::resolve($item->getUrl(), $feed->getUrl());
}
} }
/** /**
@ -229,12 +253,12 @@ abstract class Parser
public function filterItemContent(Feed $feed, Item $item) public function filterItemContent(Feed $feed, Item $item)
{ {
if ($this->isFilteringEnabled()) { if ($this->isFilteringEnabled()) {
$filter = Filter::html($item->getContent(), $feed->getUrl()); $filter = Filter::html($item->getContent(), $feed->getSiteUrl());
$filter->setConfig($this->config); $filter->setConfig($this->config);
$item->content = $filter->execute(); $item->content = $filter->execute();
} }
else { else {
Logging::setMessage(get_called_class().': Content filtering disabled'); Logger::setMessage(get_called_class().': Content filtering disabled');
} }
} }
@ -243,7 +267,7 @@ abstract class Parser
* *
* @access public * @access public
* @param string $args Pieces of data to hash * @param string $args Pieces of data to hash
* @return string Id * @return string
*/ */
public function generateId() public function generateId()
{ {
@ -331,24 +355,6 @@ abstract class Parser
return 0; return 0;
} }
/**
* Hardcoded list of hostname/token to exclude from id generation
*
* @access public
* @param string $url URL
* @return boolean
*/
public function isExcludedFromId($url)
{
$exclude_list = array('ap.org', 'jacksonville.com');
foreach ($exclude_list as $token) {
if (strpos($url, $token) !== false) return true;
}
return false;
}
/** /**
* Return true if the given language is "Right to Left" * Return true if the given language is "Right to Left"
* *
@ -386,7 +392,7 @@ abstract class Parser
* *
* @access public * @access public
* @param string $algo Algorithm name * @param string $algo Algorithm name
* @return \PicoFeed\Parser * @return \PicoFeed\Parser\Parser
*/ */
public function setHashAlgo($algo) public function setHashAlgo($algo)
{ {
@ -400,7 +406,7 @@ abstract class Parser
* @see http://php.net/manual/en/timezones.php * @see http://php.net/manual/en/timezones.php
* @access public * @access public
* @param string $timezone Timezone * @param string $timezone Timezone
* @return \PicoFeed\Parser * @return \PicoFeed\Parser\Parser
*/ */
public function setTimezone($timezone) public function setTimezone($timezone)
{ {
@ -412,8 +418,8 @@ abstract class Parser
* Set config object * Set config object
* *
* @access public * @access public
* @param \PicoFeed\Config $config Config instance * @param \PicoFeed\Config\Config $config Config instance
* @return \PicoFeed\Parser * @return \PicoFeed\Parser\Parser
*/ */
public function setConfig($config) public function setConfig($config)
{ {
@ -425,7 +431,7 @@ abstract class Parser
* Enable the content grabber * Enable the content grabber
* *
* @access public * @access public
* @return \PicoFeed\Parser * @return \PicoFeed\Parser\Parser
*/ */
public function disableContentFiltering() public function disableContentFiltering()
{ {
@ -451,7 +457,7 @@ abstract class Parser
* Enable the content grabber * Enable the content grabber
* *
* @access public * @access public
* @return \PicoFeed\Parser * @return \PicoFeed\Parser\Parser
*/ */
public function enableContentGrabber() public function enableContentGrabber()
{ {
@ -463,7 +469,7 @@ abstract class Parser
* *
* @access public * @access public
* @param array $urls URLs * @param array $urls URLs
* @return \PicoFeed\Parser * @return \PicoFeed\Parser\Parser
*/ */
public function setGrabberIgnoreUrls(array $urls) public function setGrabberIgnoreUrls(array $urls)
{ {
@ -474,17 +480,26 @@ abstract class Parser
* Find the feed url * Find the feed url
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public abstract function findFeedUrl(SimpleXMLElement $xml, Feed $feed); public abstract function findFeedUrl(SimpleXMLElement $xml, Feed $feed);
/**
* Find the site url
*
* @access public
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public abstract function findSiteUrl(SimpleXMLElement $xml, Feed $feed);
/** /**
* Find the feed title * Find the feed title
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public abstract function findFeedTitle(SimpleXMLElement $xml, Feed $feed); public abstract function findFeedTitle(SimpleXMLElement $xml, Feed $feed);
@ -492,8 +507,8 @@ abstract class Parser
* Find the feed description * Find the feed description
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public abstract function findFeedDescription(SimpleXMLElement $xml, Feed $feed); public abstract function findFeedDescription(SimpleXMLElement $xml, Feed $feed);
@ -501,8 +516,8 @@ abstract class Parser
* Find the feed language * Find the feed language
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public abstract function findFeedLanguage(SimpleXMLElement $xml, Feed $feed); public abstract function findFeedLanguage(SimpleXMLElement $xml, Feed $feed);
@ -510,8 +525,8 @@ abstract class Parser
* Find the feed id * Find the feed id
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public abstract function findFeedId(SimpleXMLElement $xml, Feed $feed); public abstract function findFeedId(SimpleXMLElement $xml, Feed $feed);
@ -519,8 +534,8 @@ abstract class Parser
* Find the feed date * Find the feed date
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public abstract function findFeedDate(SimpleXMLElement $xml, Feed $feed); public abstract function findFeedDate(SimpleXMLElement $xml, Feed $feed);
@ -528,8 +543,8 @@ abstract class Parser
* Find the feed logo url * Find the feed logo url
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public abstract function findFeedLogo(SimpleXMLElement $xml, Feed $feed); public abstract function findFeedLogo(SimpleXMLElement $xml, Feed $feed);
@ -546,9 +561,9 @@ abstract class Parser
* Find the item author * Find the item author
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed * @param SimpleXMLElement $xml Feed
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public abstract function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item); public abstract function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item);
@ -556,8 +571,8 @@ abstract class Parser
* Find the item URL * Find the item URL
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public abstract function findItemUrl(SimpleXMLElement $entry, Item $item); public abstract function findItemUrl(SimpleXMLElement $entry, Item $item);
@ -565,8 +580,8 @@ abstract class Parser
* Find the item title * Find the item title
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public abstract function findItemTitle(SimpleXMLElement $entry, Item $item); public abstract function findItemTitle(SimpleXMLElement $entry, Item $item);
@ -574,9 +589,9 @@ abstract class Parser
* Genereate the item id * Genereate the item id
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public abstract function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed); public abstract function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed);
@ -584,8 +599,8 @@ abstract class Parser
* Find the item date * Find the item date
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public abstract function findItemDate(SimpleXMLElement $entry, Item $item); public abstract function findItemDate(SimpleXMLElement $entry, Item $item);
@ -593,8 +608,8 @@ abstract class Parser
* Find the item content * Find the item content
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public abstract function findItemContent(SimpleXMLElement $entry, Item $item); public abstract function findItemContent(SimpleXMLElement $entry, Item $item);
@ -602,9 +617,9 @@ abstract class Parser
* Find the item enclosure * Find the item enclosure
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public abstract function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed); public abstract function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed);
@ -612,9 +627,9 @@ abstract class Parser
* Find the item language * Find the item language
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public abstract function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed); public abstract function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed);
} }

View File

@ -0,0 +1,16 @@
<?php
namespace PicoFeed\Parser;
use PicoFeed\PicoFeedException;
/**
* ParserException Exception
*
* @author Frederic Guillot
* @package Parser
*/
abstract class ParserException extends PicoFeedException
{
}

View File

@ -1,20 +1,14 @@
<?php <?php
namespace PicoFeed\Parsers; namespace PicoFeed\Parser;
require_once __DIR__.'/Rss20.php';
use SimpleXMLElement; use SimpleXMLElement;
use PicoFeed\Feed;
use PicoFeed\Item;
use PicoFeed\XmlParser;
use PicoFeed\Parsers\Rss20;
/** /**
* RSS 1.0 parser * RSS 1.0 parser
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package parser * @package Parser
*/ */
class Rss10 extends Rss20 class Rss10 extends Rss20
{ {
@ -35,7 +29,7 @@ class Rss10 extends Rss20
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedDate(SimpleXMLElement $xml, Feed $feed) public function findFeedDate(SimpleXMLElement $xml, Feed $feed)
{ {
@ -47,7 +41,7 @@ class Rss10 extends Rss20
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed)
{ {
@ -59,19 +53,14 @@ class Rss10 extends Rss20
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed)
{ {
if ($this->isExcludedFromId($feed->url)) { $item->id = $this->generateId(
$feed_permalink = ''; $item->getTitle(), $item->getUrl(), $item->getContent()
} );
else {
$feed_permalink = $feed->url;
}
$item->id = $this->generateId($item->url, $feed_permalink);
} }
/** /**
@ -79,8 +68,8 @@ class Rss10 extends Rss20
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed)
{ {

View File

@ -1,21 +1,16 @@
<?php <?php
namespace PicoFeed\Parsers; namespace PicoFeed\Parser;
use SimpleXMLElement; use SimpleXMLElement;
use PicoFeed\Parser; use PicoFeed\Filter\Filter;
use PicoFeed\XmlParser; use PicoFeed\Client\Url;
use PicoFeed\Logging;
use PicoFeed\Feed;
use PicoFeed\Filter;
use PicoFeed\Item;
use PicoFeed\Url;
/** /**
* RSS 2.0 Parser * RSS 2.0 Parser
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package parser * @package Parser
*/ */
class Rss20 extends Parser class Rss20 extends Parser
{ {
@ -35,35 +30,32 @@ class Rss20 extends Parser
* Find the feed url * Find the feed url
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedUrl(SimpleXMLElement $xml, Feed $feed) public function findFeedUrl(SimpleXMLElement $xml, Feed $feed)
{ {
if ($xml->channel->link && $xml->channel->link->count() > 1) { $feed->feed_url = '';
}
foreach ($xml->channel->link as $xml_link) { /**
* Find the site url
$link = (string) $xml_link; *
* @access public
if ($link !== '') { * @param SimpleXMLElement $xml Feed xml
$feed->url = $link; * @param \PicoFeed\Parser\Feed $feed Feed object
break; */
} public function findSiteUrl(SimpleXMLElement $xml, Feed $feed)
} {
} $feed->site_url = (string) $xml->channel->link;
else {
$feed->url = (string) $xml->channel->link;
}
} }
/** /**
* Find the feed description * Find the feed description
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedDescription(SimpleXMLElement $xml, Feed $feed) public function findFeedDescription(SimpleXMLElement $xml, Feed $feed)
{ {
@ -74,8 +66,8 @@ class Rss20 extends Parser
* Find the feed logo url * Find the feed logo url
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedLogo(SimpleXMLElement $xml, Feed $feed) public function findFeedLogo(SimpleXMLElement $xml, Feed $feed)
{ {
@ -88,20 +80,20 @@ class Rss20 extends Parser
* Find the feed title * Find the feed title
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedTitle(SimpleXMLElement $xml, Feed $feed) public function findFeedTitle(SimpleXMLElement $xml, Feed $feed)
{ {
$feed->title = Filter::stripWhiteSpace((string) $xml->channel->title) ?: $feed->url; $feed->title = Filter::stripWhiteSpace((string) $xml->channel->title) ?: $feed->getSiteUrl();
} }
/** /**
* Find the feed language * Find the feed language
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed) public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed)
{ {
@ -112,20 +104,20 @@ class Rss20 extends Parser
* Find the feed id * Find the feed id
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedId(SimpleXMLElement $xml, Feed $feed) public function findFeedId(SimpleXMLElement $xml, Feed $feed)
{ {
$feed->id = $feed->url; $feed->id = $feed->getFeedUrl() ?: $feed->getSiteUrl();
} }
/** /**
* Find the feed date * Find the feed date
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findFeedDate(SimpleXMLElement $xml, Feed $feed) public function findFeedDate(SimpleXMLElement $xml, Feed $feed)
{ {
@ -137,8 +129,8 @@ class Rss20 extends Parser
* Find the item date * Find the item date
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public function findItemDate(SimpleXMLElement $entry, Item $item) public function findItemDate(SimpleXMLElement $entry, Item $item)
{ {
@ -159,8 +151,8 @@ class Rss20 extends Parser
* Find the item title * Find the item title
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public function findItemTitle(SimpleXMLElement $entry, Item $item) public function findItemTitle(SimpleXMLElement $entry, Item $item)
{ {
@ -175,9 +167,9 @@ class Rss20 extends Parser
* Find the item author * Find the item author
* *
* @access public * @access public
* @param SimpleXMLElement $xml Feed * @param SimpleXMLElement $xml Feed
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item) public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item)
{ {
@ -197,8 +189,8 @@ class Rss20 extends Parser
* Find the item content * Find the item content
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public function findItemContent(SimpleXMLElement $entry, Item $item) public function findItemContent(SimpleXMLElement $entry, Item $item)
{ {
@ -215,8 +207,8 @@ class Rss20 extends Parser
* Find the item URL * Find the item URL
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
*/ */
public function findItemUrl(SimpleXMLElement $entry, Item $item) public function findItemUrl(SimpleXMLElement $entry, Item $item)
{ {
@ -239,26 +231,21 @@ class Rss20 extends Parser
* Genereate the item id * Genereate the item id
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed) public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed)
{ {
$item_permalink = $item->url; $id = (string) $entry->guid;
if ($this->isExcludedFromId($feed->url)) { if ($id) {
$feed_permalink = ''; $item->id = $this->generateId($id);
} }
else { else {
$feed_permalink = $feed->url; $item->id = $this->generateId(
} $item->getTitle(), $item->getUrl(), $item->getContent()
);
if ($entry->guid->count() > 0 && ((string) $entry->guid['isPermaLink'] === 'false' || ! isset($entry->guid['isPermaLink']))) {
$item->id = $this->generateId($item_permalink, $feed_permalink, (string) $entry->guid);
}
else {
$item->id = $this->generateId($item_permalink, $feed_permalink);
} }
} }
@ -266,9 +253,9 @@ class Rss20 extends Parser
* Find the item enclosure * Find the item enclosure
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed) public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed)
{ {
@ -281,7 +268,7 @@ class Rss20 extends Parser
} }
$item->enclosure_type = isset($entry->enclosure['type']) ? (string) $entry->enclosure['type'] : ''; $item->enclosure_type = isset($entry->enclosure['type']) ? (string) $entry->enclosure['type'] : '';
$item->enclosure_url = Url::resolve($item->enclosure_url, $feed->url); $item->enclosure_url = Url::resolve($item->enclosure_url, $feed->getSiteUrl());
} }
} }
@ -290,8 +277,8 @@ class Rss20 extends Parser
* *
* @access public * @access public
* @param SimpleXMLElement $entry Feed item * @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Item $item Item object * @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Feed $feed Feed object * @param \PicoFeed\Parser\Feed $feed Feed object
*/ */
public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed) public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed)
{ {

View File

@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Parser;
/**
* RSS 0.91 Parser
*
* @author Frederic Guillot
* @package Parser
*/
class Rss91 extends Rss20
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Parser;
/**
* RSS 0.92 Parser
*
* @author Frederic Guillot
* @package Parser
*/
class Rss92 extends Rss20
{
}

View File

@ -1,7 +1,8 @@
<?php <?php
namespace PicoFeed; namespace PicoFeed\Parser;
use Closure;
use DomDocument; use DomDocument;
use DOMXPath; use DOMXPath;
use SimpleXmlElement; use SimpleXmlElement;
@ -12,7 +13,7 @@ use SimpleXmlElement;
* Checks for XML eXternal Entity (XXE) and XML Entity Expansion (XEE) attacks on XML documents * Checks for XML eXternal Entity (XXE) and XML Entity Expansion (XEE) attacks on XML documents
* *
* @author Frederic Guillot * @author Frederic Guillot
* @package picofeed * @package Parser
*/ */
class XmlParser class XmlParser
{ {
@ -43,14 +44,16 @@ class XmlParser
} }
/** /**
* Get a DomDocument instance or return false * Scan the input for XXE attacks
* *
* @static * @param string $input Unsafe input
* @access public * @param Closure $callback Callback called to build the dom.
* @param string $input XML content * Must be an instance of DomDocument and receives the input as argument
* @return mixed *
* @return bool|DomDocument False if an XXE attack was discovered,
* otherwise the return of the callback
*/ */
public static function getDomDocument($input) private static function scanInput($input, Closure $callback)
{ {
if (substr(php_sapi_name(), 0, 3) === 'fpm') { if (substr(php_sapi_name(), 0, 3) === 'fpm') {
@ -67,13 +70,7 @@ class XmlParser
libxml_use_internal_errors(true); libxml_use_internal_errors(true);
$dom = new DomDocument; $dom = $callback($input);
$dom->loadXml($input, LIBXML_NONET);
// The document is empty, there is probably some parsing errors
if ($dom->childNodes->length === 0) {
return false;
}
// Scan for potential XEE attacks using ENTITY // Scan for potential XEE attacks using ENTITY
foreach ($dom->childNodes as $child) { foreach ($dom->childNodes as $child) {
@ -87,28 +84,56 @@ class XmlParser
return $dom; return $dom;
} }
/**
* Get a DomDocument instance or return false
*
* @static
* @access public
* @param string $input XML content
* @return \DOMNode
*/
public static function getDomDocument($input)
{
$dom = self::scanInput($input, function ($in) {
$dom = new DomDocument;
$dom->loadXml($in, LIBXML_NONET);
return $dom;
});
// The document is empty, there is probably some parsing errors
if ($dom && $dom->childNodes->length === 0) {
return false;
}
return $dom;
}
/** /**
* Load HTML document by using a DomDocument instance or return false on failure * Load HTML document by using a DomDocument instance or return false on failure
* *
* @static * @static
* @access public * @access public
* @param string $input XML content * @param string $input XML content
* @return mixed * @return \DOMDocument
*/ */
public static function getHtmlDocument($input) public static function getHtmlDocument($input)
{ {
libxml_use_internal_errors(true);
$dom = new DomDocument;
if (version_compare(PHP_VERSION, '5.4.0', '>=')) { if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
$dom->loadHTML($input, LIBXML_NONET); $callback = function ($in) {
$dom = new DomDocument;
$dom->loadHTML($in, LIBXML_NONET);
return $dom;
};
} }
else { else {
$dom->loadHTML($input); $callback = function ($in) {
$dom = new DomDocument;
$dom->loadHTML($in);
return $dom;
};
} }
return $dom; return self::scanInput($input, $callback);
} }
/** /**
@ -201,7 +226,7 @@ class XmlParser
* *
* @static * @static
* @access public * @access public
* @param SimpleXMLElement $xml XML element * @param \SimpleXMLElement $xml XML element
* @param array $namespaces XML namespaces * @param array $namespaces XML namespaces
* @param string $property XML tag name * @param string $property XML tag name
* @param string $attribute XML attribute name * @param string $attribute XML attribute name

Some files were not shown because too many files have changed in this diff Show More