forked from blallo/rss-bridge
Merge pull request #4 from Draeli/new_feature
Refactoring - Temporary merge.
This commit is contained in:
commit
37b14f459b
21 changed files with 1164 additions and 343 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,7 +1,7 @@
|
||||||
#################
|
#################
|
||||||
## Eclipse
|
## Eclipse
|
||||||
#################
|
#################
|
||||||
simple_html_dom.php
|
vendor/*
|
||||||
data/
|
data/
|
||||||
*.pydevproject
|
*.pydevproject
|
||||||
.project
|
.project
|
||||||
|
|
33
README.md
33
README.md
|
@ -6,36 +6,30 @@ rss-bridge is a collection of independant php scripts capable of generating ATOM
|
||||||
Supported sites/pages
|
Supported sites/pages
|
||||||
===
|
===
|
||||||
|
|
||||||
* `rss-bridge-flickr-explore.php` : [Latest interesting images](http://www.flickr.com/explore) from Flickr.
|
* `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr.
|
||||||
* `rss-bridge-googlesearch.php` : Most recent results from Google Search. Parameters:
|
* `GoogleSearch` : Most recent results from Google Search. Parameters:
|
||||||
* q=keyword : Keyword search.
|
* q=keyword : Keyword search.
|
||||||
* `rss-bridge-twitter.php` : Twitter. Parameters:
|
* `Twitter` : Twitter. Parameters:
|
||||||
* q=keyword : Keyword search.
|
* q=keyword : Keyword search.
|
||||||
* u=username : Get user timeline.
|
* u=username : Get user timeline.
|
||||||
|
|
||||||
|
Easy new bridge system (detail below) !
|
||||||
|
|
||||||
Output format
|
Output format
|
||||||
===
|
===
|
||||||
Output format can be used in any rss-bridge:
|
Output format can be used in any rss-bridge:
|
||||||
|
|
||||||
* `format=atom` (default): ATOM Feed.
|
* `Atom` : ATOM Feed.
|
||||||
* `format=json` : jSon
|
* `Json` : Json
|
||||||
* `format=html` : html page
|
* `Html` : html page
|
||||||
* `format=plaintext` : raw text (php object, as returned by print_r)
|
* `Plaintext` : raw text (php object, as returned by print_r)
|
||||||
|
|
||||||
If format is not specified, ATOM format will be used.
|
|
||||||
|
|
||||||
Examples
|
|
||||||
===
|
|
||||||
* `rss-bridge-twitter.php?u=Dinnerbone` : Get Dinnerbone (Minecraft developer) timeline, in ATOM format.
|
|
||||||
* `rss-bridge-twitter.php?q=minecraft&format=html` : Everything Minecraft from Twitter, in html format.
|
|
||||||
* `rss-bridge-flickr-explore.php` : Latest interesting images from Flickr, in ATOM format.
|
|
||||||
|
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
===
|
===
|
||||||
|
|
||||||
* php 5.3
|
* php 5.3
|
||||||
* [PHP Simple HTML DOM Parser](http://simplehtmldom.sourceforge.net/)
|
* [PHP Simple HTML DOM Parser](http://simplehtmldom.sourceforge.net)
|
||||||
|
* Ssl lib activated in PHP config
|
||||||
|
|
||||||
Author
|
Author
|
||||||
===
|
===
|
||||||
|
@ -43,6 +37,9 @@ I'm sebsauvage, webmaster of [sebsauvage.net](http://sebsauvage.net), author of
|
||||||
|
|
||||||
Thanks to [Mitsukarenai](https://github.com/Mitsukarenai) for the inspiration.
|
Thanks to [Mitsukarenai](https://github.com/Mitsukarenai) for the inspiration.
|
||||||
|
|
||||||
|
Patch :
|
||||||
|
- Yves ASTIER (Draeli) : PHP optimizations, fixes, dynamic brigde/format list with all stuff behind and extend cache system. Mail : contact@yves-astier.com
|
||||||
|
|
||||||
Licence
|
Licence
|
||||||
===
|
===
|
||||||
Code is public domain.
|
Code is public domain.
|
||||||
|
@ -51,7 +48,7 @@ Code is public domain.
|
||||||
Technical notes
|
Technical notes
|
||||||
===
|
===
|
||||||
* There is a cache so that source services won't ban you even if you hammer the rss-bridge with requests. Each bridge has a different duration for the cache. The `cache` subdirectory will be automatically created. You can purge it whenever you want.
|
* There is a cache so that source services won't ban you even if you hammer the rss-bridge with requests. Each bridge has a different duration for the cache. The `cache` subdirectory will be automatically created. You can purge it whenever you want.
|
||||||
* To implement a new rss-bridge, import `rss-bridge-lib.php` and subclass `RssBridgeAbstractClass`. Look at existing bridges for examples. For items you generate in `$this->items`, only `uri` and `title` are mandatory in each item. `timestamp` and `content` are optional but recommended. Any additional key will be ignored by ATOM feed (but outputed to jSon).
|
* To implement a new rss-bridge, create a new class in `bridges` directory and extends with `BridgeAbstract`. Look at existing bridges for examples. For items you generate in `$this->items`, only `uri` and `title` are mandatory in each item. `timestamp` and `content` are optional but recommended. Any additional key will be ignored by ATOM feed (but outputed to jSon). If you want your new bridge appear in `index.php`, don't forget add annotation.
|
||||||
|
|
||||||
Rant
|
Rant
|
||||||
===
|
===
|
||||||
|
|
35
bridges/FlickrExploreBridge.php
Normal file
35
bridges/FlickrExploreBridge.php
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* RssBridgeFlickrExplore
|
||||||
|
* Returns the newest interesting images from http://www.flickr.com/explore
|
||||||
|
*
|
||||||
|
* @name Flickr Explore
|
||||||
|
* @description Returns the latest interesting images from Flickr
|
||||||
|
*/
|
||||||
|
class FlickrExploreBridge extends BridgeAbstract{
|
||||||
|
|
||||||
|
public function collectData(array $param){
|
||||||
|
$html = file_get_html('http://www.flickr.com/explore') or $this->returnError('Could not request Flickr.', 404);
|
||||||
|
|
||||||
|
foreach($html->find('span.photo_container') as $element) {
|
||||||
|
$item = new \Item();
|
||||||
|
$item->uri = 'http://flickr.com'.$element->find('a',0)->href;
|
||||||
|
$item->thumbnailUri = $element->find('img',0)->getAttribute('data-defer-src');
|
||||||
|
$item->content = '<a href="' . $item->uri . '"><img src="' . $item->thumbnailUri . '" /></a>'; // FIXME: Filter javascript ?
|
||||||
|
$item->title = $element->find('a',0)->title;
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(){
|
||||||
|
return 'Flickr Explore';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getURI(){
|
||||||
|
return 'http://www.flickr.com/explore';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCacheDuration(){
|
||||||
|
return 21600; // 6 hours
|
||||||
|
}
|
||||||
|
}
|
51
bridges/GoogleSearchBridge.php
Normal file
51
bridges/GoogleSearchBridge.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* RssBridgeGoogleMostRecent
|
||||||
|
* Search Google for most recent pages regarding a specific topic.
|
||||||
|
* Returns the 100 most recent links in results in past year, sorting by date (most recent first).
|
||||||
|
* Example:
|
||||||
|
* http://www.google.com/search?q=sebsauvage&num=100&complete=0&tbs=qdr:y,sbd:1
|
||||||
|
* complete=0&num=100 : get 100 results
|
||||||
|
* qdr:y : in past year
|
||||||
|
* sbd:1 : sort by date (will only work if qdr: is specified)
|
||||||
|
*
|
||||||
|
* @name Google search
|
||||||
|
* @description Returns most recent results from Google search.
|
||||||
|
* @use1(q="keyword search")
|
||||||
|
*/
|
||||||
|
class GoogleSearchBridge extends BridgeAbstract{
|
||||||
|
|
||||||
|
public function collectData(array $param){
|
||||||
|
$html = '';
|
||||||
|
|
||||||
|
if (isset($param['q'])) { /* keyword search mode */
|
||||||
|
$html = file_get_html('http://www.google.com/search?q=' . urlencode($param['q']) . '&num=100&complete=0&tbs=qdr:y,sbd:1') or $this->returnError('No results for this query.', 404);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$this->returnError('You must specify a keyword (?q=...).', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$emIsRes = $html->find('div[id=ires]',0);
|
||||||
|
if( !is_null($emIsRes) ){
|
||||||
|
foreach($emIsRes->find('li[class=g]') as $element) {
|
||||||
|
$item = new \Item();
|
||||||
|
$item->uri = $element->find('a[href]',0)->href;
|
||||||
|
$item->title = $element->find('h3',0)->plaintext;
|
||||||
|
$item->content = $element->find('span[class=st]',0)->plaintext;
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(){
|
||||||
|
return 'Google search';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getURI(){
|
||||||
|
return 'http://google.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCacheDuration(){
|
||||||
|
return 1800; // 30 minutes
|
||||||
|
}
|
||||||
|
}
|
50
bridges/TwitterBridge.php
Normal file
50
bridges/TwitterBridge.php
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* RssBridgeTwitter
|
||||||
|
* Based on https://github.com/mitsukarenai/twitterbridge-noapi
|
||||||
|
*
|
||||||
|
* @name Twitter Bridge
|
||||||
|
* @description Returns user timelines or keyword search from http://twitter.com without using their API.
|
||||||
|
* @use1(q="keyword search")
|
||||||
|
* @use2(u="user timeline mode")
|
||||||
|
*/
|
||||||
|
class TwitterBridge extends BridgeAbstract{
|
||||||
|
|
||||||
|
public function collectData(array $param){
|
||||||
|
$html = '';
|
||||||
|
if (isset($param['q'])) { /* keyword search mode */
|
||||||
|
$html = file_get_html('http://twitter.com/search/realtime?q='.urlencode($param['q']).'+include:retweets&src=typd') or $this->returnError('No results for this query.', 404);
|
||||||
|
}
|
||||||
|
elseif (isset($param['u'])) { /* user timeline mode */
|
||||||
|
$html = file_get_html('http://twitter.com/'.urlencode($param['u'])) or $this->returnError('Requested username can\'t be found.', 404);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->returnError('You must specify a keyword (?q=...) or a Twitter username (?u=...).', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($html->find('div.tweet') as $tweet) {
|
||||||
|
$item = new \Item();
|
||||||
|
$item->username = trim(substr($tweet->find('span.username', 0)->plaintext, 1)); // extract username and sanitize
|
||||||
|
$item->fullname = $tweet->getAttribute('data-name'); // extract fullname (pseudonym)
|
||||||
|
$item->avatar = $tweet->find('img', 0)->src; // get avatar link
|
||||||
|
$item->id = $tweet->getAttribute('data-tweet-id'); // get TweetID
|
||||||
|
$item->uri = 'https://twitter.com'.$tweet->find('a.details', 0)->getAttribute('href'); // get tweet link
|
||||||
|
$item->timestamp = $tweet->find('span._timestamp', 0)->getAttribute('data-time'); // extract tweet timestamp
|
||||||
|
$item->content = str_replace('href="/', 'href="https://twitter.com/', strip_tags($tweet->find('p.tweet-text', 0)->innertext, '<a>')); // extract tweet text
|
||||||
|
$item->title = $item->fullname . ' (@'. $item->username . ') | ' . $item->content;
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(){
|
||||||
|
return 'Twitter Bridge';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getURI(){
|
||||||
|
return 'http://twitter.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCacheDuration(){
|
||||||
|
return 300; // 5 minutes
|
||||||
|
}
|
||||||
|
}
|
92
caches/FileCache.php
Normal file
92
caches/FileCache.php
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Cache with file system
|
||||||
|
*/
|
||||||
|
class FileCache extends CacheAbstract{
|
||||||
|
protected $cacheDirCreated; // boolean to avoid always chck dir cache existance
|
||||||
|
|
||||||
|
public function loadData(){
|
||||||
|
$this->isPrepareCache();
|
||||||
|
|
||||||
|
$datas = json_decode(file_get_contents($this->getCacheFile()),true);
|
||||||
|
$items = array();
|
||||||
|
foreach($datas as $aData){
|
||||||
|
$item = new \Item();
|
||||||
|
foreach($aData as $name => $value){
|
||||||
|
$item->$name = $value;
|
||||||
|
}
|
||||||
|
$items[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveData($datas){
|
||||||
|
$this->isPrepareCache();
|
||||||
|
|
||||||
|
file_put_contents($this->getCacheFile(), json_encode($datas));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTime(){
|
||||||
|
$this->isPrepareCache();
|
||||||
|
|
||||||
|
$cacheFile = $this->getCacheFile();
|
||||||
|
if( file_exists($cacheFile) ){
|
||||||
|
return filemtime($cacheFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache is prepared ?
|
||||||
|
* Note : Cache name is based on request information, then cache must be prepare before use
|
||||||
|
* @return \Exception|true
|
||||||
|
*/
|
||||||
|
protected function isPrepareCache(){
|
||||||
|
if( is_null($this->param) ){
|
||||||
|
throw new \Exception('Please feed "prepare" method before try to load');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return cache path (and create if not exist)
|
||||||
|
* @return string Cache path
|
||||||
|
*/
|
||||||
|
protected function getCachePath(){
|
||||||
|
$cacheDir = __DIR__ . '/../cache/'; // FIXME : configuration ?
|
||||||
|
|
||||||
|
// FIXME : implement recursive dir creation
|
||||||
|
if( is_null($this->cacheDirCreated) && !is_dir($cacheDir) ){
|
||||||
|
$this->cacheDirCreated = true;
|
||||||
|
|
||||||
|
mkdir($cacheDir,0705);
|
||||||
|
chmod($cacheDir,0705);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file name use for cache store
|
||||||
|
* @return string Path to the file cache
|
||||||
|
*/
|
||||||
|
protected function getCacheFile(){
|
||||||
|
return $this->getCachePath() . $this->getCacheName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines file name for store the cache
|
||||||
|
* return string
|
||||||
|
*/
|
||||||
|
protected function getCacheName(){
|
||||||
|
$this->isPrepareCache();
|
||||||
|
|
||||||
|
$stringToEncode = $_SERVER['REQUEST_URI'] . http_build_query($this->param);
|
||||||
|
return hash('sha1', $stringToEncode) . '.cache';
|
||||||
|
}
|
||||||
|
}
|
79
formats/AtomFormat.php
Normal file
79
formats/AtomFormat.php
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Atom
|
||||||
|
* Documentation Source http://en.wikipedia.org/wiki/Atom_%28standard%29 and http://tools.ietf.org/html/rfc4287
|
||||||
|
*
|
||||||
|
* @name Atom
|
||||||
|
*/
|
||||||
|
class AtomFormat extends FormatAbstract{
|
||||||
|
|
||||||
|
public function stringify(){
|
||||||
|
/* Datas preparation */
|
||||||
|
$https = ( isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 's' : '' );
|
||||||
|
$httpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
|
||||||
|
$httpInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
|
||||||
|
|
||||||
|
$serverRequestUri = htmlspecialchars($_SERVER['REQUEST_URI']);
|
||||||
|
|
||||||
|
$extraInfos = $this->getExtraInfos();
|
||||||
|
$title = htmlspecialchars($extraInfos['name']);
|
||||||
|
$uri = htmlspecialchars($extraInfos['uri']);
|
||||||
|
|
||||||
|
$entries = '';
|
||||||
|
foreach($this->getDatas() as $data){
|
||||||
|
$entryName = is_null($data->name) ? $title : $data->name;
|
||||||
|
$entryAuthor = is_null($data->author) ? $uri : $data->author;
|
||||||
|
$entryTitle = is_null($data->title) ? '' : $data->title;
|
||||||
|
$entryUri = is_null($data->uri) ? '' : $data->uri;
|
||||||
|
$entryTimestamp = is_null($data->timestamp) ? '' : date(DATE_ATOM, $data->timestamp);
|
||||||
|
$entryContent = is_null($data->content) ? '' : '<![CDATA[' . htmlentities($data->content) . ']]>';
|
||||||
|
|
||||||
|
$entries .= <<<EOD
|
||||||
|
|
||||||
|
<entry>
|
||||||
|
<author>
|
||||||
|
<name>{$entryName}</name>
|
||||||
|
<uri>{$entryAuthor}</uri>
|
||||||
|
</author>
|
||||||
|
<title type="html"><![CDATA[{$entryTitle}]]></title>
|
||||||
|
<link rel="alternate" type="text/html" href="{$entryUri}" />
|
||||||
|
<id>{$entryUri}</id>
|
||||||
|
<updated>{$entryTimestamp}</updated>
|
||||||
|
<content type="html">{$entryContent}</content>
|
||||||
|
</entry>
|
||||||
|
|
||||||
|
EOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO :
|
||||||
|
- Security: Disable Javascript ?
|
||||||
|
- <updated> : Define new extra info ?
|
||||||
|
- <content type="html"> : RFC look with xhtml, keep this in spite of ?
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Data are prepared, now let's begin the "MAGIE !!!" */
|
||||||
|
$toReturn = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||||
|
$toReturn .= <<<EOD
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xml:lang="en-US">
|
||||||
|
|
||||||
|
<title type="text">{$title}</title>
|
||||||
|
<id>http{$https}://{$httpHost}{$httpInfo}/</id>
|
||||||
|
<updated></updated>
|
||||||
|
<link rel="alternate" type="text/html" href="{$uri}" />
|
||||||
|
<link rel="self" href="http{$https}://{$httpHost}{$serverRequestUri}" />
|
||||||
|
{$entries}
|
||||||
|
</feed>
|
||||||
|
EOD;
|
||||||
|
|
||||||
|
return $toReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function display(){
|
||||||
|
// $this
|
||||||
|
// ->setContentType('application/atom+xml; charset=' . $this->getCharset())
|
||||||
|
// ->callContentType();
|
||||||
|
|
||||||
|
return parent::display();
|
||||||
|
}
|
||||||
|
}
|
62
formats/HtmlFormat.php
Normal file
62
formats/HtmlFormat.php
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Html
|
||||||
|
* Documentation Source http://en.wikipedia.org/wiki/Atom_%28standard%29 and http://tools.ietf.org/html/rfc4287
|
||||||
|
*
|
||||||
|
* @name Html
|
||||||
|
*/
|
||||||
|
class HtmlFormat extends FormatAbstract{
|
||||||
|
|
||||||
|
public function stringify(){
|
||||||
|
/* Datas preparation */
|
||||||
|
$extraInfos = $this->getExtraInfos();
|
||||||
|
$title = htmlspecialchars($extraInfos['name']);
|
||||||
|
$uri = htmlspecialchars($extraInfos['uri']);
|
||||||
|
|
||||||
|
$entries = '';
|
||||||
|
foreach($this->getDatas() as $data){
|
||||||
|
$entryUri = is_null($data->uri) ? $uri : $data->uri;
|
||||||
|
$entryTitle = is_null($data->title) ? '' : htmlspecialchars(strip_tags($data->title));
|
||||||
|
$entryTimestamp = is_null($data->timestamp) ? '' : '<small>' . date(DATE_ATOM, $data->timestamp) . '</small>';
|
||||||
|
$entryContent = is_null($data->content) ? '' : '<p>' . $data->content . '</p>';
|
||||||
|
|
||||||
|
$entries .= <<<EOD
|
||||||
|
|
||||||
|
<div class="rssitem">
|
||||||
|
<h2><a href="{$entryUri}">{$entryTitle}</a></h2>
|
||||||
|
{$entryTimestamp}
|
||||||
|
{$entryContent}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
EOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
$styleCss = <<<'EOD'
|
||||||
|
body{font-family:"Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;font-size:10pt;background-color:#aaa;}div.rssitem{border:1px solid black;padding:5px;margin:10px;background-color:#fff;}
|
||||||
|
EOD;
|
||||||
|
|
||||||
|
/* Data are prepared, now let's begin the "MAGIE !!!" */
|
||||||
|
$toReturn = <<<EOD
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{$title}</title>
|
||||||
|
<style type="text/css">{$styleCss}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{$title}</h1>
|
||||||
|
{$entries}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
EOD;
|
||||||
|
|
||||||
|
return $toReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function display(){
|
||||||
|
$this
|
||||||
|
->setContentType('text/html; charset=' . $this->getCharset())
|
||||||
|
->callContentType();
|
||||||
|
|
||||||
|
return parent::display();
|
||||||
|
}
|
||||||
|
}
|
24
formats/JsonFormat.php
Normal file
24
formats/JsonFormat.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Json
|
||||||
|
* Builds a JSON string from $this->items and return it to browser.
|
||||||
|
*
|
||||||
|
* @name Json
|
||||||
|
*/
|
||||||
|
class JsonFormat extends FormatAbstract{
|
||||||
|
|
||||||
|
public function stringify(){
|
||||||
|
// FIXME : sometime content can be null, transform to empty string
|
||||||
|
$datas = $this->getDatas();
|
||||||
|
|
||||||
|
return json_encode($datas);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function display(){
|
||||||
|
$this
|
||||||
|
->setContentType('application/json')
|
||||||
|
->callContentType();
|
||||||
|
|
||||||
|
return parent::display();
|
||||||
|
}
|
||||||
|
}
|
22
formats/PlaintextFormat.php
Normal file
22
formats/PlaintextFormat.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plaintext
|
||||||
|
* Returns $this->items as raw php data.
|
||||||
|
*
|
||||||
|
* @name Plaintext
|
||||||
|
*/
|
||||||
|
class PlaintextFormat extends FormatAbstract{
|
||||||
|
|
||||||
|
public function stringify(){
|
||||||
|
$datas = $this->getDatas();
|
||||||
|
return print_r($datas, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function display(){
|
||||||
|
$this
|
||||||
|
->setContentType('text/plain;charset=' . $this->getCharset())
|
||||||
|
->callContentType();
|
||||||
|
|
||||||
|
return parent::display();
|
||||||
|
}
|
||||||
|
}
|
172
index.php
Normal file
172
index.php
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
TODO :
|
||||||
|
- manage SSL detection because if library isn't load, some bridge crash !
|
||||||
|
- factorize the annotation system
|
||||||
|
- factorize to adapter : Format, Bridge, Cache (actually code is almost the same)
|
||||||
|
- implement annotation cache for entrance page
|
||||||
|
- Cache : I think logic must be change as least to avoid to reconvert object from json in FileCache case.
|
||||||
|
- add namespace to avoid futur problem ?
|
||||||
|
- see FIXME mentions in the code
|
||||||
|
- implement header('X-Cached-Version: '.date(DATE_ATOM, filemtime($cachefile)));
|
||||||
|
*/
|
||||||
|
|
||||||
|
date_default_timezone_set('UTC');
|
||||||
|
error_reporting(0);
|
||||||
|
ini_set('display_errors','1'); error_reporting(E_ALL); // For debugging only.
|
||||||
|
|
||||||
|
try{
|
||||||
|
require_once __DIR__ . '/lib/RssBridge.php';
|
||||||
|
|
||||||
|
Bridge::setDir(__DIR__ . '/bridges/');
|
||||||
|
Format::setDir(__DIR__ . '/formats/');
|
||||||
|
Cache::setDir(__DIR__ . '/caches/');
|
||||||
|
|
||||||
|
if( isset($_REQUEST) && isset($_REQUEST['action']) ){
|
||||||
|
switch($_REQUEST['action']){
|
||||||
|
case 'display':
|
||||||
|
if( isset($_REQUEST['bridge']) ){
|
||||||
|
unset($_REQUEST['action']);
|
||||||
|
$bridge = $_REQUEST['bridge'];
|
||||||
|
unset($_REQUEST['bridge']);
|
||||||
|
$format = $_REQUEST['format'];
|
||||||
|
unset($_REQUEST['format']);
|
||||||
|
|
||||||
|
// FIXME : necessary ?
|
||||||
|
// ini_set('user_agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:20.0) Gecko/20100101 Firefox/20.0');
|
||||||
|
|
||||||
|
$cache = Cache::create('FileCache');
|
||||||
|
|
||||||
|
// Data retrieval
|
||||||
|
$bridge = Bridge::create($bridge);
|
||||||
|
$bridge
|
||||||
|
->setCache($cache) // Comment this lign for avoid cache use
|
||||||
|
->setDatas($_REQUEST);
|
||||||
|
|
||||||
|
// Data transformation
|
||||||
|
$format = Format::create($format);
|
||||||
|
$format
|
||||||
|
->setDatas($bridge->getDatas())
|
||||||
|
->setExtraInfos(array(
|
||||||
|
'name' => $bridge->getName(),
|
||||||
|
'uri' => $bridge->getURI(),
|
||||||
|
))
|
||||||
|
->display();
|
||||||
|
die;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(HttpException $e){
|
||||||
|
header('HTTP/1.1 ' . $e->getCode() . ' ' . Http::getMessageForCode($e->getCode()));
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
die($e->getMessage());
|
||||||
|
}
|
||||||
|
catch(\Exception $e){
|
||||||
|
die($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHelperButtonFormat($value, $name){
|
||||||
|
return '<button type="submit" name="format" value="' . $value . '">' . $name . '</button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$bridges = Bridge::searchInformation();
|
||||||
|
$formats = Format::searchInformation();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Rss-bridge - Create your own network !</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="Rss-bridge" />
|
||||||
|
<style type="text/css">
|
||||||
|
*{margin:0;padding:0}
|
||||||
|
fieldset,img{border:0}
|
||||||
|
ul,ol{list-style-type:none}
|
||||||
|
|
||||||
|
body{background:#fff;color:#000;}
|
||||||
|
|
||||||
|
h1{font-size:2rem;margin-bottom:1rem;text-shadow:0 3px 3px #aaa;}
|
||||||
|
button{cursor:pointer;border:1px solid #959595;border-radius:4px;
|
||||||
|
background-image: linear-gradient(top, rgb(255,255,255) 0%, rgb(237,237,237) 100%);
|
||||||
|
background-image: -o-linear-gradient(top, rgb(255,255,255) 0%, rgb(237,237,237) 100%);
|
||||||
|
background-image: -moz-linear-gradient(top, rgb(255,255,255) 0%, rgb(237,237,237) 100%);
|
||||||
|
background-image: -webkit-linear-gradient(top, rgb(255,255,255) 0%, rgb(237,237,237) 100%);
|
||||||
|
background-image: -ms-linear-gradient(top, rgb(255,255,255) 0%, rgb(237,237,237) 100%);
|
||||||
|
}
|
||||||
|
button:hover{
|
||||||
|
background-image: linear-gradient(top, rgb(237,237,237) 0%, rgb(255,255,255) 100%);
|
||||||
|
background-image: -o-linear-gradient(top, rgb(237,237,237) 0%, rgb(255,255,255) 100%);
|
||||||
|
background-image: -moz-linear-gradient(top, rgb(237,237,237) 0%, rgb(255,255,255) 100%);
|
||||||
|
background-image: -webkit-linear-gradient(top, rgb(237,237,237) 0%, rgb(255,255,255) 100%);
|
||||||
|
background-image: -ms-linear-gradient(top, rgb(237,237,237) 0%, rgb(255,255,255) 100%);
|
||||||
|
}
|
||||||
|
input[type="text"]{width:14rem;padding:.1rem;}
|
||||||
|
|
||||||
|
.main{width:98%;margin:0 auto;font-size:1rem;}
|
||||||
|
.list-bridge > li:first-child{margin-top:0;}
|
||||||
|
.list-bridge > li{background:#f5f5f5;padding:.5rem 1rem;margin-top:2rem;border-radius:4px;
|
||||||
|
-webkit-box-shadow: 0px 0px 6px 2px #cfcfcf;
|
||||||
|
box-shadow: 0px 0px 6px 2px #cfcfcf;
|
||||||
|
}
|
||||||
|
.list-bridge > li .name{font-size:1.4rem;}
|
||||||
|
.list-bridge > li .description{font-size:.9rem;color:#717171;margin-bottom:.5rem;}
|
||||||
|
.list-bridge > li label{display:none;}
|
||||||
|
.list-bridge > li .list-use > li:first-child{margin-top:0;}
|
||||||
|
.list-bridge > li .list-use > li{margin-top:.5rem;}
|
||||||
|
|
||||||
|
#origin{text-align:center;margin-top:2rem;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main">
|
||||||
|
<h1>RSS-Bridge</h1>
|
||||||
|
<ul class="list-bridge">
|
||||||
|
<?php foreach($bridges as $bridgeReference => $bridgeInformations): ?>
|
||||||
|
<li id="bridge-<?php echo $bridgeReference ?>" data-ref="<?php echo $bridgeReference ?>">
|
||||||
|
<div class="name"><?php echo $bridgeInformations['name'] ?></div>
|
||||||
|
<div class="informations">
|
||||||
|
<p class="description">
|
||||||
|
<?php echo isset($bridgeInformations['description']) ? $bridgeInformations['description'] : 'No description provide' ?>
|
||||||
|
</p>
|
||||||
|
<?php if( isset($bridgeInformations['use']) && count($bridgeInformations['use']) > 0 ): ?>
|
||||||
|
<ol class="list-use">
|
||||||
|
<?php foreach($bridgeInformations['use'] as $anUseNum => $anUse): ?>
|
||||||
|
<li data-use="<?php echo $anUseNum ?>">
|
||||||
|
<form method="GET" action="?">
|
||||||
|
<input type="hidden" name="action" value="display" />
|
||||||
|
<input type="hidden" name="bridge" value="<?php echo $bridgeReference ?>" />
|
||||||
|
<?php foreach($anUse as $argName => $argDescription): ?>
|
||||||
|
<?php
|
||||||
|
$idArg = 'arg-' . $bridgeReference . '-' . $anUseNum . '-' . $argName;
|
||||||
|
?>
|
||||||
|
<label for="<?php echo $idArg ?>"><?php echo $argDescription ?></label><input id="<?php echo $idArg ?>" type="text" value="" name="<?php echo $argName ?>" placeholder="<?php echo $argDescription ?>" />
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php foreach( $formats as $name => $infos ): ?>
|
||||||
|
<?php if( isset($infos['name']) ){ echo getHelperButtonFormat($name, $infos['name']); } ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ol>
|
||||||
|
<?php else: ?>
|
||||||
|
<form method="GET" action="?">
|
||||||
|
<input type="hidden" name="action" value="display" />
|
||||||
|
<input type="hidden" name="bridge" value="<?php echo $bridgeReference ?>" />
|
||||||
|
<?php foreach( $formats as $name => $infos ): ?>
|
||||||
|
<?php if( isset($infos['name']) ){ echo getHelperButtonFormat($name, $infos['name']); } ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<p id="origin">
|
||||||
|
<a href="">RSS-Bridge</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
187
lib/Bridge.php
Normal file
187
lib/Bridge.php
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* All bridge logic
|
||||||
|
* Note : adapter are store in other place
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface BridgeInterface{
|
||||||
|
public function collectData(array $param);
|
||||||
|
public function getName();
|
||||||
|
public function getURI();
|
||||||
|
public function getCacheDuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BridgeAbstract implements BridgeInterface{
|
||||||
|
protected $cache;
|
||||||
|
protected $items = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch probative exception
|
||||||
|
*/
|
||||||
|
protected function returnError($message, $code){
|
||||||
|
throw new \HttpException($message, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return datas store in the bridge
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getDatas(){
|
||||||
|
return $this->items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defined datas with parameters depending choose bridge
|
||||||
|
* Note : you can defined a cache before with "setCache"
|
||||||
|
* @param array $param $_REQUEST, $_GET, $_POST, or array with bridge expected paramters
|
||||||
|
*/
|
||||||
|
public function setDatas(array $param){
|
||||||
|
if( !is_null($this->cache) ){
|
||||||
|
$this->cache->prepare($param);
|
||||||
|
$time = $this->cache->getTime();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$time = false; // No cache ? No time !
|
||||||
|
}
|
||||||
|
|
||||||
|
if( $time !== false && ( time() - $this->getCacheDuration() < $time ) ){ // Cache file has not expired. Serve it.
|
||||||
|
$this->items = $this->cache->loadData();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$this->collectData($param);
|
||||||
|
|
||||||
|
if( !is_null($this->cache) ){ // Cache defined ? We go to refresh is memory :D
|
||||||
|
$this->cache->saveData($this->getDatas());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define default duraction for cache
|
||||||
|
*/
|
||||||
|
public function getCacheDuration(){
|
||||||
|
return 3600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defined cache object to use
|
||||||
|
*/
|
||||||
|
public function setCache(\CacheAbstract $cache){
|
||||||
|
$this->cache = $cache;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bridge{
|
||||||
|
|
||||||
|
static protected $dirBridge;
|
||||||
|
|
||||||
|
public function __construct(){
|
||||||
|
throw new \LogicException('Please use ' . __CLASS__ . '::create for new object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new bridge object
|
||||||
|
* @param string $nameBridge Defined bridge name you want use
|
||||||
|
* @return Bridge object dedicated
|
||||||
|
*/
|
||||||
|
static public function create($nameBridge){
|
||||||
|
if( !static::isValidNameBridge($nameBridge) ){
|
||||||
|
throw new \InvalidArgumentException('Name bridge must be at least one uppercase follow or not by alphanumeric or dash characters.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pathBridge = self::getDir() . $nameBridge . '.php';
|
||||||
|
|
||||||
|
if( !file_exists($pathBridge) ){
|
||||||
|
throw new \Exception('The bridge you looking for does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once $pathBridge;
|
||||||
|
|
||||||
|
return new $nameBridge();
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function setDir($dirBridge){
|
||||||
|
if( !is_string($dirBridge) ){
|
||||||
|
throw new \InvalidArgumentException('Dir bridge must be a string.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !file_exists($dirBridge) ){
|
||||||
|
throw new \Exception('Dir bridge does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$dirBridge = $dirBridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function getDir(){
|
||||||
|
$dirBridge = self::$dirBridge;
|
||||||
|
|
||||||
|
if( is_null($dirBridge) ){
|
||||||
|
throw new \LogicException(__CLASS__ . ' class need to know bridge path !');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dirBridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function isValidNameBridge($nameBridge){
|
||||||
|
return preg_match('@^[A-Z][a-zA-Z0-9-]*$@', $nameBridge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read bridge dir and catch informations about each bridge depending annotation
|
||||||
|
* @return array Informations about each bridge
|
||||||
|
*/
|
||||||
|
static public function searchInformation(){
|
||||||
|
$pathDirBridge = self::getDir();
|
||||||
|
|
||||||
|
$listBridge = array();
|
||||||
|
|
||||||
|
$searchCommonPattern = array('description', 'name');
|
||||||
|
|
||||||
|
$dirFiles = scandir($pathDirBridge);
|
||||||
|
if( $dirFiles !== false ){
|
||||||
|
foreach( $dirFiles as $fileName ){
|
||||||
|
if( preg_match('@([^.]+)\.php@U', $fileName, $out) ){ // Is PHP file ?
|
||||||
|
$infos = array(); // Information about the bridge
|
||||||
|
$resParse = token_get_all(file_get_contents($pathDirBridge . $fileName)); // Parse PHP file
|
||||||
|
foreach($resParse as $v){
|
||||||
|
if( is_array($v) && $v[0] == T_DOC_COMMENT ){ // Lexer node is COMMENT ?
|
||||||
|
$commentary = $v[1];
|
||||||
|
foreach( $searchCommonPattern as $name){ // Catch information with common pattern
|
||||||
|
preg_match('#@' . preg_quote($name, '#') . '\s+(.+)#', $commentary, $outComment);
|
||||||
|
if( isset($outComment[1]) ){
|
||||||
|
$infos[$name] = $outComment[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preg_match_all('#@use(?<num>[1-9][0-9]*)\s?\((?<args>.+)\)(?:\r|\n)#', $commentary, $outComment); // Catch specific information about "use".
|
||||||
|
if( isset($outComment['args']) && is_array($outComment['args']) ){
|
||||||
|
$infos['use'] = array();
|
||||||
|
foreach($outComment['args'] as $num => $args){ // Each use
|
||||||
|
preg_match_all('#(?<name>[a-z]+)="(?<value>.*)"(?:,|$)#U', $args, $outArg); // Catch arguments for current use
|
||||||
|
if( isset($outArg['name']) ){
|
||||||
|
$usePos = $outComment['num'][$num]; // Current use name
|
||||||
|
if( !isset($infos['use'][$usePos]) ){ // Not information actually for this "use" ?
|
||||||
|
$infos['use'][$usePos] = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($outArg['name'] as $numArg => $name){ // Each arguments
|
||||||
|
$infos['use'][$usePos][$name] = $outArg['value'][$numArg];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( isset($infos['name']) ){ // If informations containt at least a name
|
||||||
|
$listBridge[$out[1]] = $infos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $listBridge;
|
||||||
|
}
|
||||||
|
}
|
72
lib/Cache.php
Normal file
72
lib/Cache.php
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* All cache logic
|
||||||
|
* Note : adapter are store in other place
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface CacheInterface{
|
||||||
|
public function loadData();
|
||||||
|
public function saveData($datas);
|
||||||
|
public function getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class CacheAbstract implements CacheInterface{
|
||||||
|
protected $param;
|
||||||
|
|
||||||
|
public function prepare(array $param){
|
||||||
|
$this->param = $param;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cache{
|
||||||
|
|
||||||
|
static protected $dirCache;
|
||||||
|
|
||||||
|
public function __construct(){
|
||||||
|
throw new \LogicException('Please use ' . __CLASS__ . '::create for new object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function create($nameCache){
|
||||||
|
if( !static::isValidNameCache($nameCache) ){
|
||||||
|
throw new \InvalidArgumentException('Name cache must be at least one uppercase follow or not by alphanumeric or dash characters.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pathCache = self::getDir() . $nameCache . '.php';
|
||||||
|
|
||||||
|
if( !file_exists($pathCache) ){
|
||||||
|
throw new \Exception('The cache you looking for does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once $pathCache;
|
||||||
|
|
||||||
|
return new $nameCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function setDir($dirCache){
|
||||||
|
if( !is_string($dirCache) ){
|
||||||
|
throw new \InvalidArgumentException('Dir cache must be a string.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !file_exists($dirCache) ){
|
||||||
|
throw new \Exception('Dir cache does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$dirCache = $dirCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function getDir(){
|
||||||
|
$dirCache = self::$dirCache;
|
||||||
|
|
||||||
|
if( is_null($dirCache) ){
|
||||||
|
throw new \LogicException(__CLASS__ . ' class need to know cache path !');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dirCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function isValidNameCache($nameCache){
|
||||||
|
return preg_match('@^[A-Z][a-zA-Z0-9-]*$@', $nameCache);
|
||||||
|
}
|
||||||
|
}
|
61
lib/Exceptions.php
Normal file
61
lib/Exceptions.php
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
class HttpException extends \Exception{}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not real http implementation but only utils stuff
|
||||||
|
*/
|
||||||
|
class Http{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return message corresponding to Http code
|
||||||
|
*/
|
||||||
|
static public function getMessageForCode($code){
|
||||||
|
$codes = self::getCodes();
|
||||||
|
|
||||||
|
if( isset($codes[$code]) ){
|
||||||
|
return $codes[$code];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of common Http code
|
||||||
|
*/
|
||||||
|
static public function getCodes(){
|
||||||
|
return array(
|
||||||
|
200 => 'OK',
|
||||||
|
201 => 'Created',
|
||||||
|
202 => 'Accepted',
|
||||||
|
300 => 'Multiple Choices',
|
||||||
|
301 => 'Moved Permanently',
|
||||||
|
302 => 'Moved Temporarily',
|
||||||
|
307 => 'Temporary Redirect',
|
||||||
|
310 => 'Too many Redirects',
|
||||||
|
400 => 'Bad Request',
|
||||||
|
401 => 'Unauthorized',
|
||||||
|
402 => 'Payment Required',
|
||||||
|
403 => 'Forbidden',
|
||||||
|
404 => 'Not Found',
|
||||||
|
405 => 'Method Not',
|
||||||
|
406 => 'Not Acceptable',
|
||||||
|
407 => 'Proxy Authentication Required',
|
||||||
|
408 => 'Request Time-out',
|
||||||
|
409 => 'Conflict',
|
||||||
|
410 => 'Gone',
|
||||||
|
411 => 'Length Required',
|
||||||
|
412 => 'Precondition Failed',
|
||||||
|
413 => 'Request Entity Too Large',
|
||||||
|
414 => 'Request-URI Too Long',
|
||||||
|
415 => 'Unsupported Media Type',
|
||||||
|
416 => 'Requested range unsatisfiable',
|
||||||
|
417 => 'Expectation failed',
|
||||||
|
500 => 'Internal Server Error',
|
||||||
|
501 => 'Not Implemented',
|
||||||
|
502 => 'Bad Gateway',
|
||||||
|
503 => 'Service Unavailable',
|
||||||
|
504 => 'Gateway Time-out',
|
||||||
|
508 => 'Loop detected',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
183
lib/Format.php
Normal file
183
lib/Format.php
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* All format logic
|
||||||
|
* Note : adapter are store in other place
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface FormatInterface{
|
||||||
|
public function stringify();
|
||||||
|
public function display();
|
||||||
|
public function setDatas(array $bridge);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class FormatAbstract implements FormatInterface{
|
||||||
|
const DEFAULT_CHARSET = 'UTF-8';
|
||||||
|
|
||||||
|
protected
|
||||||
|
$contentType,
|
||||||
|
$charset,
|
||||||
|
$datas,
|
||||||
|
$extraInfos
|
||||||
|
;
|
||||||
|
|
||||||
|
public function setCharset($charset){
|
||||||
|
$this->charset = $charset;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCharset(){
|
||||||
|
$charset = $this->charset;
|
||||||
|
|
||||||
|
return is_null($charset) ? self::DEFAULT_CHARSET : $charset;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setContentType($contentType){
|
||||||
|
$this->contentType = $contentType;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function callContentType(){
|
||||||
|
header('Content-Type: ' . $this->contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function display(){
|
||||||
|
echo $this->stringify();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDatas(array $datas){
|
||||||
|
$this->datas = $datas;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDatas(){
|
||||||
|
if( !is_array($this->datas) ){
|
||||||
|
throw new \LogicException('Feed the ' . get_class($this) . ' with "setDatas" method before !');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->datas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define common informations can be required by formats and set default value for unknow values
|
||||||
|
* @param array $extraInfos array with know informations (there isn't merge !!!)
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public function setExtraInfos(array $extraInfos = array()){
|
||||||
|
foreach(array('name', 'uri') as $infoName){
|
||||||
|
if( !isset($extraInfos[$infoName]) ){
|
||||||
|
$extraInfos[$infoName] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->extraInfos = $extraInfos;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return extra infos
|
||||||
|
* @return array See "setExtraInfos" detail method to know what extra are disponibles
|
||||||
|
*/
|
||||||
|
public function getExtraInfos(){
|
||||||
|
if( is_null($this->extraInfos) ){ // No extra info ?
|
||||||
|
$this->setExtraInfos(); // Define with default value
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->extraInfos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Format{
|
||||||
|
|
||||||
|
static protected $dirFormat;
|
||||||
|
|
||||||
|
public function __construct(){
|
||||||
|
throw new \LogicException('Please use ' . __CLASS__ . '::create for new object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function create($nameFormat){
|
||||||
|
if( !static::isValidNameFormat($nameFormat) ){
|
||||||
|
throw new \InvalidArgumentException('Name format must be at least one uppercase follow or not by alphabetic characters.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pathFormat = self::getDir() . $nameFormat . '.php';
|
||||||
|
|
||||||
|
if( !file_exists($pathFormat) ){
|
||||||
|
throw new \Exception('The format you looking for does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once $pathFormat;
|
||||||
|
|
||||||
|
return new $nameFormat();
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function setDir($dirFormat){
|
||||||
|
if( !is_string($dirFormat) ){
|
||||||
|
throw new \InvalidArgumentException('Dir format must be a string.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !file_exists($dirFormat) ){
|
||||||
|
throw new \Exception('Dir format does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$dirFormat = $dirFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function getDir(){
|
||||||
|
$dirFormat = self::$dirFormat;
|
||||||
|
|
||||||
|
if( is_null($dirFormat) ){
|
||||||
|
throw new \LogicException(__CLASS__ . ' class need to know format path !');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dirFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function isValidNameFormat($nameFormat){
|
||||||
|
return preg_match('@^[A-Z][a-zA-Z]*$@', $nameFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read format dir and catch informations about each format depending annotation
|
||||||
|
* @return array Informations about each format
|
||||||
|
*/
|
||||||
|
static public function searchInformation(){
|
||||||
|
$pathDirFormat = self::getDir();
|
||||||
|
|
||||||
|
$listFormat = array();
|
||||||
|
|
||||||
|
$searchCommonPattern = array('name');
|
||||||
|
|
||||||
|
$dirFiles = scandir($pathDirFormat);
|
||||||
|
if( $dirFiles !== false ){
|
||||||
|
foreach( $dirFiles as $fileName ){
|
||||||
|
if( preg_match('@([^.]+)\.php@U', $fileName, $out) ){ // Is PHP file ?
|
||||||
|
$infos = array(); // Information about the bridge
|
||||||
|
$resParse = token_get_all(file_get_contents($pathDirFormat . $fileName)); // Parse PHP file
|
||||||
|
foreach($resParse as $v){
|
||||||
|
if( is_array($v) && $v[0] == T_DOC_COMMENT ){ // Lexer node is COMMENT ?
|
||||||
|
$commentary = $v[1];
|
||||||
|
foreach( $searchCommonPattern as $name){ // Catch information with common pattern
|
||||||
|
preg_match('#@' . preg_quote($name, '#') . '\s+(.+)#', $commentary, $outComment);
|
||||||
|
if( isset($outComment[1]) ){
|
||||||
|
$infos[$name] = $outComment[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( isset($infos['name']) ){ // If informations containt at least a name
|
||||||
|
$listFormat[$out[1]] = $infos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $listFormat;
|
||||||
|
}
|
||||||
|
}
|
16
lib/Item.php
Normal file
16
lib/Item.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
interface ItemInterface{}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object to store datas collect informations
|
||||||
|
* FIXME : not sur this logic is the good, I think recast all is necessary
|
||||||
|
*/
|
||||||
|
class Item implements ItemInterface{
|
||||||
|
public function __set($name, $value){
|
||||||
|
$this->$name = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __get($name){
|
||||||
|
return isset($this->$name) ? $this->$name : null;
|
||||||
|
}
|
||||||
|
}
|
42
lib/RssBridge.php
Normal file
42
lib/RssBridge.php
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
/* rss-bridge library.
|
||||||
|
Foundation functions for rss-bridge project.
|
||||||
|
See https://github.com/sebsauvage/rss-bridge
|
||||||
|
Licence: Public domain.
|
||||||
|
*/
|
||||||
|
|
||||||
|
define('PATH_VENDOR', '/../vendor');
|
||||||
|
|
||||||
|
require __DIR__ . '/Exceptions.php';
|
||||||
|
require __DIR__ . '/Item.php';
|
||||||
|
require __DIR__ . '/Format.php';
|
||||||
|
require __DIR__ . '/Bridge.php';
|
||||||
|
require __DIR__ . '/Cache.php';
|
||||||
|
|
||||||
|
$vendorLibSimpleHtmlDom = __DIR__ . PATH_VENDOR . '/simplehtmldom/simple_html_dom.php';
|
||||||
|
if( !file_exists($vendorLibSimpleHtmlDom) ){
|
||||||
|
throw new \HttpException('"PHP Simple HTML DOM Parser" is missing. Get it from http://simplehtmldom.sourceforge.net and place the script "simple_html_dom.php" in the same folder to allow me to work.', 500);
|
||||||
|
}
|
||||||
|
require_once $vendorLibSimpleHtmlDom;
|
||||||
|
|
||||||
|
/* Example use
|
||||||
|
|
||||||
|
require_once __DIR__ . '/lib/RssBridge.php';
|
||||||
|
|
||||||
|
// Data retrieval
|
||||||
|
Bridge::setDir(__DIR__ . '/bridges/');
|
||||||
|
$bridge = Bridge::create('GoogleSearch');
|
||||||
|
$bridge->collectData($_REQUEST);
|
||||||
|
|
||||||
|
// Data transformation
|
||||||
|
Format::setDir(__DIR__ . '/formats/');
|
||||||
|
$format = Format::create('Atom');
|
||||||
|
$format
|
||||||
|
->setDatas($bridge->getDatas())
|
||||||
|
->setExtraInfos(array(
|
||||||
|
'name' => $bridge->getName(),
|
||||||
|
'uri' => $bridge->getURI(),
|
||||||
|
))
|
||||||
|
->display();
|
||||||
|
|
||||||
|
*/
|
|
@ -1,29 +0,0 @@
|
||||||
<?php
|
|
||||||
require_once('rss-bridge-lib.php');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RssBridgeFlickrExplore
|
|
||||||
* Returns the newest interesting images from http://www.flickr.com/explore
|
|
||||||
*/
|
|
||||||
class RssBridgeFlickrExplore extends RssBridgeAbstractClass
|
|
||||||
{
|
|
||||||
protected $bridgeName = 'Flickr Explore';
|
|
||||||
protected $bridgeURI = 'http://www.flickr.com/explore';
|
|
||||||
protected $bridgeDescription = 'Returns the latest interesting images from Flickr';
|
|
||||||
protected $cacheDuration = 360; // 6 hours. No need to get more.
|
|
||||||
protected function collectData($request) {
|
|
||||||
$html = file_get_html('http://www.flickr.com/explore') or $this->returnError('404 Not Found', 'ERROR: could not request Flickr.');
|
|
||||||
$this->items = Array();
|
|
||||||
foreach($html->find('span.photo_container') as $element) {
|
|
||||||
$item['uri'] = 'http://flickr.com'.$element->find('a',0)->href;
|
|
||||||
$item['thumbnailUri'] = $element->find('img',0)->getAttribute('data-defer-src');
|
|
||||||
$item['content'] = '<a href="'.$item['uri'].'"><img src="'.$item['thumbnailUri'].'" /></a>'; // FIXME: Filter javascript ?
|
|
||||||
$item['title'] = $element->find('a',0)->title;
|
|
||||||
$this->items[] = $item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$bridge = new RssBridgeFlickrExplore();
|
|
||||||
$bridge->process();
|
|
||||||
?>
|
|
|
@ -1,41 +0,0 @@
|
||||||
<?php
|
|
||||||
require_once('rss-bridge-lib.php');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RssBridgeGoogleMostRecent
|
|
||||||
* Search Google for most recent pages regarding a specific topic.
|
|
||||||
* Returns the 100 most recent links in results in past year,
|
|
||||||
* sorting by date (most recent first).
|
|
||||||
* Example:
|
|
||||||
* http://www.google.com/search?q=sebsauvage&num=100&complete=0&tbs=qdr:y,sbd:1
|
|
||||||
* complete=0&num=100 : get 100 results
|
|
||||||
* qdr:y : in past year
|
|
||||||
* sbd:1 : sort by date (will only work if qdr: is specified)
|
|
||||||
*/
|
|
||||||
|
|
||||||
class RssBridgeGoogleSearch extends RssBridgeAbstractClass
|
|
||||||
{
|
|
||||||
protected $bridgeName = 'Google search';
|
|
||||||
protected $bridgeURI = 'http://google.com';
|
|
||||||
protected $bridgeDescription = 'Returns most recent results from Google search.';
|
|
||||||
protected $cacheDuration = 30; // 30 minutes, otherwise you could get banned by Google, or stumblr upon their captcha.
|
|
||||||
protected function collectData($request) {
|
|
||||||
$html = '';
|
|
||||||
if (isset($request['q'])) { /* keyword search mode */
|
|
||||||
$html = file_get_html('http://www.google.com/search?q='.urlencode($request['q']).'&num=100&complete=0&tbs=qdr:y,sbd:1') or $this->returnError('404 Not Found', 'ERROR: no results for this query.');
|
|
||||||
} else {
|
|
||||||
$this->returnError('400 Bad Request', 'ERROR: You must specify a keyword (?q=...).');
|
|
||||||
}
|
|
||||||
$this->items = Array();
|
|
||||||
foreach($html->find('div[id=ires]',0)->find('li[class=g]') as $element) {
|
|
||||||
$item['uri'] = $element->find('a[href]',0)->href;
|
|
||||||
$item['title'] = $element->find('h3',0)->plaintext;
|
|
||||||
$item['content'] = $element->find('span[class=st]',0)->plaintext;
|
|
||||||
$this->items[] = $item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$bridge = new RssBridgeGoogleSearch();
|
|
||||||
$bridge->process();
|
|
||||||
?>
|
|
|
@ -1,214 +0,0 @@
|
||||||
<?php
|
|
||||||
/* rss-bridge library.
|
|
||||||
Foundation functions for rss-bridge project.
|
|
||||||
See https://github.com/sebsauvage/rss-bridge
|
|
||||||
Licence: Public domain.
|
|
||||||
*/
|
|
||||||
ini_set('user_agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:20.0) Gecko/20100101 Firefox/20.0');
|
|
||||||
date_default_timezone_set('UTC');
|
|
||||||
error_reporting(0);
|
|
||||||
//ini_set('display_errors','1'); error_reporting(E_ALL); // For debugging only.
|
|
||||||
define('CACHEDIR','cache/'); // Directory containing cache files. Do not forget trailing slash.
|
|
||||||
ob_start();
|
|
||||||
|
|
||||||
// Create cache directory if it does not exist.
|
|
||||||
if (!is_dir(CACHEDIR)) { mkdir(CACHEDIR,0705); chmod(CACHEDIR,0705); }
|
|
||||||
|
|
||||||
// Import DOM library.
|
|
||||||
if (!file_exists('simple_html_dom.php'))
|
|
||||||
{
|
|
||||||
header('HTTP/1.1 500 Internal Server Error');
|
|
||||||
header('Content-Type: text/plain');
|
|
||||||
die('"PHP Simple HTML DOM Parser" is missing. Get it from http://simplehtmldom.sourceforge.net/ and place the script "simple_html_dom.php" in the same folder to allow me to work.');
|
|
||||||
}
|
|
||||||
require_once('simple_html_dom.php');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract RSSBridge class on which all bridges are build upon.
|
|
||||||
* It provides utility methods (cache, ATOM feed building...)
|
|
||||||
*/
|
|
||||||
abstract class RssBridgeAbstractClass
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* $items is an array of dictionnaries. Each subclass must fill this array when collectData() is called.
|
|
||||||
* eg. $items = Array( Array('uri'=>'http://foo.bar', 'title'=>'My beautiful foobar', 'content'='Hello, <b>world !</b>','timestamp'=>'1375864834'),
|
|
||||||
* Array('uri'=>'http://toto.com', 'title'=>'Welcome to toto', 'content'='What is this website about ?','timestamp'=>'1375868313')
|
|
||||||
* )
|
|
||||||
* Keys in dictionnaries:
|
|
||||||
* uri (string;mandatory) = The URI the item points to.
|
|
||||||
* title (string;mandatory) = Title of item
|
|
||||||
* content (string;optionnal) = item content (usually HTML code)
|
|
||||||
* timestamp (string;optionnal) = item date. Must be in EPOCH format.
|
|
||||||
* Other keys can be added, but will be ignored.
|
|
||||||
* $items will be used to build the ATOM feed, json and other outputs.
|
|
||||||
*/
|
|
||||||
var $items;
|
|
||||||
|
|
||||||
private $contentType; // MIME type returned to browser.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the content-type returns to browser.
|
|
||||||
* Example: $this->setContentType('text/html; charset=UTF-8')
|
|
||||||
*/
|
|
||||||
private function setContentType($value)
|
|
||||||
{
|
|
||||||
$this->contentType = $value;
|
|
||||||
header('Content-Type: '.$value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* collectData() will be called to ask the bridge to go collect data on the net.
|
|
||||||
* All derived classes must implement this method.
|
|
||||||
* This method must fill $this->items with collected items.
|
|
||||||
* Input: $request : The incoming request (=$_GET). This can be used or ignored by the bridge.
|
|
||||||
*/
|
|
||||||
abstract protected function collectData($request);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a HTTP error to user, with a message.
|
|
||||||
* Example: $this->returnError('404 Not Found', 'ERROR: no results.');
|
|
||||||
*/
|
|
||||||
protected function returnError($code, $message)
|
|
||||||
{
|
|
||||||
header("HTTP/1.1 $code"); header('Content-Type: text/plain;charset=UTF-8');
|
|
||||||
die($message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds an ATOM feed from $this->items and return it to browser.
|
|
||||||
*/
|
|
||||||
private function returnATOM()
|
|
||||||
{
|
|
||||||
$this->setContentType('application/atom+xml; charset=UTF-8');
|
|
||||||
echo '<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xml:lang="en-US">'."\n";
|
|
||||||
echo '<title type="text">'.htmlspecialchars($this->bridgeName).'</title>'."\n";
|
|
||||||
echo '<id>http'.(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 's' : '')."://{$_SERVER['HTTP_HOST']}{$_SERVER['PATH_INFO']}".'/</id>'."\n";
|
|
||||||
echo '<updated></updated>'."\n"; // FIXME
|
|
||||||
echo '<link rel="alternate" type="text/html" href="'.htmlspecialchars($this->bridgeURI).'" />'."\n";
|
|
||||||
echo '<link rel="self" href="http'.(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 's' : '')."://{$_SERVER['HTTP_HOST']}".htmlentities($_SERVER['REQUEST_URI']).'" />'."\n"."\n";
|
|
||||||
|
|
||||||
foreach($this->items as $item) {
|
|
||||||
echo '<entry><author><name>'.htmlspecialchars($this->bridgeName).'</name><uri>'.htmlspecialchars($this->bridgeURI).'</uri></author>'."\n";
|
|
||||||
echo '<title type="html"><![CDATA['.$item['title'].']]></title>'."\n";
|
|
||||||
echo '<link rel="alternate" type="text/html" href="'.$item['uri'].'" />'."\n";
|
|
||||||
echo '<id>'.$item['uri'].'</id>'."\n";
|
|
||||||
if (isset($item['timestamp']))
|
|
||||||
{
|
|
||||||
echo '<updated>'.date(DATE_ATOM, $item['timestamp']).'</updated>'."\n";
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
echo '<updated></updated>'."\n";
|
|
||||||
}
|
|
||||||
if (isset($item['content']))
|
|
||||||
{
|
|
||||||
echo '<content type="html"><![CDATA['.$item['content'].']]></content>'."\n";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
echo '<content type="html"></content>'."\n";
|
|
||||||
}
|
|
||||||
// FIXME: Security: Disable Javascript ?
|
|
||||||
echo '</entry>'."\n\n";
|
|
||||||
}
|
|
||||||
echo '</feed>';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function returnHTML()
|
|
||||||
{
|
|
||||||
$this->setContentType('text/html; charset=UTF-8');
|
|
||||||
echo '<html><head><title>'.htmlspecialchars($this->bridgeName).'</title>';
|
|
||||||
echo '<style>body{font-family:"Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;font-size:10pt;background-color:#aaa;}div.rssitem{border:1px solid black;padding:5px;margin:10px;background-color:#fff;}</style></head><body>';
|
|
||||||
echo '<h1>'.htmlspecialchars($this->bridgeName).'</h1>';
|
|
||||||
foreach($this->items as $item) {
|
|
||||||
echo '<div class="rssitem"><h2><a href="'.$item['uri'].'">'.htmlspecialchars(strip_tags($item['title'])).'</a></h2>';
|
|
||||||
if (isset($item['timestamp'])) { echo '<small>'.date(DATE_ATOM, $item['timestamp']).'</small>'; }
|
|
||||||
if (isset($item['content'])) { echo '<p>'.$item['content'].'</p>'; }
|
|
||||||
|
|
||||||
echo "</div>\n\n";
|
|
||||||
}
|
|
||||||
echo '</body></html>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a JSON string from $this->items and return it to browser.
|
|
||||||
*/
|
|
||||||
private function returnJSON()
|
|
||||||
{
|
|
||||||
$this->setContentType('application/json');
|
|
||||||
echo json_encode($this->items);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns $this->items as raw php data.
|
|
||||||
*/
|
|
||||||
private function returnPlaintext()
|
|
||||||
{
|
|
||||||
$this->setContentType('text/plain;charset=UTF-8');
|
|
||||||
print_r($this->items);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start processing request and return response to browser.
|
|
||||||
*/
|
|
||||||
public function process()
|
|
||||||
{
|
|
||||||
$this->serveCachedVersion();
|
|
||||||
|
|
||||||
// Cache file does not exists or has expired: We re-fetch the results and cache it.
|
|
||||||
$this->collectData($_GET);
|
|
||||||
if (empty($this->items)) { $this->returnError('404 Not Found', 'ERROR: no results.'); }
|
|
||||||
|
|
||||||
$format = 'atom';
|
|
||||||
if (!empty($_GET['format'])) { $format = $_GET['format']; }
|
|
||||||
switch($format) {
|
|
||||||
case 'plaintext':
|
|
||||||
$this->returnPlaintext();
|
|
||||||
break;
|
|
||||||
case 'json':
|
|
||||||
$this->returnJSON();
|
|
||||||
break;
|
|
||||||
case 'html':
|
|
||||||
$this->returnHTML();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$this->returnATOM();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->storeReponseInCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the cached version of current request URI directly to the browser
|
|
||||||
* if it exists and if cache has not expired.
|
|
||||||
* Continues execution no cached version available.
|
|
||||||
*/
|
|
||||||
private function serveCachedVersion()
|
|
||||||
{
|
|
||||||
// See if cache exists for this request
|
|
||||||
$cachefile = CACHEDIR.hash('sha1',$_SERVER['REQUEST_URI']).'.cache'; // Cache path and filename
|
|
||||||
if (file_exists($cachefile)) { // The cache file exists.
|
|
||||||
if (time() - ($this->cacheDuration*60) < filemtime($cachefile)) { // Cache file has not expired. Serve it.
|
|
||||||
$data = json_decode(file_get_contents($cachefile),true);
|
|
||||||
header('Content-Type: '.$data['Content-Type']); // Send proper MIME Type
|
|
||||||
header('X-Cached-Version: '.date(DATE_ATOM, filemtime($cachefile)));
|
|
||||||
echo $data['data'];
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores currently generated page in cache.
|
|
||||||
*/
|
|
||||||
private function storeReponseInCache()
|
|
||||||
{
|
|
||||||
$cachefile = CACHEDIR.hash('sha1',$_SERVER['REQUEST_URI']).'.cache'; // Cache path and filename
|
|
||||||
$data = Array('data'=>ob_get_contents(), 'Content-Type'=>$this->contentType);
|
|
||||||
file_put_contents($cachefile,json_encode($data));
|
|
||||||
ob_end_flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
|
@ -1,40 +0,0 @@
|
||||||
<?php
|
|
||||||
require_once('rss-bridge-lib.php');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RssBridgeTwitter
|
|
||||||
* Based on https://github.com/mitsukarenai/twitterbridge-noapi
|
|
||||||
*/
|
|
||||||
class RssBridgeTwitter extends RssBridgeAbstractClass
|
|
||||||
{
|
|
||||||
protected $bridgeName = 'Twitter Bridge';
|
|
||||||
protected $bridgeURI = 'http://twitter.com';
|
|
||||||
protected $bridgeDescription = 'Returns user timelines or keyword search from http://twitter.com without using their API.';
|
|
||||||
protected $cacheDuration = 5; // 5 minutes
|
|
||||||
protected function collectData($request) {
|
|
||||||
$html = '';
|
|
||||||
if (isset($request['q'])) { /* keyword search mode */
|
|
||||||
$html = file_get_html('http://twitter.com/search/realtime?q='.urlencode($request['q']).'+include:retweets&src=typd') or $this->returnError('404 Not Found', 'ERROR: no results for this query.');
|
|
||||||
} elseif (isset($request['u'])) { /* user timeline mode */
|
|
||||||
$html = file_get_html('http://twitter.com/'.urlencode($request['u'])) or $this->returnError('404 Not Found', 'ERROR: requested username can\'t be found.');
|
|
||||||
} else {
|
|
||||||
$this->returnError('400 Bad Request', 'ERROR: You must specify a keyword (?q=...) or a Twitter username (?u=...).');
|
|
||||||
}
|
|
||||||
$this->items = Array();
|
|
||||||
foreach($html->find('div.tweet') as $tweet) {
|
|
||||||
$item['username'] = trim(substr($tweet->find('span.username', 0)->plaintext, 1)); // extract username and sanitize
|
|
||||||
$item['fullname'] = $tweet->getAttribute('data-name'); // extract fullname (pseudonym)
|
|
||||||
$item['avatar'] = $tweet->find('img', 0)->src; // get avatar link
|
|
||||||
$item['id'] = $tweet->getAttribute('data-tweet-id'); // get TweetID
|
|
||||||
$item['uri'] = 'https://twitter.com'.$tweet->find('a.details', 0)->getAttribute('href'); // get tweet link
|
|
||||||
$item['timestamp'] = $tweet->find('span._timestamp', 0)->getAttribute('data-time'); // extract tweet timestamp
|
|
||||||
$item['content'] = str_replace('href="/', 'href="https://twitter.com/', strip_tags($tweet->find('p.tweet-text', 0)->innertext, '<a>')); // extract tweet text
|
|
||||||
$item['title'] = $item['fullname'] . ' (@'.$item['username'] . ') | ' . $item['content'];
|
|
||||||
$this->items[] = $item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$bridge = new RssBridgeTwitter();
|
|
||||||
$bridge->process();
|
|
||||||
?>
|
|
Loading…
Reference in a new issue