Browse Source

Refonte du code

Yves ASTIER 10 years ago
parent
commit
927b04dfef

+ 13 - 18
README.md

@@ -6,36 +6,31 @@ rss-bridge is a collection of independant php scripts capable of generating ATOM
 Supported sites/pages
 ===
 
- * `rss-bridge-flickr-explore.php` : [Latest interesting images](http://www.flickr.com/explore) from Flickr.
- * `rss-bridge-googlesearch.php` : Most recent results from Google Search. Parameters:
+ * `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr.
+ * `GoogleSearch` : Most recent results from Google Search. Parameters:
    * q=keyword : Keyword search.
- * `rss-bridge-twitter.php` : Twitter. Parameters:
+ * `Twitter.php` : Twitter. Parameters:
    * q=keyword : Keyword search.
    * u=username : Get user timeline.
 
+Easy new bridge system (detail below) !
+
 Output format
 ===
 Output format can be used in any rss-bridge:
 
- * `format=atom` (default): ATOM Feed.
- * `format=json` : jSon
- * `format=html` : html page
- * `format=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.
+ * `Atom` : ATOM Feed.
+ * `Json` : jSon
+ * `Html` : html page
+ * `Plaintext` : raw text (php object, as returned by print_r)
 
+Easy new format system (detail below) !
    
 Requirements
 ===
 
  * php 5.3
- * [PHP Simple HTML DOM Parser](http://simplehtmldom.sourceforge.net/)
+ * [PHP Simple HTML DOM Parser](http://simplehtmldom.sourceforge.net)
  
 Author
 ===
@@ -44,7 +39,7 @@ I'm sebsauvage, webmaster of [sebsauvage.net](http://sebsauvage.net), author of
 Thanks to [Mitsukarenai](https://github.com/Mitsukarenai) for the inspiration.
 
 Patch :
-- Yves ASTIER (Draeli) : PHP optimizations, minor fixes, dynamic brigde list with all stuff behind
+- Yves ASTIER (Draeli) : PHP optimizations, minor fixes, dynamic brigde/format list with all stuff behind and extend cache system. Mail : contact@yves-astier.com
 
 Licence
 ===
@@ -54,7 +49,7 @@ Code is public domain.
 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.
-  * 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
 ===

+ 35 - 0
bridges/FlickrExploreBridge.php

@@ -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 - 0
bridges/GoogleSearchBridge.php

@@ -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 - 0
bridges/TwitterBridge.php

@@ -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
+    }
+}

+ 0 - 29
bridges/flickr-explore.php

@@ -1,29 +0,0 @@
-<?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 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, '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();

+ 0 - 41
bridges/googlesearch.php

@@ -1,41 +0,0 @@
-<?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 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, 'no results for this query.');
-        } else {
-            $this->returnError(400, '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();

+ 0 - 42
bridges/twitter.php

@@ -1,42 +0,0 @@
-<?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 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, '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, 'requested username can\'t be found.');
-        } else {
-            $this->returnError(400, '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();

+ 92 - 0
caches/FileCache.php

@@ -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 - 0
formats/AtomFormat.php

@@ -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 - 0
formats/HtmlFormat.php

@@ -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 - 0
formats/JsonFormat.php

@@ -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 - 0
formats/PlaintextFormat.php

@@ -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();
+    }
+}

+ 144 - 120
index.php

@@ -1,96 +1,78 @@
 <?php
-require_once('rss-bridge-lib.php');
-
-define('PATH_BRIDGES_RELATIVE', 'bridges/');
-define('PATH_BRIDGES', __DIR__ . DIRECTORY_SEPARATOR . 'bridges' . DIRECTORY_SEPARATOR);
-
 /*
 TODO :
-- gérer la détection du SSL
-- faire la création de l'objet en dehors du bridge
+- 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)));
 */
 
-/**
-* Read bridge dir and catch informations about each bridge
-* @param string @pathDirBridge Dir to the bridge path
-* @return array Informations about each bridge
-*/
-function searchBridgeInformation($pathDirBridge){
-    $searchCommonPattern = array('description', 'name');
-    $listBridge = array();
-    if($handle = opendir($pathDirBridge)) {
-        while(false !== ($entry = readdir($handle))) {
-            if( preg_match('@([^.]+)\.php@U', $entry, $out) ){ // Is PHP file ?
-                $infos = array(); // Information about the bridge
-                $resParse = token_get_all(file_get_contents($pathDirBridge . $entry)); // 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];
-                            }
-                        }
+date_default_timezone_set('UTC');
+error_reporting(0);
+ini_set('display_errors','1'); error_reporting(E_ALL);  // For debugging only.
 
-                        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();
-                                    }
+try{
+    require_once __DIR__ . '/lib/RssBridge.php';
 
-                                    foreach($outArg['name'] as $numArg => $name){ // Each arguments
-                                        $infos['use'][$usePos][$name] = $outArg['value'][$numArg];
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
+    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');
 
-                if( isset($infos['name']) ){ // If informations containt at least a name
-                    // $listBridge
-                    $listBridge[$out[1]] = $infos;
+                    $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;
         }
-        closedir($handle);
     }
-
-    return $listBridge;
 }
-
-function createNetworkLink($bridgeName, $arguments){
-    
+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());
 }
 
-if( isset($_REQUEST) && isset($_REQUEST['action']) ){
-    switch($_REQUEST['action']){
-        case 'create':
-            if( isset($_REQUEST['bridge']) ){
-                unset($_REQUEST['action']);
-                $bridge = $_REQUEST['bridge'];
-                unset($_REQUEST['bridge']);
-                // var_dump($_REQUEST);die;
-                $pathBridge = PATH_BRIDGES_RELATIVE . $bridge . '.php';
-                if( file_exists($pathBridge) ){
-                    require $pathBridge;
-                    exit();
-                }
-            }
-            break;
-    }
+function getHelperButtonFormat($value, $name){
+    return '<button type="submit" name="format" value="' . $value . '">' . $name . '</button>';
 }
 
-$listBridge = searchBridgeInformation(PATH_BRIDGES);
-// echo '<pre>';
-// var_dump($listBridge);
-// echo '</pre>';
+$bridges = Bridge::searchInformation();
+$formats = Format::searchInformation();
 ?>
 <!DOCTYPE html>
 <html lang="en">
@@ -99,50 +81,92 @@ $listBridge = searchBridgeInformation(PATH_BRIDGES);
         <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>
-        <ul class="list-bridge">
-        <?php foreach($listBridge 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="POST" action="?">
-                                <input type="hidden" name="action" value="create" />
-                                <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 ?>" /><br />
-                                <?php endforeach; ?>
-                                <button type="submit" name="format" value="json">Json</button>
-                                <button type="submit" name="format" value="plaintext">Text</button>
-                                <button type="submit" name="format" value="html">HTML</button>
-                                <button type="submit" name="format" value="atom">ATOM</button>
-                            </form>
-                        </li>
-                        <?php endforeach; ?>
-                    </ol>
-                    <?php else: ?>
-                    <form method="POST" action="?">
-                        <input type="hidden" name="action" value="create" />
-                        <input type="hidden" name="bridge" value="<?php echo $bridgeReference ?>" />
-                        <button type="submit" name="format" value="json">Json</button>
-                        <button type="submit" name="format" value="plaintext">Text</button>
-                        <button type="submit" name="format" value="html">HTML</button>
-                        <button type="submit" name="format" value="atom">ATOM</button>
-                    </form>
-                    <?php endif; ?>
-                </div>
-            </li>
-        <?php endforeach; ?>
-        </ul>
+        <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 - 0
lib/Bridge.php

@@ -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 - 0
lib/Cache.php

@@ -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 - 0
lib/Exceptions.php

@@ -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 - 0
lib/Format.php

@@ -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 - 0
lib/Item.php

@@ -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 - 0
lib/RssBridge.php

@@ -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();
+
+*/

+ 0 - 221
rss-bridge-lib.php

@@ -1,221 +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.
-define('CHARSET', 'UTF-8');
-define('SimpleDomLib', 'vendor/simplehtmldom/simple_html_dom.php');
-
-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(SimpleDomLib)) 
-{ 
-    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(SimpleDomLib);
-
-/**
- * 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.
-     */
-    public $items;
-    
-    private $contentType;  // MIME type returned to browser.
-
-    /**
-     * Sets the content-type returns to browser.
-     * 
-     * @param string Content-type returns to browser - Example: $this->setContentType('text/html; charset=UTF-8')
-     * @return this
-     */
-    private function setContentType($value){
-        $this->contentType = $value;
-        header('Content-Type: '.$value);
-        return $this;
-    }
-    
-    /**
-     * 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.
-     * @param mixed $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, 'no results.');
-     * @param integer $code
-     * @param string $message
-     */
-    protected function returnError($code, $message){
-        $errors = array(
-            400 => 'Bad Request',
-            404 => 'Not Found',
-            501 => 'Not Implemented',
-        );
-
-        header('HTTP/1.1 ' . $code . ( isset($errors[$code]) ? ' ' . $errors[$code] : ''));
-        header('Content-Type: text/plain;charset=' . CHARSET);
-        die('ERROR : ' . $message); 
-    }
-
-    /**
-     * Builds an ATOM feed from $this->items and return it to browser.
-     */
-    private function returnATOM(){
-        $this->setContentType('application/atom+xml; charset=' . CHARSET);
-
-        $https = ( isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 's' : '' );
-        $httpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
-        $httpInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
-
-        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' . $https . '://' . $httpHost . $httpInfo . './</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'.$https.'://' . $httpHost . 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";
-            echo '<updated>' . ( isset($item['timestamp']) ? date(DATE_ATOM, $item['timestamp']) : '' ) . '</updated>'."\n";
-            echo '<content type="html">' . ( isset($item['content']) ? '<![CDATA[' . $item['content'] . ']]>' : '') . '</content>'."\n";
-
-            // FIXME: Security: Disable Javascript ?
-            echo '</entry>'."\n\n";
-        }
-
-        echo '</feed>';    
-    }
-    
-    private function returnHTML(){
-        $this->setContentType('text/html; charset=' . CHARSET);
-        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=' . CHARSET); 
-        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($_REQUEST);
-
-        if (empty($this->items)) { $this->returnError(404, 'no results.'); }
-
-        $format = isset($_REQUEST['format']) ? $_REQUEST['format'] : 'atom';
-        switch($format) {
-            case 'plaintext':
-                $this->returnPlaintext();
-                break;
-            case 'json':
-                $this->returnJSON();
-                break;               
-            case 'html':
-                $this->returnHTML();
-                break;              
-            default:
-                $this->returnATOM();
-        }
-        
-        $this->storeReponseInCache();
-    }
-
-    private function getCacheName(){
-        if( !isset($_REQUEST) ){
-            $this->returnError(501, 'WTF ?');
-        }
-
-        $stringToEncode = $_SERVER['REQUEST_URI'] . http_build_query($_REQUEST);
-        return CACHEDIR.hash('sha1',$stringToEncode).'.cache';
-    }
-
-    /**
-     * 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 = $this->getCacheName(); // 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.
-     * @return this
-     */
-    private function storeReponseInCache(){
-        $cachefile = $this->getCacheName(); // Cache path and filename
-        $data = array('data'=>ob_get_contents(), 'Content-Type'=>$this->contentType);
-        file_put_contents($cachefile,json_encode($data));
-        ob_end_flush();
-        return $this;
-    }
-}