summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohannes Schauer <josch@debian.org>2018-03-27 15:05:30 +0200
committerJohannes Schauer <josch@debian.org>2018-03-27 15:05:30 +0200
commit96ca5535d146ed4a14d1ca66495e7ce1d38cf35b (patch)
tree0b0c8dc557b7e8bc45c4cc0a204cec86d5f22ddc
parent397a196e4b0b351fb7fda6581a927d1afa02f413 (diff)
Import upstream version 2018-03-11
-rw-r--r--.travis.yml17
-rw-r--r--CHANGELOG.md148
-rw-r--r--README.md44
-rw-r--r--bridges/AllocineFRBridge.php4
-rw-r--r--bridges/Arte7Bridge.php80
-rw-r--r--bridges/BloombergBridge.php65
-rw-r--r--bridges/DanbooruBridge.php7
-rw-r--r--bridges/DealabsBridge.php474
-rw-r--r--bridges/DemonoidBridge.php146
-rw-r--r--bridges/DribbbleBridge.php91
-rw-r--r--bridges/FacebookBridge.php10
-rw-r--r--bridges/GelbooruBridge.php13
-rw-r--r--bridges/GoComicsBridge.php6
-rw-r--r--bridges/IPBBridge.php307
-rw-r--r--bridges/LWNprevBridge.php4
-rw-r--r--[-rwxr-xr-x]bridges/LeBonCoinBridge.php0
-rw-r--r--bridges/LegifranceJOBridge.php4
-rw-r--r--bridges/MixCloudBridge.php5
-rw-r--r--bridges/PcGamerBridge.php23
-rw-r--r--bridges/PinterestBridge.php62
-rw-r--r--bridges/PlanetLibreBridge.php38
-rw-r--r--bridges/SteamBridge.php75
-rw-r--r--bridges/TebeoBridge.php38
-rw-r--r--bridges/ThePirateBayBridge.php8
-rw-r--r--bridges/Torrent9Bridge.php2
-rw-r--r--bridges/TwitterBridge.php40
-rw-r--r--bridges/VineBridge.php40
-rw-r--r--bridges/VkBridge.php94
-rw-r--r--bridges/YoutubeBridge.php55
-rw-r--r--index.php115
-rw-r--r--lib/Bridge.php37
-rw-r--r--lib/Exceptions.php58
-rw-r--r--lib/FeedExpander.php6
-rw-r--r--lib/validation.php11
-rw-r--r--static/select.js10
-rw-r--r--static/style.css49
36 files changed, 1689 insertions, 497 deletions
diff --git a/.travis.yml b/.travis.yml
index 969c1b3..cd5e2d9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,9 @@
+dist: trusty
+sudo: false
language: php
-php:
- - '5.6'
- - '7.0'
- - hhvm
- - nightly
install:
+ - pear channel-update pear.php.net
- pear install PHP_CodeSniffer
script:
@@ -14,6 +12,13 @@ script:
matrix:
fast_finish: true
+
+ include:
+ - php: 5.6
+ - php: 7.0
+ - php: hhvm
+ - php: nightly
+
allow_failures:
- php: hhvm
- - php: nightly \ No newline at end of file
+ - php: nightly
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f589ff..467040e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,75 +1,105 @@
rss-bridge Changelog
===
+RSS-Bridge 2017-08-19
+==
+
+## General changes
+* whitelist: Do case-insensitive whitelist matching
+* [FeedExpander] Fix Serialization of 'SimpleXMLElement' is not allowed
+* [FeedExpander] Remove whitespace from source content
+* [index] Add GET parameter 'q' for search queries
+ - **Example**: You can now add `&q=Twitter` to load into the search field
+* [index] Check permissions for cache folder and whitelist file
+* [index] Show bridge options when loading with URL fragment
+ - **Example**: You can now add `#bridge-Twitter` to load the card with all
+parameters visible
+* [style] Center search cursor and hide placeholder
+* [validation] Fix error on undefined optional numeric value
+
+## Modified bridges
+* [DanbooruBridge] Allow descendant classes to override tag collection
+* [DribbbleBridge] Add dribble bridge listing last dribble popular shots (#558)
+* [FacebookBridge] Fix &amp; in URLs
+* [GelbooruBridge] Fix bridge not getting tags correctly
+* [GoComicsBridge] Fix for page structure changes (#568)
+* [LeBonCoinBridge] Fix bridge is marked executable
+* [LWNprevBridge] Fix everchanging url
+* [YoutubeBridge] Fix error on certain keywords
+* [YoutubeBridge] Fix issues loading playlists
+
+## Removed bridges
+* VineBridge
+
RSS-Bridge 2017-08-03
==
## Important changes
-RSS-Bridge now has [contribution guidelines](CONTRIBUTING.md)
-[phpcs rules](phpcs.xml) follow the [contribution guidelines](CONTRIBUTING.md)
+* RSS-Bridge now has [contribution guidelines](CONTRIBUTING.md)
+* [phpcs rules](phpcs.xml) follow the [contribution guidelines](CONTRIBUTING.md)
## General changes
-Added a search bar to make searching for bridges easier
-Added user friendly error page for when a bridge fails
-Added caching of extraInfos (name, uri)
-Added an indicator to warn for bridges using HTTP instead of HTTPS
-Various bug fixes and improvements
+* Added a search bar to make searching for bridges easier
+* Added user friendly error page for when a bridge fails
+* Added caching of extraInfos (name, uri)
+* Added an indicator to warn for bridges using HTTP instead of HTTPS
+* Various bug fixes and improvements
## Modified bridges
-[AllocineFRBridge] Update Faux Raccord link
-[DanbooruBridge] Fix broken URI
-[DuckDuckGoBridge] Disable DuckDuckGo redirects so that the links returned are correct.
-[FacebookBridge] Add option to hide posts with facebook videos
-[FacebookBridge] Add requester languages to HTTP header
-[FacebookBridge] Handle summary posts
-[FacebookBridge] Replace 'novideo' with 'media_type'
-[FilterBridge] Initial implementation of basic title permit and block
-[FlickrTagBridge] Fix and improve bridge by using the FlickrExploreBridge approach
-[GooglePlusPostBridge] Autofix user names
-[GooglePlusPostBridge] Fix bridge implementation
-[GooglePlusPostBridge] Fix content loading
-[InstagramBridge] Add option to filter for videos and pictures
-[LWNprevBridge] full rewrite
-[MangareaderBridge] Fix double forward slashes
-[NasaApodBridge] Use HTTPS instead of HTTP
-[PinterestBridge] Fix checkbox not working
-[PinterestBridge] Fix implementation after DOM changes
-[RTBFBridge] Update URI
-[SexactuBridge] Fix URI and timestamp
-[SexactuBridge] Use most modern version of bridge api and cached pages (#504)
-[ShanaprojectBridge] Don't throw error if timestamp is missing
-[TwitterBridge] Add option to hide retweets
-[TwitterBridge] Avoid empty content caused by new login policy
-[TwitterBridge] Fix double slashes in URI
-[TwitterBridge] Fix missing spaces
-[TwitterBridge] Fix title includes anchors in plaintext format
-[TwitterBridge] ignore promoted tweets
-[TwitterBridge] Optimize returned image sizes
-[TwitterBridge] Show quotes and pictures
-[WebfailBridge] Properly handle gifs (DOM changed)
-[YoutubeBridge] Improve readability of feed contents
-[YoutubeBridge] Improve URL handling in video descriptions
+* AllocineFRBridge] Update Faux Raccord link
+* [DanbooruBridge] Fix broken URI
+* [DuckDuckGoBridge] Disable DuckDuckGo redirects so that the links returned are correct.
+* [FacebookBridge] Add option to hide posts with facebook videos
+* [FacebookBridge] Add requester languages to HTTP header
+* [FacebookBridge] Handle summary posts
+* [FacebookBridge] Replace 'novideo' with 'media_type'
+* [FilterBridge] Initial implementation of basic title permit and block
+* [FlickrTagBridge] Fix and improve bridge by using the FlickrExploreBridge approach
+* [GooglePlusPostBridge] Autofix user names
+* [GooglePlusPostBridge] Fix bridge implementation
+* [GooglePlusPostBridge] Fix content loading
+* [InstagramBridge] Add option to filter for videos and pictures
+* [LWNprevBridge] full rewrite
+* [MangareaderBridge] Fix double forward slashes
+* [NasaApodBridge] Use HTTPS instead of HTTP
+* [PinterestBridge] Fix checkbox not working
+* [PinterestBridge] Fix implementation after DOM changes
+* [RTBFBridge] Update URI
+* [SexactuBridge] Fix URI and timestamp
+* [SexactuBridge] Use most modern version of bridge api and cached pages (#504)
+* [ShanaprojectBridge] Don't throw error if timestamp is missing
+* [TwitterBridge] Add option to hide retweets
+* [TwitterBridge] Avoid empty content caused by new login policy
+* [TwitterBridge] Fix double slashes in URI
+* [TwitterBridge] Fix missing spaces
+* [TwitterBridge] Fix title includes anchors in plaintext format
+* [TwitterBridge] ignore promoted tweets
+* [TwitterBridge] Optimize returned image sizes
+* [TwitterBridge] Show quotes and pictures
+* [WebfailBridge] Properly handle gifs (DOM changed)
+* [YoutubeBridge] Improve readability of feed contents
+* [YoutubeBridge] Improve URL handling in video descriptions
## New bridges
-AmazonBridge
-DiceBridge
-EtsyBridge
-FB2Bridge
-FilterBridge
-FlickrBridge
-GithubSearchBridge
-GoComicsBridge
-KATBridge
-KernelBugTrackerBridge
-MixCloudBridge
-MoinMoinBridge
-RainbowSixSiegeBridge
-SteamBridge
-TheTVDBBridge
-Torrent9Bridge
-UsbekEtRicaBridge
-WikiLeaksBridge
-WordPressPluginUpdateBridge
+* AmazonBridge
+* DiceBridge
+* EtsyBridge
+* FB2Bridge
+* FilterBridge
+* FlickrBridge
+* GithubSearchBridge
+* GoComicsBridge
+* KATBridge
+* KernelBugTrackerBridge
+* MixCloudBridge
+* MoinMoinBridge
+* RainbowSixSiegeBridge
+* SteamBridge
+* TheTVDBBridge
+* Torrent9Bridge
+* UsbekEtRicaBridge
+* WikiLeaksBridge
+* WordPressPluginUpdateBridge
Alpha 0.2
===
diff --git a/README.md b/README.md
index e36f22b..45bbc6d 100644
--- a/README.md
+++ b/README.md
@@ -7,23 +7,23 @@ rss-bridge is a PHP project capable of generating ATOM feeds for websites which
Supported sites/pages (main)
===
- * `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr
- * `GoogleSearch` : Most recent results from Google Search
- * `GooglePlus` : Most recent posts of user timeline
- * `Twitter` : Return keyword/hashtag search or user timeline
- * `Identi.ca` : Identica user timeline (Should be compatible with other Pump.io instances)
- * `YouTube` : YouTube user channel, playlist or search
- * `Cryptome` : Returns the most recent documents from [Cryptome.org](http://cryptome.org/)
- * `DansTonChat`: Most recent quotes from [danstonchat.com](http://danstonchat.com/)
- * `DuckDuckGo`: Most recent results from [DuckDuckGo.com](https://duckduckgo.com/)
- * `Instagram`: Most recent photos from an Instagram user
- * `OpenClassrooms`: Lastest tutorials from [fr.openclassrooms.com](http://fr.openclassrooms.com/)
- * `Pinterest`: Most recent photos from user or search
- * `ScmbBridge`: Newest stories from [secouchermoinsbete.fr](http://secouchermoinsbete.fr/)
- * `Wikipedia`: highlighted articles from [Wikipedia](https://wikipedia.org/) in English, German, French or Esperanto
- * `Bandcamp` : Returns last release from [bandcamp](https://bandcamp.com/) for a tag
- * `ThePirateBay` : Returns the newest indexed torrents from [The Pirate Bay](https://thepiratebay.se/) with keywords
- * `Facebook` : Returns the latest posts on a page or profile on [Facebook](https://facebook.com/)
+* `Bandcamp` : Returns last release from [bandcamp](https://bandcamp.com/) for a tag
+* `Cryptome` : Returns the most recent documents from [Cryptome.org](http://cryptome.org/)
+* `DansTonChat`: Most recent quotes from [danstonchat.com](http://danstonchat.com/)
+* `DuckDuckGo`: Most recent results from [DuckDuckGo.com](https://duckduckgo.com/)
+* `Facebook` : Returns the latest posts on a page or profile on [Facebook](https://facebook.com/)
+* `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr
+* `GooglePlus` : Most recent posts of user timeline
+* `GoogleSearch` : Most recent results from Google Search
+* `Identi.ca` : Identica user timeline (Should be compatible with other Pump.io instances)
+* `Instagram`: Most recent photos from an Instagram user
+* `OpenClassrooms`: Lastest tutorials from [fr.openclassrooms.com](http://fr.openclassrooms.com/)
+* `Pinterest`: Most recent photos from user or search
+* `ScmbBridge`: Newest stories from [secouchermoinsbete.fr](http://secouchermoinsbete.fr/)
+* `ThePirateBay` : Returns the newest indexed torrents from [The Pirate Bay](https://thepiratebay.se/) with keywords
+* `Twitter` : Return keyword/hashtag search or user timeline
+* `Wikipedia`: highlighted articles from [Wikipedia](https://wikipedia.org/) in English, German, French or Esperanto
+* `YouTube` : YouTube user channel, playlist or search
Plus [many other bridges](bridges/) to enable, thanks to the community
@@ -31,11 +31,11 @@ Output format
===
Output format can take several forms:
- * `Atom` : ATOM Feed, for use in RSS/Feed readers
- * `Mrss` : MRSS Feed, for use in RSS/Feed readers
- * `Json` : Json, for consumption by other applications.
- * `Html` : Simple html page.
- * `Plaintext` : raw text (php object, as returned by print_r)
+* `Atom` : ATOM Feed, for use in RSS/Feed readers
+* `Html` : Simple html page.
+* `Json` : Json, for consumption by other applications.
+* `Mrss` : MRSS Feed, for use in RSS/Feed readers
+* `Plaintext` : raw text (php object, as returned by print_r)
Screenshot
===
diff --git a/bridges/AllocineFRBridge.php b/bridges/AllocineFRBridge.php
index 604199b..959d0ef 100644
--- a/bridges/AllocineFRBridge.php
+++ b/bridges/AllocineFRBridge.php
@@ -26,7 +26,7 @@ class AllocineFRBridge extends BridgeAbstract {
switch($this->getInput('category')) {
case 'faux-raccord':
- $uri = static::URI . 'video/programme-12284/saison-29841/';
+ $uri = static::URI . 'video/programme-12284/saison-32180/';
break;
case 'top-5':
$uri = static::URI . 'video/programme-12299/saison-29561/';
@@ -64,7 +64,7 @@ class AllocineFRBridge extends BridgeAbstract {
self::PARAMETERS[$this->queriedContext]['category']['values']
);
- foreach($html->find('figure.media-meta-fig') as $element) {
+ foreach($html->find('.media-meta-list figure.media-meta-fig') as $element) {
$item = array();
$title = $element->find('div.titlebar h3.title a', 0);
diff --git a/bridges/Arte7Bridge.php b/bridges/Arte7Bridge.php
index 3d7ae9d..1162d17 100644
--- a/bridges/Arte7Bridge.php
+++ b/bridges/Arte7Bridge.php
@@ -3,24 +3,28 @@ class Arte7Bridge extends BridgeAbstract {
const MAINTAINER = 'mitsukarenai';
const NAME = 'Arte +7';
- const URI = 'http://www.arte.tv/';
+ const URI = 'https://www.arte.tv/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns newest videos from ARTE +7';
+
+ const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA';
+
const PARAMETERS = array(
'Catégorie (Français)' => array(
'catfr' => array(
'type' => 'list',
'name' => 'Catégorie',
'values' => array(
- 'Toutes les vidéos (français)' => 'toutes-les-videos',
- 'Actu & société' => 'actu-société',
- 'Séries & fiction' => 'séries-fiction',
- 'Cinéma' => 'cinéma',
- 'Arts & spectacles classiques' => 'arts-spectacles-classiques',
- 'Culture pop' => 'culture-pop',
- 'Découverte' => 'découverte',
- 'Histoire' => 'histoire',
- 'Junior' => 'junior'
+ 'Toutes les vidéos (français)' => null,
+ 'Actu & société' => 'ACT',
+ 'Séries & fiction' => 'SER',
+ 'Cinéma' => 'CIN',
+ 'Arts & spectacles classiques' => 'ARS',
+ 'Culture pop' => 'CPO',
+ 'Découverte' => 'DEC',
+ 'Histoire' => 'HIST',
+ 'Science' => 'SCI',
+ 'Autre' => 'AUT'
)
)
),
@@ -29,15 +33,16 @@ class Arte7Bridge extends BridgeAbstract {
'type' => 'list',
'name' => 'Catégorie',
'values' => array(
- 'Alle Videos (deutsch)' => 'alle-videos',
- 'Aktuelles & Gesellschaft' => 'aktuelles-gesellschaft',
- 'Fernsehfilme & Serien' => 'fernsehfilme-serien',
- 'Kino' => 'kino',
- 'Kunst & Kultur' => 'kunst-kultur',
- 'Popkultur & Alternativ' => 'popkultur-alternativ',
- 'Entdeckung' => 'entdeckung',
- 'Geschichte' => 'geschichte',
- 'Junior' => 'junior'
+ 'Alle Videos (deutsch)' => null,
+ 'Aktuelles & Gesellschaft' => 'ACT',
+ 'Fernsehfilme & Serien' => 'SER',
+ 'Kino' => 'CIN',
+ 'Kunst & Kultur' => 'ARS',
+ 'Popkultur & Alternativ' => 'CPO',
+ 'Entdeckung' => 'DEC',
+ 'Geschichte' => 'HIST',
+ 'Wissenschaft' => 'SCI',
+ 'Sonstiges' => 'AUT'
)
)
)
@@ -55,44 +60,39 @@ class Arte7Bridge extends BridgeAbstract {
break;
}
- $url = self::URI . 'guide/' . $lang . '/plus7/' . $category;
- $input = getContents($url) or die('Could not request ARTE.');
+ $url = 'https://api.arte.tv/api/opa/v3/videos?sort=-lastModified&limit=10&language='
+ . $lang
+ . ($category != null ? '&category.code=' . $category : '');
- if(strpos($input, 'categoryVideoSet') !== false) {
- $input = explode('categoryVideoSet="', $input);
- $input = explode('}}', $input[1]);
- $input = $input[0] . '}}';
- } else {
- $input = explode('videoSet="', $input);
- $input = explode('}]}', $input[1]);
- $input = $input[0] . '}]}';
- }
+ $context = array(
+ 'http' => array(
+ 'header' => 'Authorization: Bearer '. self::API_TOKEN
+ )
+ );
- $input_json = json_decode(html_entity_decode($input, ENT_QUOTES), true);
+ $input = getContents($url, false, stream_context_create($context)) or die('Could not request ARTE.');
+ $input_json = json_decode($input, true);
foreach($input_json['videos'] as $element) {
+
$item = array();
- $item['uri'] = str_replace("autoplay=1", "", $element['url']);
+ $item['uri'] = $element['url'];
$item['id'] = $element['id'];
- $hack_broadcast_time = $element['rights_end'];
- $hack_broadcast_time = strtok($hack_broadcast_time, 'T');
- $hack_broadcast_time = strtok('T');
-
- $item['timestamp'] = strtotime($element['scheduled_on'] . 'T' . $hack_broadcast_time);
+ $item['timestamp'] = strtotime($element['videoRightsBegin']);
$item['title'] = $element['title'];
if(!empty($element['subtitle']))
$item['title'] = $element['title'] . ' | ' . $element['subtitle'];
- $item['duration'] = round((int)$element['duration'] / 60);
- $item['content'] = $element['teaser']
+ $item['duration'] = round((int)$element['durationSeconds'] / 60);
+ $item['content'] = $element['teaserText']
. '<br><br>'
. $item['duration']
. 'min<br><a href="'
. $item['uri']
. '"><img src="'
- . $element['thumbnail_url']
+ . $element['mainImage']['url']
. '" /></a>';
$this->items[] = $item;
diff --git a/bridges/BloombergBridge.php b/bridges/BloombergBridge.php
new file mode 100644
index 0000000..8aff0ec
--- /dev/null
+++ b/bridges/BloombergBridge.php
@@ -0,0 +1,65 @@
+<?php
+class BloombergBridge extends BridgeAbstract
+{
+ const NAME = 'Bloomberg';
+ const URI = 'https://www.bloomberg.com/';
+ const DESCRIPTION = 'Trending stories from Bloomberg';
+ const MAINTAINER = 'mdemoss';
+
+ const PARAMETERS = array(
+ 'Trending Stories' => array(),
+ 'From Search' => array(
+ 'q' => array(
+ 'name' => 'Keyword',
+ 'required' => true
+ )
+ )
+ );
+
+ public function getName()
+ {
+ switch($this->queriedContext) {
+ case 'Trending Stories':
+ return self::NAME . ' Trending Stories';
+ case 'From Search':
+ if (!is_null($this->getInput('q'))) {
+ return self::NAME . ' Search : ' . $this->getInput('q');
+ }
+ break;
+ }
+
+ return parent::getName();
+ }
+
+ public function collectData()
+ {
+ switch($this->queriedContext) {
+ case 'Trending Stories': // Get list of top new <article>s from the front page.
+ $html = getSimpleHTMLDOMCached($this->getURI(), 300);
+ $stories = $html->find('ul.top-news-v3__stories article.top-news-v3-story');
+ break;
+ case 'From Search': // Get list of <article> elements from search.
+ $html = getSimpleHTMLDOMCached(
+ $this->getURI() .
+ 'search?sort=time:desc&page=1&query=' .
+ urlencode($this->getInput('q')), 300
+ );
+ $stories = $html->find('div.search-result-items article.search-result-story');
+ break;
+ }
+ foreach ($stories as $element) {
+ $item['uri'] = $element->find('h1 a', 0)->href;
+ if (preg_match('#^https://#i', $item['uri']) !== 1) {
+ $item['uri'] = $this->getURI() . $item['uri'];
+ }
+ $articleHtml = getSimpleHTMLDOMCached($item['uri']);
+ if (!$articleHtml) {
+ continue;
+ }
+ $item['title'] = $element->find('h1 a', 0)->plaintext;
+ $item['timestamp'] = strtotime($articleHtml->find('meta[name=iso-8601-publish-date],meta[name=date]', 0)->content);
+ $item['content'] = $articleHtml->find('meta[name=description]', 0)->content;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/DanbooruBridge.php b/bridges/DanbooruBridge.php
index f2cddf4..36b8c08 100644
--- a/bridges/DanbooruBridge.php
+++ b/bridges/DanbooruBridge.php
@@ -23,6 +23,7 @@ class DanbooruBridge extends BridgeAbstract {
const PATHTODATA = 'article';
const IDATTRIBUTE = 'data-id';
+ const TAGATTRIBUTE = 'alt';
protected function getFullURI(){
return $this->getURI()
@@ -30,6 +31,10 @@ class DanbooruBridge extends BridgeAbstract {
. '&tags=' . urlencode($this->getInput('t'));
}
+ protected function getTags($element){
+ return $element->find('img', 0)->getAttribute(static::TAGATTRIBUTE);
+ }
+
protected function getItemFromElement($element){
// Fix links
defaultLinkTo($element, $this->getURI());
@@ -39,7 +44,7 @@ class DanbooruBridge extends BridgeAbstract {
$item['postid'] = (int)preg_replace("/[^0-9]/", '', $element->getAttribute(static::IDATTRIBUTE));
$item['timestamp'] = time();
$thumbnailUri = $element->find('img', 0)->src;
- $item['tags'] = $element->find('img', 0)->getAttribute('alt');
+ $item['tags'] = $this->getTags($element);
$item['title'] = $this->getName() . ' | ' . $item['postid'];
$item['content'] = '<a href="'
. $item['uri']
diff --git a/bridges/DealabsBridge.php b/bridges/DealabsBridge.php
new file mode 100644
index 0000000..d2cab2f
--- /dev/null
+++ b/bridges/DealabsBridge.php
@@ -0,0 +1,474 @@
+<?php
+class DealabsBridge extends BridgeAbstract {
+ const NAME = 'Dealabs search bridge';
+ const URI = 'https://www.dealabs.com/';
+ const DESCRIPTION = 'Return the Dealabs search result using keywords';
+ const MAINTAINER = 'sysadminstory';
+ const PARAMETERS = array(
+ 'Recherche par Mot(s) clé(s)' => array (
+ 'q' => array(
+ 'name' => 'Mot(s) clé(s)',
+ 'type' => 'text',
+ 'required' => true
+ ),
+ 'hide_expired' => array(
+ 'name' => 'Masquer les éléments expirés',
+ 'type' => 'checkbox',
+ 'required' => 'true'
+ ),
+ 'hide_local' => array(
+ 'name' => 'Masquer les deals locaux',
+ 'type' => 'checkbox',
+ 'title' => 'Masquer les deals en magasins physiques',
+ 'required' => 'true'
+ ),
+ 'priceFrom' => array(
+ 'name' => 'Prix minimum',
+ 'type' => 'text',
+ 'title' => 'Prix mnimum en euros',
+ 'required' => 'false',
+ 'defaultValue' => ''
+ ),
+ 'priceTo' => array(
+ 'name' => 'Prix maximum',
+ 'type' => 'text',
+ 'title' => 'Prix maximum en euros',
+ 'required' => 'false',
+ 'defaultValue' => ''
+ ),
+ ),
+
+ 'Deals par groupe' => array(
+ 'groupe' => array(
+ 'name' => 'Groupe',
+ 'type' => 'list',
+ 'required' => 'true',
+ 'title' => 'Groupe dont il faut afficher les deals',
+ 'values' => array(
+ 'Accessoires & gadgets' => 'accessoires-gadgets',
+ 'Alimentation & boissons' => 'alimentation-boissons',
+ 'Animaux' => 'animaux',
+ 'Applis & logiciels' => 'applis-logiciels',
+ 'Consoles & jeux vidéo' => 'consoles-jeux-video',
+ 'Culture & divertissement' => 'culture-divertissement',
+ 'Gratuit' => 'gratuit',
+ 'Image, son & vidéo' => 'image-son-video',
+ 'Informatique' => 'informatique',
+ 'Jeux & jouets' => 'jeux-jouets',
+ 'Maison & jardin' => 'maison-jardin',
+ 'Mode & accessoires' => 'mode-accessoires',
+ 'Santé & cosmétiques' => 'hygiene-sante-cosmetiques',
+ 'Services divers' => 'services-divers',
+ 'Sports & plein air' => 'sports-plein-air',
+ 'Téléphonie' => 'telephonie',
+ 'Voyages & sorties' => 'voyages-sorties-restaurants'
+ )
+ ),
+ 'ordre' => array(
+ 'name' => 'Trier par',
+ 'type' => 'list',
+ 'required' => 'true',
+ 'title' => 'Ordre de tri des deals',
+ 'values' => array(
+ 'Du deal le plus Hot au moins Hot' => '',
+ 'Du deal le plus récent au plus ancien' => '-nouveaux',
+ 'Du deal le plus commentés au moins commentés' => '-commentes'
+ )
+ )
+ )
+ );
+
+ const CACHE_TIMEOUT = 3600;
+
+ public function collectData(){
+ switch($this->queriedContext) {
+ case 'Recherche par Mot(s) clé(s)':
+ return $this->collectDataMotsCles();
+ break;
+ case 'Deals par groupe':
+ return $this->collectDataGroupe();
+ break;
+ }
+ }
+
+ /**
+ * Get the Deal data from the choosen groupe in the choose order
+ */
+ public function collectDataGroupe()
+ {
+
+ $groupe = $this->getInput('groupe');
+ $ordre = $this->getInput('ordre');
+
+ $url = self::URI
+ . '/groupe/' . $groupe . $ordre;
+ $this->collectDeals($url);
+ }
+
+ /**
+ * Get the Deal data from the choosen keywords and parameters
+ */
+ public function collectDataMotsCles()
+ {
+ $q = $this->getInput('q');
+ $hide_expired = $this->getInput('hide_expired');
+ $hide_local = $this->getInput('hide_local');
+ $priceFrom = $this->getInput('priceFrom');
+ $priceTo = $this->getInput('priceFrom');
+
+ /* Even if the original website uses POST with the search page, GET works too */
+ $url = self::URI
+ . '/search/advanced?q='
+ . urlencode($q)
+ . '&hide_expired='. $hide_expired
+ . '&hide_local='. $hide_local
+ . '&priceFrom='. $priceFrom
+ . '&priceTo='. $priceTo
+ /* Some default parameters
+ * search_fields : Search in Titres & Descriptions & Codes
+ * sort_by : Sort the search by new deals
+ * time_frame : Search will not be on a limited timeframe
+ */
+ . '&search_fields[]=1&search_fields[]=2&search_fields[]=3&sort_by=new&time_frame=0';
+ $this->collectDeals($url);
+ }
+
+ /**
+ * Get the Deal data using the given URL
+ */
+ public function collectDeals($url){
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Could not request Dealabs.');
+ $list = $html->find('article');
+
+ // Deal Image Link CSS Selector
+ $selectorImageLink = implode(
+ ' ', /* Notice this is a space! */
+ array(
+ 'cept-thread-image-link',
+ 'imgFrame',
+ 'imgFrame--noBorder',
+ 'box--all-i',
+ 'thread-listImgCell',
+ )
+ );
+
+ // Deal Link CSS Selector
+ $selectorLink = implode(
+ ' ', /* Notice this is a space! */
+ array(
+ 'cept-tt',
+ 'thread-link',
+ 'linkPlain',
+ 'space--r-1',
+ 'size--all-s',
+ 'size--fromW3-m',
+ )
+ );
+
+ // Deal Hotness CSS Selector
+ $selectorHot = implode(
+ ' ', /* Notice this is a space! */
+ array(
+ 'flex',
+ 'flex--align-c',
+ 'flex--justify-space-between',
+ 'space--b-2',
+ )
+ );
+
+ // Deal Description CSS Selector
+ $selectorDescription = implode(
+ ' ', /* Notice this is a space! */
+ array(
+ 'cept-description-container',
+ 'overflow--wrap-break',
+ 'size--all-s',
+ 'size--fromW3-m',
+ )
+ );
+
+ // Deal Date CSS Selector
+ $selectorDate = implode(
+ ' ', /* Notice this is a space! */
+ array(
+ 'size--all-s',
+ 'flex',
+ 'flex--wrap',
+ 'flex--justify-e',
+ 'flex--grow-1',
+ )
+ );
+
+ // If there is no results, we don't parse the content because it display some random deals
+ $noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0);
+ if($noresult != null && $noresult->plaintext == 'Il n&#039;y a rien à afficher pour le moment :(') {
+ $this->items = array();
+ } else {
+ foreach($list as $deal) {
+ $item = array();
+ $item['uri'] = $deal->find('div[class=threadGrid-title]', 0)->find('a', 0)->href;
+ $item['title'] = $deal->find('a[class='. $selectorLink .']', 0
+ )->plaintext;
+ $item['author'] = $deal->find('span.thread-username', 0)->plaintext;
+ $item['content'] = '<table><tr><td><a href="'
+ . $deal->find(
+ 'a[class*='. $selectorImageLink .']', 0)->href
+ . '"><img src="'
+ . $this->getImage($deal)
+ . '"/></td><td><h2><a href="'
+ . $deal->find('a[class='. $selectorLink .']', 0)->href
+ . '">'
+ . $deal->find('a[class='. $selectorLink .']', 0)->innertext
+ . '</a></h2>'
+ . $this->getPrix($deal)
+ . $this->getReduction($deal)
+ . $this->getExpedition($deal)
+ . $this->getLivraison($deal)
+ . $this->getOrigine($deal)
+ . $deal->find('div[class='. $selectorDescription .']', 0)->innertext
+ . '</td><td>'
+ . $deal->find('div[class='. $selectorHot .']', 0)->children(0)->outertext
+ . '</td></table>';
+ $dealDateDiv = $deal->find('div[class='. $selectorDate .']', 0)
+ ->find('span[class=hide--toW3]');
+ $itemDate = end($dealDateDiv)->plaintext;
+ if(substr( $itemDate, 0, 6 ) === 'il y a') {
+ $item['timestamp'] = $this->relativeDateToTimestamp($itemDate);
+ } else {
+ $item['timestamp'] = $this->parseDate($itemDate);
+ }
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ /**
+ * Get the Price from a Deal if it exists
+ * @return string String of the deal price
+ */
+ private function getPrix($deal)
+ {
+ if($deal->find(
+ 'span[class*=thread-price]', 0) != null) {
+ return '<div>Prix : '
+ . $deal->find(
+ 'span[class*=thread-price]', 0
+ )->plaintext
+ . '</div>';
+ } else {
+ return '';
+ }
+ }
+
+
+ /**
+ * Get the Shipping costs from a Deal if it exists
+ * @return string String of the deal shipping Cost
+ */
+ private function getLivraison($deal)
+ {
+ if($deal->find('span[class*=cept-shipping-price]', 0) != null) {
+ if($deal->find('span[class*=cept-shipping-price]', 0)->children(0) != null) {
+ return '<div>Livraison : '
+ . $deal->find('span[class*=cept-shipping-price]', 0)->children(0)->innertext
+ . '</div>';
+ } else {
+ return '<div>Livraison : '
+ . $deal->find('span[class*=cept-shipping-price]', 0)->innertext
+ . '</div>';
+ }
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get the source of a Deal if it exists
+ * @return string String of the deal source
+ */
+ private function getOrigine($deal)
+ {
+ if($deal->find('a[class=text--color-greyShade]', 0) != null) {
+ return '<div>Origine : '
+ . $deal->find('a[class=text--color-greyShade]', 0)->outertext
+ . '</div>';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get the original Price and discout from a Deal if it exists
+ * @return string String of the deal original price and discount
+ */
+ private function getReduction($deal)
+ {
+ if($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) {
+ return '<div>Réduction : <span style="text-decoration: line-through;">'
+ . $deal->find(
+ 'span[class*=mute--text text--lineThrough]', 0
+ )->plaintext
+ . '</span>&nbsp;'
+ . $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0)->plaintext
+ . '</div>';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get the Picture URL from a Deal if it exists
+ * @return string String of the deal Picture URL
+ */
+ private function getImage($deal)
+ {
+
+ $selectorLazy = implode(
+ ' ', /* Notice this is a space! */
+ array(
+ 'thread-image',
+ 'width--all-auto',
+ 'height--all-auto',
+ 'imgFrame-img',
+ 'cept-thread-img',
+ 'img--dummy',
+ 'js-lazy-img'
+ )
+ );
+
+ $selectorPlain = implode(
+ ' ', /* Notice this is a space! */
+ array(
+ 'thread-image',
+ 'width--all-auto',
+ 'height--all-auto',
+ 'imgFrame-img',
+ 'cept-thread-img'
+ )
+ );
+ if($deal->find('img[class='. $selectorLazy .']', 0) != null) {
+ return json_decode(
+ html_entity_decode(
+ $deal->find('img[class='. $selectorLazy .']', 0)
+ ->getAttribute('data-lazy-img')))->{'src'};
+ } else {
+ return $deal->find('img[class='. $selectorPlain .']', 0 )->src;
+ }
+ }
+
+ /**
+ * Get the originating country from a Deal if it existsa
+ * @return string String of the deal originating country
+ */
+ private function getExpedition($deal)
+ {
+ $selector = implode(
+ ' ', /* Notice this is a space! */
+ array(
+ 'meta-ribbon',
+ 'overflow--wrap-off',
+ 'space--l-3',
+ 'text--color-greyShade'
+ )
+ );
+ if($deal->find('span[class='. $selector .']', 0) != null) {
+ return '<div>'
+ . $deal->find('span[class='. $selector .']', 0)->children(2)->plaintext
+ . '</div>';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Transforms a French date into a timestam
+ * @return int timestamp of the input date
+ */
+ private function parseDate($string)
+ {
+ $month_fr = array(
+ 'janvier',
+ 'février',
+ 'mars',
+ 'avril',
+ 'mai',
+ 'juin',
+ 'juillet',
+ 'août',
+ 'septembre',
+ 'octobre',
+ 'novembre',
+ 'décembre'
+ );
+ $month_en = array(
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+ );
+ $date_str = trim(str_replace($month_fr, $month_en, $string));
+
+ if(!preg_match('/[0-9]{4}/', $string)) {
+ $date_str .= ' ' . date('Y');
+ }
+ $date_str .= ' 00:00';
+
+ $date = DateTime::createFromFormat('j F Y H:i', $date_str);
+ return $date->getTimestamp();
+ }
+
+ /**
+ * Transforms a relate French date into a timestam
+ * @return int timestamp of the input date
+ */
+ private function relativeDateToTimestamp($str) {
+ $date = new DateTime();
+ $search = array(
+ 'il y a ',
+ 'min',
+ 'h',
+ 'jour',
+ 'jours',
+ 'mois',
+ 'ans',
+ 'et '
+ );
+ $replace = array(
+ '-',
+ 'minute',
+ 'hour',
+ 'day',
+ 'month',
+ 'year',
+ ''
+ );
+
+ $date->modify(str_replace($search, $replace, $str));
+ return $date->getTimestamp();
+ }
+
+ public function getName(){
+ switch($this->queriedContext) {
+ case 'Recherche par Mot(s) clé(s)':
+ return self::NAME . ' - Recherche : '. $this->getInput('q');
+ break;
+ case 'Deals par groupe':
+ $values = self::PARAMETERS['Deals par groupe']['groupe']['values'];
+ $groupe = array_search($this->getInput('groupe'), $values);
+ return self::NAME . ' - Groupe : '. $groupe;
+ break;
+ default: // Return default value
+ return self::NAME;
+ }
+ }
+
+}
diff --git a/bridges/DemonoidBridge.php b/bridges/DemonoidBridge.php
new file mode 100644
index 0000000..f99b80f
--- /dev/null
+++ b/bridges/DemonoidBridge.php
@@ -0,0 +1,146 @@
+<?php
+class DemonoidBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'metaMMA';
+ const NAME = 'Demonoid';
+ const URI = 'https://www.demonoid.pw/';
+ const DESCRIPTION = 'Returns results for the keywords (in all categories or
+ a specific category). You can put several keywords separated by a semicolon
+ (e.g. "one show;another show"). Searches can by done in a specific category;
+ category number must be specified. (All=0, Movies=1, Music=2, TV=3, Games=4,
+ Applications=5, Pictures=8, Anime=9, Comics=10, Books=11 Music Videos=8,
+ Audio Books=17). User feed takes the Uploader ID number (not uploader name)
+ as keyword. Uploader ID is found by clicking on uploader, clicking on
+ "View this user\'s torrents", and copying the number after "uid=". An entire
+ category feed is accomplished by leaving "keywords" box blank and using the
+ corresponding category number.';
+
+ const PARAMETERS = array( array(
+ 'q' => array(
+ 'name' => 'keywords/user ID/category, separated by semicolons',
+ 'exampleValue' => 'first list;second list;…',
+ 'required' => true
+ ),
+ 'crit' => array(
+ 'type' => 'list',
+ 'name' => 'Feed type',
+ 'values' => array(
+ 'search' => 'search',
+ 'category' => 'cat',
+ 'user' => 'usr'
+ )
+ ),
+ 'catCheck' => array(
+ 'type' => 'checkbox',
+ 'name' => 'Specify category for keyword search ?',
+ ),
+ 'cat' => array(
+ 'name' => 'Category number',
+ ),
+ ));
+
+ public function collectData() {
+
+ $catBool = $this->getInput('catCheck');
+ if($catBool) {
+ $catNum = $this->getInput('cat');
+ }
+ $critList = $this->getInput('crit');
+
+ $keywordsList = explode(';', $this->getInput('q'));
+ foreach($keywordsList as $keywords) {
+ switch($critList) {
+ case 'search':
+ if($catBool == false) {
+ $html = file_get_contents(
+ self::URI .
+ 'files/?category=0&subcategory=All&quality=All&seeded=2&external=2&query=' .
+ urlencode($keywords) . #not rawurlencode so space -> '+'
+ '&uid=0&sort='
+ ) or returnServerError('Could not request Demonoid.');
+ } else {
+ $html = file_get_contents(
+ self::URI .
+ 'files/?category=' .
+ rawurlencode($catNum) .
+ '&subcategory=All&quality=All&seeded=2&external=2&query=' .
+ urlencode($keywords) . #not rawurlencode so space -> '+'
+ '&uid=0&sort='
+ ) or returnServerError('Could not request Demonoid.');
+ }
+ break;
+ case 'usr':
+ $html = file_get_contents(
+ self::URI .
+ 'files/?uid=' .
+ rawurlencode($keywords) .
+ '&seeded=2'
+ ) or returnServerError('Could not request Demonoid.');
+ break;
+ case 'cat':
+ $html = file_get_contents(
+ self::URI .
+ 'files/?uid=0&category=' .
+ rawurlencode($keywords) .
+ '&subcategory=0&language=0&seeded=2&quality=0&query=&sort='
+ ) or returnServerError('Could not request Demonoid.');
+ break;
+ }
+
+ if(preg_match('~No torrents found~', $html)) {
+ returnServerError('No result for query ' . $keywords);
+ }
+
+ $bigTable = explode('<!-- start torrent list -->', $html)[1];
+ $last50 = explode('<!-- end torrent list -->', $bigTable)[0];
+ $dateChunk = explode('added_today', $last50);
+ $item = array ();
+
+ for($block = 1;$block < count($dateChunk);$block++) {
+ preg_match('~(?<=>Add).*?(?=<)~', $dateChunk[$block], $dateStr);
+ if(preg_match('~today~', $dateStr[0])) {
+ date_default_timezone_set('UTC');
+ $timestamp = mktime(0, 0, 0, gmdate('n'), gmdate('j'), gmdate('Y'));
+ } else {
+ preg_match('~(?<=ed on ).*\d+~', $dateStr[0], $fullDateStr);
+ date_default_timezone_set('UTC');
+ $dateObj = strptime($fullDateStr[0], '%A, %b %d, %Y');
+ $timestamp = mktime(0, 0, 0, $dateObj['tm_mon'] + 1, $dateObj['tm_mday'], 1900 + $dateObj['tm_year']);
+ }
+
+ $itemsChunk = explode('<!-- tstart -->', $dateChunk[$block]);
+
+ for($items = 1;$items < count($itemsChunk);$items++) {
+ $item = array();
+ $cols = explode('<td', $itemsChunk[$items]);
+ preg_match('~(?<=href=\"/).*?(?=\")~', $cols[1], $matches);
+ $item['id'] = self::URI . $matches[0];
+ preg_match('~(?<=href=\").*?(?=\")~', $cols[4], $matches);
+ $item['uri'] = $matches[0];
+ $item['timestamp'] = $timestamp;
+ preg_match('~(?<=href=\"/users/).*?(?=\")~', $cols[3], $matches);
+ $item['author'] = $matches[0];
+ preg_match('~(?<=/\">).*?(?=</a>)~', $cols[1], $matches);
+ $item['title'] = $matches[0];
+ preg_match('~(?<=green\">)\d+(?=</font>)~', $cols[8], $matches);
+ $item['seeders'] = $matches[0];
+ preg_match('~(?<=red\">)\d+(?=</font>)~', $cols[9], $matches);
+ $item['leechers'] = $matches[0];
+ preg_match('~(?<=>).*?(?=</td>)~', $cols[5], $matches);
+ $item['size'] = $matches[0];
+ $item['content'] = 'Uploaded by ' . $item['author']
+ . ' , Size ' . $item['size']
+ . '<br>seeders: '
+ . $item['seeders']
+ . ' | leechers: '
+ . $item['leechers']
+ . '<br><a href="'
+ . $item['id']
+ . '">info page</a>';
+ if(isset($item['title']))
+ $this->items[] = $item;
+ }
+ }
+ }
+ }
+}
diff --git a/bridges/DribbbleBridge.php b/bridges/DribbbleBridge.php
new file mode 100644
index 0000000..07c4c6e
--- /dev/null
+++ b/bridges/DribbbleBridge.php
@@ -0,0 +1,91 @@
+<?php
+class DribbbleBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'quentinus95';
+ const NAME = 'Dribbble popular shots';
+ const URI = 'https://dribbble.com';
+ const CACHE_TIMEOUT = 1800;
+ const DESCRIPTION = 'Returns the newest popular shots from Dribbble.';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI . '/shots')
+ or returnServerError('Error while downloading the website content');
+
+ $json = $this->loadEmbeddedJsonData($html);
+
+ foreach($html->find('li[id^="screenshot-"]') as $shot) {
+ $item = [];
+
+ $additional_data = $this->findJsonForShot($shot, $json);
+ if ($additional_data === null) {
+ $item['uri'] = self::URI . $shot->find('a', 0)->href;
+ $item['title'] = $shot->find('.dribbble-over strong', 0)->plaintext;
+ } else {
+ $item['timestamp'] = strtotime($additional_data['published_at']);
+ $item['uri'] = self::URI . $additional_data['path'];
+ $item['title'] = $additional_data['title'];
+ }
+
+ $item['author'] = trim($shot->find('.attribution-user a', 0)->plaintext);
+
+ $description = $shot->find('.comment', 0);
+ $item['content'] = $description === null ? '' : $description->plaintext;
+
+ $preview_path = $shot->find('picture source', 0)->attr['srcset'];
+ $item['content'] .= $this->getImageTag($preview_path, $item['title']);
+ $item['enclosures'] = [$this->getFullSizeImagePath($preview_path)];
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function loadEmbeddedJsonData($html){
+ $json = [];
+ $scripts = $html->find('script');
+
+ foreach($scripts as $script) {
+ if(strpos($script->innertext, 'newestShots') !== false) {
+ // fix single quotes
+ $script->innertext = str_replace('\'', '"', $script->innertext);
+
+ // fix JavaScript JSON (why do they not adhere to the standard?)
+ $script->innertext = preg_replace('/(\w+):/i', '"\1":', $script->innertext);
+
+ // find beginning of JSON array
+ $start = strpos($script->innertext, '[');
+
+ // find end of JSON array, compensate for missing character!
+ $end = strpos($script->innertext, '];') + 1;
+
+ // convert JSON to PHP array
+ $json = json_decode(substr($script->innertext, $start, $end - $start), true);
+ break;
+ }
+ }
+
+ return $json;
+ }
+
+ private function findJsonForShot($shot, $json){
+ foreach($json as $element) {
+ if(strpos($shot->getAttribute('id'), (string)$element['id']) !== false) {
+ return $element;
+ }
+ }
+
+ return null;
+ }
+
+ private function getImageTag($preview_path, $title){
+ return sprintf(
+ '<br /> <a href="%s"><img src="%s" alt="%s" /></a>',
+ $this->getFullSizeImagePath($preview_path),
+ $preview_path,
+ $title
+ );
+ }
+
+ private function getFullSizeImagePath($preview_path){
+ return str_replace('_1x', '', $preview_path);
+ }
+}
diff --git a/bridges/FacebookBridge.php b/bridges/FacebookBridge.php
index cc16196..90f3d74 100644
--- a/bridges/FacebookBridge.php
+++ b/bridges/FacebookBridge.php
@@ -46,7 +46,7 @@ class FacebookBridge extends BridgeAbstract {
if(is_array($matches) && count($matches) > 1) {
$link = $matches[1];
if(strpos($link, '/') === 0)
- $link = self::URI . $link . '"';
+ $link = self::URI . $link;
if(strpos($link, 'facebook.com/l.php?u=') !== false)
$link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&'));
return ' href="' . $link . '"';
@@ -155,7 +155,7 @@ class FacebookBridge extends BridgeAbstract {
//Show captcha filling form to the viewer, proxying the captcha image
$img = base64_encode(getContents($captcha->find('img', 0)->src));
- header('HTTP/1.1 500 ' . Http::getMessageForCode(500));
+ http_response_code(500);
header('Content-Type: text/html');
$message = <<<EOD
<form method="post" action="?{$_SERVER['QUERY_STRING']}">
@@ -281,9 +281,11 @@ EOD;
if(strlen($title) > 64)
$title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
+ $uri = self::URI . $post->find('abbr')[0]->parent()->getAttribute('href');
+
//Build and add final item
- $item['uri'] = self::URI . $post->find('abbr')[0]->parent()->getAttribute('href');
- $item['content'] = $content;
+ $item['uri'] = htmlspecialchars_decode($uri);
+ $item['content'] = htmlspecialchars_decode($content);
$item['title'] = $title;
$item['author'] = $author;
$item['timestamp'] = $date;
diff --git a/bridges/GelbooruBridge.php b/bridges/GelbooruBridge.php
index fa4ce11..4fe30e2 100644
--- a/bridges/GelbooruBridge.php
+++ b/bridges/GelbooruBridge.php
@@ -10,6 +10,7 @@ class GelbooruBridge extends DanbooruBridge {
const PATHTODATA = '.thumb';
const IDATTRIBUTE = 'id';
+ const TAGATTRIBUTE = 'title';
const PIDBYPAGE = 63;
@@ -19,4 +20,16 @@ class GelbooruBridge extends DanbooruBridge {
. ($this->getInput('p') ? ($this->getInput('p') - 1) * static::PIDBYPAGE : '')
. '&tags=' . urlencode($this->getInput('t'));
}
+
+ protected function getTags($element){
+ $tags = parent::getTags($element);
+ $tags = explode(' ', $tags);
+
+ // Remove statistics from the tags list (identified by colon)
+ foreach($tags as $key => $tag) {
+ if(strpos($tag, ':') !== false) unset($tags[$key]);
+ }
+
+ return implode(' ', $tags);
+ }
}
diff --git a/bridges/GoComicsBridge.php b/bridges/GoComicsBridge.php
index 27621d8..268bd90 100644
--- a/bridges/GoComicsBridge.php
+++ b/bridges/GoComicsBridge.php
@@ -18,10 +18,10 @@ class GoComicsBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request GoComics: ' . $this->getURI());
- foreach($html->find('div.item-comic-container') as $element) {
+ foreach($html->find('div.comic__container') as $element) {
- $img = $element->find('img', 0);
- $link = $element->find('a.item-comic-link', 0);
+ $img = $element->find('.item-comic-image img', 0);
+ $link = $element->find('a.js-item-comic-link', 0);
$comic = $img->src;
$title = $link->title;
$url = $html->find('input.js-copy-link', 0)->value;
diff --git a/bridges/IPBBridge.php b/bridges/IPBBridge.php
new file mode 100644
index 0000000..f3fa14f
--- /dev/null
+++ b/bridges/IPBBridge.php
@@ -0,0 +1,307 @@
+<?php
+class IPBBridge extends FeedExpander {
+
+ const NAME = 'IPB Bridge';
+ const URI = 'https://www.invisionpower.com';
+ const DESCRIPTION = 'Returns feeds for forums powered by IPB';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = array(
+ array(
+ 'uri' => array(
+ 'name' => 'URI',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert forum, subforum or topic URI',
+ 'exampleValue' => 'https://invisioncommunity.com/forums/forum/499-feedback-and-ideas/'
+ ),
+ 'limit' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Specify how many pages should be fetched (-1: all)',
+ 'defaultValue' => 1
+ )
+ )
+ );
+ const CACHE_TIMEOUT = 3600;
+
+ // Constants for internal use
+ const FORUM_TYPE_LIST_FILTER = '.cForumTopicTable';
+ const FORUM_TYPE_TABLE_FILTER = '#forum_table';
+
+ const TOPIC_TYPE_ARTICLE = 'article';
+ const TOPIC_TYPE_DIV = 'div.post_block';
+
+ public function getURI(){
+ return $this->getInput('uri') ?: parent::getURI();
+ }
+
+ public function collectData(){
+ // The URI cannot be the mainpage (or anything related)
+ switch(parse_url($this->getInput('uri'), PHP_URL_PATH)) {
+ case null:
+ case '/index.php':
+ returnClientError('Provided URI is invalid!');
+ break;
+ default:
+ break;
+ }
+
+ // Sanitize the URI (because else it won't work)
+ $uri = rtrim($this->getInput('uri'), '/'); // No trailing slashes!
+
+ // Forums might provide feeds, though that's optional *facepalm*
+ // Let's check if there is a valid feed available
+ $headers = get_headers($uri . '.xml');
+
+ if($headers[0] === 'HTTP/1.1 200 OK') { // Heureka! It's a valid feed!
+ return $this->collectExpandableDatas($uri);
+ }
+
+ // No valid feed, so do it the hard way
+ $html = getSimpleHTMLDOM($uri)
+ or returnServerError('Could not request ' . $this->getInput('uri') . '!');
+
+ $limit = $this->getInput('limit');
+
+ // Determine if this is a topic or a forum
+ switch(true) {
+ case $this->isTopic($html):
+ $this->collectTopic($html, $limit);
+ break;
+ case $this->isForum($html);
+ $this->collectForum($html);
+ break;
+ default:
+ returnClientError('Unknown type!');
+ break;
+ }
+ }
+
+ private function isForum($html){
+ return !is_null($html->find('div[data-controller*=forums.front.forum.forumPage]', 0))
+ || !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0));
+ }
+
+ private function isTopic($html){
+ return !is_null($html->find('div[data-controller*=core.front.core.commentFeed]', 0))
+ || !is_null($html->find(static::TOPIC_TYPE_DIV, 0));
+ }
+
+ private function collectForum($html){
+ // There are multiple forum designs in use (depends on version?)
+ // 1 - Uses an ordered list (based on https://invisioncommunity.com/forums)
+ // 2 - Uses a table (based on https://onehallyu.com)
+
+ switch(true) {
+ case !is_null($html->find(static::FORUM_TYPE_LIST_FILTER, 0)):
+ $this->collectForumList($html);
+ break;
+ case !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)):
+ $this->collectForumTable($html);
+ break;
+ default:
+ returnClientError('Unknown forum format!');
+ break;
+ }
+ }
+
+ private function collectForumList($html){
+ foreach($html->find(static::FORUM_TYPE_LIST_FILTER, 0)->children() as $row) {
+ // Columns: Title, Statistics, Last modified
+ $item = array();
+
+ $item['uri'] = $row->find('a', 0)->href;
+ $item['title'] = $row->find('a', 0)->title;
+ $item['author'] = $row->find('a', 1)->innertext;
+ $item['timestamp'] = strtotime($row->find('time', 0)->getAttribute('datetime'));
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function collectForumTable($html){
+ foreach($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)->children() as $row) {
+ // Columns: Icon, Content, Preview, Statistics, Last modified
+ $item = array();
+
+ // Skip header row
+ if(!is_null($row->find('th', 0))) continue;
+
+ $item['uri'] = $row->find('a', 0)->href;
+ $item['title'] = $row->find('.title', 0)->plaintext;
+ $item['timestamp'] = strtotime($row->find('[itemprop=dateCreated]', 0)->plaintext);
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function collectTopic($html, $limit){
+ // There are multiple topic designs in use (depends on version?)
+ // 1 - Uses articles (based on https://invisioncommunity.com/forums)
+ // 2 - Uses divs (based on https://onehallyu.com)
+
+ switch(true) {
+ case !is_null($html->find(static::TOPIC_TYPE_ARTICLE, 0)):
+ $this->collectTopicHistory($html, $limit, 'collectTopicArticle');
+ break;
+ case !is_null($html->find(static::TOPIC_TYPE_DIV, 0)):
+ $this->collectTopicHistory($html, $limit, 'collectTopicDiv');
+ break;
+ default:
+ returnClientError('Unknown topic format!');
+ break;
+ }
+ }
+
+ private function collectTopicHistory($html, $limit, $callback){
+ // Make sure the callback is valid!
+ if(!method_exists($this, $callback))
+ returnServerError('Unknown function (\'' . $callback . '\')!');
+
+ $next = null; // Holds the URI of the next page
+
+ do {
+ // Skip loading HTML on first iteration
+ if(!is_null($next)) {
+ $html = getSimpleHTMLDOMCached($next);
+ }
+
+ $next = $this->$callback($html, is_null($next));
+ $limit--;
+ } while(!is_null($next) && $limit <> 0);
+ }
+
+ private function collectTopicArticle($html, $firstrun = true){
+ $title = $html->find('h1.ipsType_pageTitle', 0)->plaintext;
+
+ // Are we on last page?
+ if($firstrun && !is_null($html->find('.ipsPagination', 0))) {
+ $last = $html->find('.ipsPagination_last a', 0)->{'data-page'};
+ $active = $html->find('.ipsPagination_active a', 0)->{'data-page'};
+
+ if($active !== $last) {
+ // Load last page into memory (cached)
+ $html = getSimpleHTMLDOMCached($html->find('.ipsPagination_last a', 0)->href);
+ }
+ }
+
+ foreach(array_reverse($html->find(static::TOPIC_TYPE_ARTICLE)) as $article) {
+ $item = array();
+
+ $item['uri'] = $article->find('time', 0)->parent()->href;
+ $item['author'] = $article->find('aside a', 0)->plaintext;
+ $item['title'] = $item['author'] . ' - ' . $title;
+ $item['timestamp'] = strtotime($article->find('time', 0)->getAttribute('datetime'));
+
+ $content = $article->find('[data-role=commentContent]', 0);
+ $content = $this->scaleImages($content);
+ $item['content'] = $this->fixContent($content);
+ $item['enclosures'] = $this->findImages($article->find('[data-role=commentContent]', 0)) ?: null;
+
+ $this->items[] = $item;
+ }
+
+ // Return whatever page comes next (previous, as we add in inverse order)
+ // Do we have a previous page? (inactive means no)
+ if(!is_null($html->find('li[class=ipsPagination_prev ipsPagination_inactive]', 0))) {
+ return null; // No, or no more
+ } elseif(!is_null($html->find('li[class=ipsPagination_prev]', 0))) {
+ return $html->find('.ipsPagination_prev a', 0)->href;
+ }
+
+ return null;
+ }
+
+ private function collectTopicDiv($html, $firstrun = true){
+ $title = $html->find('h1.ipsType_pagetitle', 0)->plaintext;
+
+ // Are we on last page?
+ if($firstrun && !is_null($html->find('.pagination', 0))) {
+
+ $active = $html->find('li[class=page active]', 0)->plaintext;
+
+ // There are two ways the 'last' page is displayed:
+ // - With a distict 'last' button (only if there are enough pages)
+ // - With a button for each page (use last button)
+ if(!is_null($html->find('li.last', 0))) {
+ $last = $html->find('li.last a', 0);
+ } else {
+ $last = $html->find('li[class=page] a', -1);
+ }
+
+ if($active !== $last->plaintext) {
+ // Load last page into memory (cached)
+ $html = getSimpleHTMLDOMCached($last->href);
+ }
+ }
+
+ foreach(array_reverse($html->find(static::TOPIC_TYPE_DIV)) as $article) {
+ $item = array();
+
+ $item['uri'] = $article->find('a[rel=bookmark]', 0)->href;
+ $item['author'] = $article->find('.author', 0)->plaintext;
+ $item['title'] = $item['author'] . ' - ' . $title;
+ $item['timestamp'] = strtotime($article->find('.published', 0)->getAttribute('title'));
+
+ $content = $article->find('[itemprop=commentText]', 0);
+ $content = $this->scaleImages($content);
+ $item['content'] = $this->fixContent($content);
+
+ $item['enclosures'] = $this->findImages($article->find('.post_body', 0)) ?: null;
+
+ $this->items[] = $item;
+ }
+
+ // Return whatever page comes next (previous, as we add in inverse order)
+ // Do we have a previous page?
+ if(!is_null($html->find('li.prev', 0))) {
+ return $html->find('li.prev a', 0)->href;
+ }
+
+ return null;
+ }
+
+ /** Returns all images from the provide HTML DOM */
+ private function findImages($html){
+ $images = array();
+
+ foreach($html->find('img') as $img) {
+ $images[] = $img->src;
+ }
+
+ return $images;
+ }
+
+ /** Sets the maximum width and height for all images */
+ private function scaleImages($html, $width = 400, $height = 400){
+ foreach($html->find('img') as $img) {
+ $img->style = "max-width: {$width}px; max-height: {$height}px;";
+ }
+
+ return $html;
+ }
+
+ /** Removes all unnecessary tags and adds formatting */
+ private function fixContent($html){
+
+ // Restore quote highlighting
+ foreach($html->find('blockquote') as $quote) {
+ $quote->style = <<<EOD
+padding: 0px 15px;
+border-width: 1px 1px 1px 2px;
+border-style: solid;
+border-color: #ededed #e8e8e8 #dbdbdb #666666;
+background: #fbfbfb;
+EOD;
+ }
+
+ // Remove unnecessary tags
+ $content = strip_tags(
+ $html->innertext,
+ '<p><a><img><ol><ul><li><table><tr><th><td><strong><blockquote><br><hr><h>'
+ );
+
+ return $content;
+ }
+}
diff --git a/bridges/LWNprevBridge.php b/bridges/LWNprevBridge.php
index 6d71c9d..0b79aeb 100644
--- a/bridges/LWNprevBridge.php
+++ b/bridges/LWNprevBridge.php
@@ -188,9 +188,9 @@ EOD;
$item = array();
- $item['uri'] = self::URI.'#'.microtime(true);
+ $item['uri'] = self::URI.'#'.count($items);
- $item['timestamp'] = $this->editionTimeStamp;//+$URICounter;
+ $item['timestamp'] = $this->editionTimeStamp;
$item['author'] = 'LWN';
diff --git a/bridges/LeBonCoinBridge.php b/bridges/LeBonCoinBridge.php
index d4da546..d4da546 100755..100644
--- a/bridges/LeBonCoinBridge.php
+++ b/bridges/LeBonCoinBridge.php
diff --git a/bridges/LegifranceJOBridge.php b/bridges/LegifranceJOBridge.php
index 348be8f..d97fcff 100644
--- a/bridges/LegifranceJOBridge.php
+++ b/bridges/LegifranceJOBridge.php
@@ -42,10 +42,10 @@ class LegifranceJOBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI)
or $this->returnServer('Unable to download ' . self::URI);
- $this->author = trim($html->find('h2.title', 0)->plaintext);
+ $this->author = trim($html->find('h2.titleJO', 0)->plaintext);
$uri = $html->find('h2.titleELI', 0)->plaintext;
$this->uri = trim(substr($uri, strpos($uri, 'https')));
- $this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/')));
+ $this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/'), -5));
foreach($html->find('h3') as $section) {
$subsections = $section->nextSibling()->find('h4');
diff --git a/bridges/MixCloudBridge.php b/bridges/MixCloudBridge.php
index aa6340a..723f634 100644
--- a/bridges/MixCloudBridge.php
+++ b/bridges/MixCloudBridge.php
@@ -4,7 +4,7 @@ class MixCloudBridge extends BridgeAbstract {
const MAINTAINER = 'Alexis CHEMEL';
const NAME = 'MixCloud';
- const URI = 'https://mixcloud.com/';
+ const URI = 'https://www.mixcloud.com';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns latest musics on user stream';
@@ -24,8 +24,9 @@ class MixCloudBridge extends BridgeAbstract {
}
public function collectData(){
+ ini_set('user_agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0');
- $html = getSimpleHTMLDOM(self::URI . $this->getInput('u'))
+ $html = getSimpleHTMLDOM(self::URI . '/' . $this->getInput('u'))
or returnServerError('Could not request MixCloud.');
foreach($html->find('section.card') as $element) {
diff --git a/bridges/PcGamerBridge.php b/bridges/PcGamerBridge.php
new file mode 100644
index 0000000..e0e55ce
--- /dev/null
+++ b/bridges/PcGamerBridge.php
@@ -0,0 +1,23 @@
+<?php
+class PcGamerBridge extends BridgeAbstract
+{
+ const NAME = 'PC Gamer';
+ const URI = 'https://www.pcgamer.com/';
+ const DESCRIPTION = 'PC Gamer Most Read Stories';
+ const MAINTAINER = 'mdemoss';
+
+ public function collectData()
+ {
+ $html = getSimpleHTMLDOMCached($this->getURI(), 300);
+ $stories = $html->find('div#popularcontent li.most-popular-item');
+ foreach ($stories as $element) {
+ $item['uri'] = $element->find('a', 0)->href;
+ $articleHtml = getSimpleHTMLDOMCached($item['uri']);
+ $item['title'] = $element->find('h4 a', 0)->plaintext;
+ $item['timestamp'] = strtotime($articleHtml->find('meta[name=pub_date]', 0)->content);
+ $item['content'] = $articleHtml->find('meta[name=description]', 0)->content;
+ $item['author'] = $articleHtml->find('a[itemprop=author]', 0)->plaintext;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/PinterestBridge.php b/bridges/PinterestBridge.php
index c5282ff..7eeafc1 100644
--- a/bridges/PinterestBridge.php
+++ b/bridges/PinterestBridge.php
@@ -15,12 +15,6 @@ class PinterestBridge extends FeedExpander {
'b' => array(
'name' => 'board',
'required' => true
- ),
- 'r' => array(
- 'name' => 'Use custom RSS',
- 'type' => 'checkbox',
- 'required' => false,
- 'title' => 'Uncheck to return data via custom filters (more data)'
)
),
'From search' => array(
@@ -34,12 +28,8 @@ class PinterestBridge extends FeedExpander {
public function collectData(){
switch($this->queriedContext) {
case 'By username and board':
- if($this->getInput('r')) {
- $html = getSimpleHTMLDOMCached($this->getURI());
- $this->getUserResults($html);
- } else {
- $this->collectExpandableDatas($this->getURI() . '.rss');
- }
+ $this->collectExpandableDatas($this->getURI() . '.rss');
+ $this->fixLowRes();
break;
case 'From search':
default:
@@ -48,49 +38,17 @@ class PinterestBridge extends FeedExpander {
}
}
- private function getUserResults($html){
- $json = json_decode($html->find('#jsInit1', 0)->innertext, true);
- $results = $json['tree']['children'][0]['children'][0]['children'][0]['options']['props']['data']['board_feed'];
- $username = $json['resourceDataCache'][0]['data']['owner']['username'];
- $fullname = $json['resourceDataCache'][0]['data']['owner']['full_name'];
- $avatar = $json['resourceDataCache'][0]['data']['owner']['image_small_url'];
-
- foreach($results as $result) {
- $item = array();
-
- $item['uri'] = $result['link'];
-
- // Some use regular titles, others provide 'advanced' infos, a few
- // provide even less info. Thus we attempt multiple options.
- $item['title'] = trim($result['title']);
-
- if($item['title'] === "")
- $item['title'] = trim($result['rich_summary']['display_name']);
-
- if($item['title'] === "")
- $item['title'] = trim($result['description']);
-
- $item['timestamp'] = strtotime($result['created_at']);
- $item['username'] = $username;
- $item['fullname'] = $fullname;
- $item['avatar'] = $avatar;
- $item['author'] = $item['username'] . ' (' . $item['fullname'] . ')';
- $item['content'] = '<img align="left" style="margin: 2px 4px;" src="'
- . htmlentities($item['avatar'])
- . '" /><p><strong>'
- . $item['username']
- . '</strong><br>'
- . $item['fullname']
- . '</p><br><img src="'
- . $result['images']['736x']['url']
- . '" alt="" /><br><p>'
- . $result['description']
- . '</p>';
+ private function fixLowRes() {
- $item['enclosures'] = array($result['images']['orig']['url']);
+ $newitems = [];
+ $pattern = '/https\:\/\/i\.pinimg\.com\/[a-zA-Z0-9]*x\//';
+ foreach($this->items as $item) {
- $this->items[] = $item;
+ $item["content"] = preg_replace($pattern, 'https://i.pinimg.com/originals/', $item["content"]);
+ $newitems[] = $item;
}
+ $this->items = $newitems;
+
}
private function getSearchResults($html){
diff --git a/bridges/PlanetLibreBridge.php b/bridges/PlanetLibreBridge.php
deleted file mode 100644
index 03a6024..0000000
--- a/bridges/PlanetLibreBridge.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-class PlanetLibreBridge extends BridgeAbstract {
-
- const MAINTAINER = 'pit-fgfjiudghdf';
- const NAME = 'PlanetLibre';
- const URI = 'http://www.planet-libre.org';
- const DESCRIPTION = 'Returns the 5 newest posts from PlanetLibre (full text)';
-
- private function extractContent($url){
- $html2 = getSimpleHTMLDOM($url);
- $text = $html2->find('div[class="post-text"]', 0)->innertext;
- return $text;
- }
-
- public function collectData(){
- $html = getSimpleHTMLDOM(self::URI)
- or returnServerError('Could not request PlanetLibre.');
- $limit = 0;
- foreach($html->find('div.post') as $element) {
- if($limit < 5) {
- $item = array();
- $item['title'] = $element->find('h1', 0)->plaintext;
- $item['uri'] = $element->find('a', 0)->href;
- $item['timestamp'] = strtotime(
- str_replace(
- '/',
- '-',
- $element->find('div[class="post-date"]', 0)->plaintext
- )
- );
-
- $item['content'] = $this->extractContent($item['uri']);
- $this->items[] = $item;
- $limit++;
- }
- }
- }
-}
diff --git a/bridges/SteamBridge.php b/bridges/SteamBridge.php
index b0f1033..8d6e4f1 100644
--- a/bridges/SteamBridge.php
+++ b/bridges/SteamBridge.php
@@ -2,7 +2,7 @@
class SteamBridge extends BridgeAbstract {
const NAME = 'Steam Bridge';
- const URI = 'https://steamcommunity.com/';
+ const URI = 'https://store.steampowered.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns games list';
const MAINTAINER = 'jacknumber';
@@ -68,62 +68,65 @@ class SteamBridge extends BridgeAbstract {
$username = $this->getInput('username');
$params = array(
- 'sort' => $this->getInput('sort'),
- 'cc' => $this->getInput('currency')
+ 'cc' => $this->getInput('currency'),
+ 'sort' => $this->getInput('sort')
);
- $url = self::URI . 'id/' . $username . '/wishlist?' . http_build_query($params);
+ $url = self::URI . 'wishlist/id/' . $username . '/?' . http_build_query($params);
- $html = '';
- $html = getSimpleHTMLDOM($url)
+ $jsonDataRegex = '/var g_rg(?:WishlistData|AppInfo) = ([^;]*)/';
+ $content = getContents($url)
or returnServerError("Could not request Steam Wishlist. Tried:\n - $url");
- foreach($html->find('#wishlist_items .wishlistRow') as $element) {
+ preg_match_all($jsonDataRegex, $content, $matches, PREG_SET_ORDER, 0);
- $gameTitle = $element->find('h4', 0)->plaintext;
- $gameUri = $element->find('.storepage_btn_ctn a', 0)->href;
- $gameImg = $element->find('.gameListRowLogo img', 0)->src;
+ $appList = json_decode($matches[0][1], true);
+ $fullAppList = json_decode($matches[1][1], true);
+ //var_dump($matches[1][1]);
+ //var_dump($fullAppList);
+ $sortedElementList = array_fill(0, count($appList), 0);
+ foreach($appList as $app) {
- $discountBlock = $element->find('.discount_block', 0);
+ $sortedElementList[$app["priority"] - 1] = $app["appid"];
- if($element->find('.discount_block', 0)) {
- $gameHasPromo = 1;
- } else {
+ }
- if($this->getInput('only_discount')) {
- continue;
- }
+ foreach($sortedElementList as $appId) {
- $gameHasPromo = 0;
+ $app = $fullAppList[$appId];
+ $gameTitle = $app["name"];
+ $gameUri = "http://store.steampowered.com/app/" . $appId . "/";
+ $gameImg = $app["capsule"];
- }
+ $item = array();
+ $item['uri'] = $gameUri;
+ $item['title'] = $gameTitle;
- if($gameHasPromo) {
+ if(count($app["subs"]) > 0) {
+ if($app["subs"][0]["discount_pct"] != 0) {
- $gamePromoValue = $discountBlock->find('.discount_pct', 0)->plaintext;
- $gameOldPrice = $discountBlock->find('.discount_original_price', 0)->plaintext;
- $gameNewPrice = $discountBlock->find('.discount_final_price', 0)->plaintext;
- $gamePrice = $gameNewPrice;
+ $item['promoValue'] = $app["subs"][0]["discount_pct"];
+ $item['oldPrice'] = $app["subs"][0]["price"] / 100 / ((100 - $gamePromoValue / 100));
+ $item['newPrice'] = $app["subs"][0]["price"] / 100;
+ $item['price'] = $item['newPrice'];
- } else {
- $gamePrice = $element->find('.gameListPriceData .price', 0)->plaintext;
- }
+ $item['hasPromo'] = true;
- $item = array();
- $item['uri'] = $gameUri;
- $item['title'] = $gameTitle;
- $item['price'] = $gamePrice;
- $item['hasPromo'] = $gameHasPromo;
+ } else {
- if($gameHasPromo) {
+ if($this->getInput('only_discount')) {
+ continue;
+ }
- $item['promoValue'] = $gamePromoValue;
- $item['oldPrice'] = $gameOldPrice;
- $item['newPrice'] = $gameNewPrice;
+ $item['price'] = $app["subs"][0]["price"] / 100;
+ $item['hasPromo'] = false;
+ }
}
$this->items[] = $item;
+
}
+
}
}
diff --git a/bridges/TebeoBridge.php b/bridges/TebeoBridge.php
new file mode 100644
index 0000000..9050439
--- /dev/null
+++ b/bridges/TebeoBridge.php
@@ -0,0 +1,38 @@
+<?php
+class TebeoBridge extends FeedExpander {
+ const NAME = 'Tébéo Bridge';
+ const URI = 'http://www.tebeo.bzh/';
+ const CACHE_TIMEOUT = 21600; //6h
+ const DESCRIPTION = 'Returns the newest Tébéo videos by category';
+ const MAINTAINER = 'Mitsukarenai';
+
+ const PARAMETERS = array( array(
+ 'cat' => array(
+ 'name' => 'Catégorie',
+ 'type' => 'list',
+ 'values' => array(
+ 'Toutes les vidéos' => '/',
+ 'Actualité' => '/14-actualite',
+ 'Sport' => '/3-sport',
+ 'Culture-Loisirs' => '/5-culture-loisirs',
+ 'Société' => '/15-societe',
+ 'Langue Bretonne' => '/9-langue-bretonne'
+ )
+ )
+ ));
+
+ public function collectData(){
+ $url = self::URI . '/le-replay/' . $this->getInput('cat');
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Could not request Tébéo.');
+
+ foreach($html->find('div[id=items_replay] div.replay') as $element) {
+ $item = array();
+ $item['uri'] = $element->find('a', 0)->href;
+ $item['title'] = $element->find('h3', 0)->plaintext;
+ $item['timestamp'] = strtotime($element->find('p.moment-format-day', 0)->plaintext);
+ $item['content'] = '<a href="'.$item['uri'].'"><img alt="" src="'.$element->find('img', 0)->src.'"></a>';
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/ThePirateBayBridge.php b/bridges/ThePirateBayBridge.php
index 103737d..0deaded 100644
--- a/bridges/ThePirateBayBridge.php
+++ b/bridges/ThePirateBayBridge.php
@@ -11,7 +11,7 @@ class ThePirateBayBridge extends BridgeAbstract {
const PARAMETERS = array( array(
'q' => array(
- 'name' => 'keywords, separated by semicolons',
+ 'name' => 'keywords/username/category, separated by semicolons',
'exampleValue' => 'first list;second list;…',
'required' => true
),
@@ -24,9 +24,9 @@ class ThePirateBayBridge extends BridgeAbstract {
'user' => 'usr'
)
),
- 'cat_check' => array(
+ 'catCheck' => array(
'type' => 'checkbox',
- 'name' => 'Specify category for normal search ?',
+ 'name' => 'Specify category for keyword search ?',
),
'cat' => array(
'name' => 'Category number',
@@ -94,7 +94,7 @@ class ThePirateBayBridge extends BridgeAbstract {
return $timestamp;
}
- $catBool = $this->getInput('cat_check');
+ $catBool = $this->getInput('catCheck');
if($catBool) {
$catNum = $this->getInput('cat');
}
diff --git a/bridges/Torrent9Bridge.php b/bridges/Torrent9Bridge.php
index 742e777..40db4ac 100644
--- a/bridges/Torrent9Bridge.php
+++ b/bridges/Torrent9Bridge.php
@@ -3,7 +3,7 @@ class Torrent9Bridge extends BridgeAbstract {
const MAINTAINER = 'lagaisse';
const NAME = 'Torrent9 Bridge';
- const URI = 'http://www.torrent9.biz';
+ const URI = 'http://www.torrent9.pe';
const CACHE_TIMEOUT = 86400; // 24h = 86400s
const DESCRIPTION = 'Returns latest torrents';
diff --git a/bridges/TwitterBridge.php b/bridges/TwitterBridge.php
index d588f6b..aedb372 100644
--- a/bridges/TwitterBridge.php
+++ b/bridges/TwitterBridge.php
@@ -44,6 +44,25 @@ class TwitterBridge extends BridgeAbstract {
'type' => 'checkbox',
'title' => 'Hide retweets'
)
+ ),
+ 'By list' => array(
+ 'user' => array(
+ 'name' => 'User',
+ 'required' => true,
+ 'exampleValue' => 'sebsauvage',
+ 'title' => 'Insert a user name'
+ ),
+ 'list' => array(
+ 'name' => 'List',
+ 'required' => true,
+ 'title' => 'Insert the list name'
+ ),
+ 'filter' => array(
+ 'name' => 'Filter',
+ 'exampleValue' => '#rss-bridge',
+ 'required' => false,
+ 'title' => 'Specify term to search for'
+ )
)
);
@@ -57,6 +76,8 @@ class TwitterBridge extends BridgeAbstract {
$specific = '@';
$param = 'u';
break;
+ case 'By list':
+ return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user');
default: return parent::getName();
}
return 'Twitter ' . $specific . $this->getInput($param);
@@ -74,6 +95,11 @@ class TwitterBridge extends BridgeAbstract {
. urlencode($this->getInput('u'));
// Always return without replies!
// . ($this->getInput('norep') ? '' : '/with_replies');
+ case 'By list':
+ return self::URI
+ . urlencode($this->getInput('user'))
+ . '/lists/'
+ . str_replace(' ', '-', strtolower($this->getInput('list')));
default: return parent::getURI();
}
}
@@ -88,6 +114,8 @@ class TwitterBridge extends BridgeAbstract {
returnServerError('No results for this query.');
case 'By username':
returnServerError('Requested username can\'t be found.');
+ case 'By list':
+ returnServerError('Requested username or list can\'t be found');
}
}
@@ -132,6 +160,18 @@ class TwitterBridge extends BridgeAbstract {
// generate the title
$item['title'] = strip_tags($this->fixAnchorSpacing($tweet->find('p.js-tweet-text', 0), '<a>'));
+ switch($this->queriedContext) {
+ case 'By list':
+ // Check if filter applies to list (using raw content)
+ if($this->getInput('filter')) {
+ if(stripos($tweet->find('p.js-tweet-text', 0)->plaintext, $this->getInput('filter')) === false) {
+ continue 2; // switch + for-loop!
+ }
+ }
+ break;
+ default:
+ }
+
$this->processContentLinks($tweet);
$this->processEmojis($tweet);
diff --git a/bridges/VineBridge.php b/bridges/VineBridge.php
deleted file mode 100644
index 61534a0..0000000
--- a/bridges/VineBridge.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-class VineBridge extends BridgeAbstract {
-
- const MAINTAINER = 'ckiw';
- const NAME = 'Vine bridge';
- const URI = 'http://vine.co/';
- const DESCRIPTION = 'Returns the latests vines from vine user page';
-
- const PARAMETERS = array( array(
- 'u' => array(
- 'name' => 'User id',
- 'required' => true
- )
- ));
-
- public function collectData(){
- $html = '';
- $uri = self::URI . '/u/' . $this->getInput('u') . '?mode=list';
-
- $html = getSimpleHTMLDOM($uri)
- or returnServerError('No results for this query.');
-
- foreach($html->find('.post') as $element) {
- $a = $element->find('a', 0);
- $a->href = str_replace('https://', 'http://', $a->href);
- $time = strtotime(ltrim($element->find('p', 0)->plaintext, ' Uploaded at '));
- $video = $element->find('video', 0);
- $video->controls = 'true';
- $element->find('h2', 0)->outertext = '';
-
- $item = array();
- $item['uri'] = $a->href;
- $item['timestamp'] = $time;
- $item['title'] = $a->plaintext;
- $item['content'] = $element;
-
- $this->items[] = $item;
- }
- }
-}
diff --git a/bridges/VkBridge.php b/bridges/VkBridge.php
index 9981da1..4eba961 100644
--- a/bridges/VkBridge.php
+++ b/bridges/VkBridge.php
@@ -1,9 +1,11 @@
<?php
-class VkBridge extends BridgeAbstract {
+
+class VkBridge extends BridgeAbstract
+{
const MAINTAINER = 'ahiles3005';
const NAME = 'VK.com';
- const URI = 'http://vk.com/';
+ const URI = 'https://vk.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Working with open pages';
const PARAMETERS = array(
@@ -15,42 +17,54 @@ class VkBridge extends BridgeAbstract {
)
);
- public function getURI(){
- if(!is_null($this->getInput('u'))) {
+ protected $pageName;
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('u'))) {
return static::URI . urlencode($this->getInput('u'));
}
return parent::getURI();
}
- public function collectData(){
+ public function getName()
+ {
+ if ($this->pageName) {
+ return $this->pageName;
+ }
- ini_set('user-agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0');
+ return parent::getName();
+ }
- $text_html = getContents($this->getURI())
- or returnServerError('No results for group or user name "' . $this->getInput('u') . '".');
+ public function collectData()
+ {
+ $text_html = $this->getContents()
+ or returnServerError('No results for group or user name "' . $this->getInput('u') . '".');
$text_html = iconv('windows-1251', 'utf-8', $text_html);
$html = str_get_html($text_html);
+ $pageName = $html->find('.page_name', 0)->plaintext;
+ $this->pageName = $pageName;
- foreach($html->find('.post') as $post) {
+ foreach ($html->find('.post') as $post) {
- if(is_object($post->find('a.wall_post_more', 0))) {
+ if (is_object($post->find('a.wall_post_more', 0))) {
//delete link "show full" in content
$post->find('a.wall_post_more', 0)->outertext = '';
}
$item = array();
$item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<br><img>');
- if(is_object($post->find('a.page_media_link_title', 0))) {
- $link = $post->find('a.page_media_link_title', 0)->getAttribute('href');
+ if (is_object($post->find('a.page_media_link_title', 0))) {
+ $link = $post->find('a.page_media_link_title', 0)->getAttribute('href');
//external link in the post
$item['content'] .= "\n\rExternal link: "
- . str_replace('/away.php?to=', '', urldecode($link));
+ . str_replace('/away.php?to=', '', urldecode($link));
}
//get video on post
- if(is_object($post->find('span.post_video_title_content', 0))) {
+ if (is_object($post->find('span.post_video_title_content', 0))) {
$titleVideo = $post->find('span.post_video_title_content', 0)->plaintext;
$linkToVideo = self::URI . $post->find('a.page_post_thumb_video', 0)->getAttribute('href');
$item['content'] .= "\n\r {$titleVideo}: {$linkToVideo}";
@@ -58,9 +72,57 @@ class VkBridge extends BridgeAbstract {
// get post link
$item['uri'] = self::URI . $post->find('a.post_link', 0)->getAttribute('href');
- $item['date'] = $post->find('span.rel_date', 0)->plaintext;
+ $item['timestamp'] = $this->getTime($post);
+ $item['author'] = $pageName;
$this->items[] = $item;
- // var_dump($item['date']);
+
+ }
+ }
+
+ private function getTime($post)
+ {
+ if ($time = $post->find('span.rel_date', 0)->getAttribute('time')) {
+ return $time;
+ } else {
+ $strdate = $post->find('span.rel_date', 0)->plaintext;
+
+ $date = date_parse($strdate);
+ if (!$date['year']) {
+ if (strstr($strdate, 'today') !== false) {
+ $strdate = date('d-m-Y') . ' ' . $strdate;
+ } elseif (strstr($strdate, 'yesterday ') !== false) {
+ $time = time() - 60 * 60 * 24;
+ $strdate = date('d-m-Y', $time) . ' ' . $strdate;
+ } else {
+ $strdate = $strdate . ' ' . date('Y');
+ }
+
+ $date = date_parse($strdate);
+ }
+ return strtotime($date['day'] . '-' . $date['month'] . '-' . $date['year'] . ' ' .
+ $date['hour'] . ':' . $date['minute']);
}
+
+ }
+
+ public function getContents()
+ {
+ ini_set('user-agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0');
+
+ $opts = array(
+ 'http' => array(
+ 'method' => "GET",
+ 'user_agent' => ini_get('user_agent'),
+ 'accept_encoding' => 'gzip',
+ 'header' => "Accept-language: en\r\n
+ Cookie: remixlang=3\r\n"
+ )
+ );
+
+ $context = stream_context_create($opts);
+
+ return getContents($this->getURI(), false, $context);
}
+
+
}
diff --git a/bridges/YoutubeBridge.php b/bridges/YoutubeBridge.php
index dab3252..321fb26 100644
--- a/bridges/YoutubeBridge.php
+++ b/bridges/YoutubeBridge.php
@@ -50,11 +50,31 @@ class YoutubeBridge extends BridgeAbstract {
private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time){
$html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid");
- $author = $html->innertext;
- $author = substr($author, strpos($author, '"author=') + 8);
- $author = substr($author, 0, strpos($author, '\u0026'));
- $desc = $html->find('div#watch-description-text', 0)->innertext;
- $time = strtotime($html->find('meta[itemprop=datePublished]', 0)->getAttribute('content'));
+
+ // Skip unavailable videos
+ if(!strpos($html->innertext, 'IS_UNAVAILABLE_PAGE')) {
+ return;
+ }
+
+ foreach($html->find('script') as $script) {
+ $data = trim($script->innertext);
+
+ if(strpos($data, '{') !== 0)
+ continue; // Wrong script
+
+ $json = json_decode($data);
+
+ if(!isset($json->itemListElement))
+ continue; // Wrong script
+
+ $author = $json->itemListElement[0]->item->name;
+ }
+
+ if(!is_null($html->find('#watch-description-text', 0)))
+ $desc = $html->find('#watch-description-text', 0)->innertext;
+
+ if(!is_null($html->find('meta[itemprop=datePublished]', 0)))
+ $time = strtotime($html->find('meta[itemprop=datePublished]', 0)->getAttribute('content'));
}
private function ytBridgeAddItem($vid, $title, $author, $desc, $time){
@@ -84,9 +104,10 @@ class YoutubeBridge extends BridgeAbstract {
$vid = str_replace('yt:video:', '', $element->find('id', 0)->plaintext);
$time = strtotime($element->find('published', 0)->plaintext);
- $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
+ if(strpos($vid, 'googleads') === false)
+ $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
}
- $this->request = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext);
+ $this->feedName = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext); // feedName will be used by getName()
}
private function ytBridgeParseHtmlListing($html, $element_selector, $title_selector){
@@ -98,8 +119,9 @@ class YoutubeBridge extends BridgeAbstract {
$desc = '';
$time = 0;
$vid = str_replace('/watch?v=', '', $element->find('a', 0)->href);
+ $vid = substr($vid, 0, strpos($vid, '&') ?: strlen($vid));
$title = $this->ytBridgeFixTitle($element->find($title_selector, 0)->plaintext);
- if($title != '[Private Video]') {
+ if($title != '[Private Video]' && strpos($vid, 'googleads') === false) {
$this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
$count++;
@@ -158,7 +180,7 @@ class YoutubeBridge extends BridgeAbstract {
$html = $this->ytGetSimpleHTMLDOM($url_listing)
or returnServerError("Could not request YouTube. Tried:\n - $url_listing");
$this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a');
- $this->request = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext);
+ $this->feedName = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
} elseif($this->getInput('s')) { /* search mode */
$this->request = $this->getInput('s');
$page = 1;
@@ -176,7 +198,7 @@ class YoutubeBridge extends BridgeAbstract {
or returnServerError("Could not request YouTube. Tried:\n - $url_listing");
$this->ytBridgeParseHtmlListing($html, 'div.yt-lockup', 'h3');
- $this->request = 'Search: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext);
+ $this->feedName = 'Search: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
} else { /* no valid mode */
returnClientError("You must either specify either:\n - YouTube
username (?u=...)\n - Channel id (?c=...)\n - Playlist id (?p=...)\n - Search (?s=...)");
@@ -184,6 +206,15 @@ class YoutubeBridge extends BridgeAbstract {
}
public function getName(){
- return (!empty($this->request) ? $this->request . ' - ' : '') . 'YouTube Bridge';
- }
+ // Name depends on queriedContext:
+ switch($this->queriedContext) {
+ case 'By username':
+ case 'By channel id':
+ case 'By playlist Id':
+ case 'Search result':
+ return $this->feedName . ' - YouTube'; // We already know it's a bridge, right?
+ default:
+ return parent::getName();
+ }
+ }
}
diff --git a/index.php b/index.php
index a47dd3e..851f0f9 100644
--- a/index.php
+++ b/index.php
@@ -25,25 +25,31 @@ error_reporting(0);
// Specify directory for cached files (using FileCache)
define('CACHE_DIR', __DIR__ . '/cache');
+// Specify path for whitelist file
+define('WHITELIST_FILE', __DIR__ . '/whitelist.txt');
+
+
+/*
+Move the CLI arguments to the $_GET array, in order to be able to use
+rss-bridge from the command line
+*/
+parse_str(implode('&', array_slice($argv, 1)), $cliArgs);
+$params = array_merge($_GET, $cliArgs);
+
/*
Create a file named 'DEBUG' for enabling debug mode.
- For further security, you may put whitelisted IP addresses
- in the 'DEBUG' file, one IP per line. Empty file allows anyone(!).
- Debugging allows displaying PHP error messages and bypasses the cache: this can allow a malicious
- client to retrieve data about your server and hammer a provider throught your rss-bridge instance.
+ For further security, you may put whitelisted IP addresses in the file,
+ one IP per line. Empty file allows anyone(!).
+ Debugging allows displaying PHP error messages and bypasses the cache: this
+ can allow a malicious client to retrieve data about your server and hammer
+ a provider throught your rss-bridge instance.
*/
if(file_exists('DEBUG')) {
- $debug_enabled = true;
$debug_whitelist = trim(file_get_contents('DEBUG'));
- if(strlen($debug_whitelist) > 0) {
- $debug_enabled = false;
- foreach(explode("\n", $debug_whitelist) as $allowed_ip) {
- if(trim($allowed_ip) === $_SERVER['REMOTE_ADDR']) {
- $debug_enabled = true;
- break;
- }
- }
- }
+
+ $debug_enabled = empty($debug_whitelist)
+ || in_array($_SERVER['REMOTE_ADDR'], explode("\n", $debug_whitelist));
+
if($debug_enabled) {
ini_set('display_errors', '1');
error_reporting(E_ALL);
@@ -64,10 +70,21 @@ if(!extension_loaded('openssl'))
if(!extension_loaded('libxml'))
die('"libxml" extension not loaded. Please check "php.ini"');
+if(!extension_loaded('mbstring'))
+ die('"mbstring" extension not loaded. Please check "php.ini"');
+
// configuration checks
if(ini_get('allow_url_fopen') !== "1")
die('"allow_url_fopen" is not set to "1". Please check "php.ini');
+// Check cache folder permissions (write permissions required)
+if(!is_writable(CACHE_DIR))
+ die('RSS-Bridge does not have write permissions for ' . CACHE_DIR . '!');
+
+// Check whitelist file permissions (only in DEBUG mode)
+if(!file_exists(WHITELIST_FILE) && !is_writable(dirname(WHITELIST_FILE)))
+ die('RSS-Bridge does not have write permissions for ' . WHITELIST_FILE . '!');
+
// FIXME : beta test UA spoofing, please report any blacklisting by PHP-fopen-unfriendly websites
$userAgent = 'Mozilla/5.0(X11; Linux x86_64; rv:30.0)';
@@ -77,24 +94,23 @@ $userAgent .= '+https://github.com/RSS-Bridge/rss-bridge)';
ini_set('user_agent', $userAgent);
// default whitelist
-$whitelist_file = './whitelist.txt';
$whitelist_default = array(
- "BandcampBridge",
- "CryptomeBridge",
- "DansTonChatBridge",
- "DuckDuckGoBridge",
- "FacebookBridge",
- "FlickrExploreBridge",
- "GooglePlusPostBridge",
- "GoogleSearchBridge",
- "IdenticaBridge",
- "InstagramBridge",
- "OpenClassroomsBridge",
- "PinterestBridge",
- "ScmbBridge",
- "TwitterBridge",
- "WikipediaBridge",
- "YoutubeBridge");
+ 'BandcampBridge',
+ 'CryptomeBridge',
+ 'DansTonChatBridge',
+ 'DuckDuckGoBridge',
+ 'FacebookBridge',
+ 'FlickrExploreBridge',
+ 'GooglePlusPostBridge',
+ 'GoogleSearchBridge',
+ 'IdenticaBridge',
+ 'InstagramBridge',
+ 'OpenClassroomsBridge',
+ 'PinterestBridge',
+ 'ScmbBridge',
+ 'TwitterBridge',
+ 'WikipediaBridge',
+ 'YoutubeBridge');
try {
@@ -102,22 +118,25 @@ try {
Format::setDir(__DIR__ . '/formats/');
Cache::setDir(__DIR__ . '/caches/');
- if(!file_exists($whitelist_file)) {
+ if(!file_exists(WHITELIST_FILE)) {
$whitelist_selection = $whitelist_default;
$whitelist_write = implode("\n", $whitelist_default);
- file_put_contents($whitelist_file, $whitelist_write);
+ file_put_contents(WHITELIST_FILE, $whitelist_write);
} else {
- $whitelist_file_content = file_get_contents($whitelist_file);
+ $whitelist_file_content = file_get_contents(WHITELIST_FILE);
if($whitelist_file_content != "*\n") {
$whitelist_selection = explode("\n", $whitelist_file_content);
} else {
$whitelist_selection = Bridge::listBridges();
}
+
+ // Prepare for case-insensitive match
+ $whitelist_selection = array_map('strtolower', $whitelist_selection);
}
- $action = filter_input(INPUT_GET, 'action');
- $bridge = filter_input(INPUT_GET, 'bridge');
+ $action = array_key_exists('action', $params) ? $params['action'] : null;
+ $bridge = array_key_exists('bridge', $params) ? $params['bridge'] : null;
if($action === 'display' && !empty($bridge)) {
// DEPRECATED: 'nameBridge' scheme is replaced by 'name' in bridge parameter values
@@ -126,7 +145,8 @@ try {
$bridge = substr($bridge, 0, $pos);
}
- $format = filter_input(INPUT_GET, 'format');
+ $format = $params['format']
+ or returnClientError('You must specify a format!');
// DEPRECATED: 'nameFormat' scheme is replaced by 'name' in format parameter values
// this is to keep compatibility until futher complete removal
@@ -135,7 +155,7 @@ try {
}
// whitelist control
- if(!Bridge::isWhitelisted($whitelist_selection, $bridge)) {
+ if(!Bridge::isWhitelisted($whitelist_selection, strtolower($bridge))) {
throw new \HttpException('This bridge is not whitelisted', 401);
die;
}
@@ -143,13 +163,11 @@ try {
// Data retrieval
$bridge = Bridge::create($bridge);
- $noproxy = filter_input(INPUT_GET, '_noproxy', FILTER_VALIDATE_BOOLEAN);
+ $noproxy = array_key_exists('_noproxy', $params) && filter_var($params['_noproxy'], FILTER_VALIDATE_BOOLEAN);
if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
define('NOPROXY', true);
}
- $params = $_GET;
-
// Initialize cache
$cache = Cache::create('FileCache');
$cache->setPath(CACHE_DIR);
@@ -166,7 +184,7 @@ try {
$bridge->setCache($cache);
$bridge->setDatas($params);
} catch(Exception $e) {
- header('HTTP/1.1 ' . $e->getCode() . ' ' . Http::getMessageForCode($e->getCode()));
+ http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildBridgeException($e, $bridge));
}
@@ -178,7 +196,7 @@ try {
$format->setExtraInfos($bridge->getExtraInfos());
$format->display();
} catch(Exception $e) {
- header('HTTP/1.1 ' . $e->getCode() . ' ' . Http::getMessageForCode($e->getCode()));
+ http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildTransformException($e, $bridge));
}
@@ -186,7 +204,7 @@ try {
die;
}
} catch(HttpException $e) {
- header('HTTP/1.1 ' . $e->getCode() . ' ' . Http::getMessageForCode($e->getCode()));
+ http_response_code($e->getCode());
header('Content-Type: text/plain');
die($e->getMessage());
} catch(\Exception $e) {
@@ -205,6 +223,7 @@ $formats = Format::searchInformation();
<title>RSS-Bridge</title>
<link href="static/style.css" rel="stylesheet">
<script src="static/search.js"></script>
+ <script src="static/select.js"></script>
<noscript>
<style>
.searchbar {
@@ -221,6 +240,8 @@ $formats = Format::searchInformation();
$status .= 'debug mode active';
}
+ $query = filter_input(INPUT_GET, 'q');
+
echo <<<EOD
<header>
<h1>RSS-Bridge</h1>
@@ -231,7 +252,7 @@ $formats = Format::searchInformation();
<h3>Search</h3>
<input type="text" name="searchfield"
id="searchfield" placeholder="Enter the bridge you want to search for"
- onchange="search()" onkeyup="search()">
+ onchange="search()" onkeyup="search()" value="{$query}">
</section>
EOD;
@@ -241,7 +262,7 @@ EOD;
$inactiveBridges = '';
$bridgeList = Bridge::listBridges();
foreach($bridgeList as $bridgeName) {
- if(Bridge::isWhitelisted($whitelist_selection, $bridgeName)) {
+ if(Bridge::isWhitelisted($whitelist_selection, strtolower($bridgeName))) {
echo displayBridgeCard($bridgeName, $formats);
$activeFoundBridgeCount++;
} elseif($showInactive) {
@@ -252,7 +273,7 @@ EOD;
echo $inactiveBridges;
?>
<section class="footer">
- <a href="https://github.com/RSS-Bridge/rss-bridge">RSS-Bridge 2017-08-03 ~ Public Domain</a><br />
+ <a href="https://github.com/RSS-Bridge/rss-bridge">RSS-Bridge 2017-08-19 ~ Public Domain</a><br />
<?= $activeFoundBridgeCount; ?>/<?= count($bridgeList) ?> active bridges. <br />
<?php
if($activeFoundBridgeCount !== count($bridgeList)) {
diff --git a/lib/Bridge.php b/lib/Bridge.php
index d0a127e..a2ae927 100644
--- a/lib/Bridge.php
+++ b/lib/Bridge.php
@@ -9,16 +9,6 @@ class Bridge {
}
/**
- * Checks if a bridge is an instantiable bridge.
- * @param string $nameBridge name of the bridge that you want to use
- * @return true if it is an instantiable bridge, false otherwise.
- */
- static public function isInstantiable($nameBridge){
- $re = new ReflectionClass($nameBridge);
- return $re->IsInstantiable();
- }
-
- /**
* Create a new bridge object
* @param string $nameBridge Defined bridge name you want use
* @return Bridge object dedicated
@@ -42,11 +32,11 @@ EOD;
require_once $pathBridge;
- if(Bridge::isInstantiable($nameBridge)) {
+ if((new ReflectionClass($nameBridge))->isInstantiable()) {
return new $nameBridge();
- } else {
- return false;
}
+
+ return false;
}
static public function setDir($dirBridge){
@@ -62,13 +52,11 @@ EOD;
}
static public function getDir(){
- $dirBridge = self::$dirBridge;
-
- if(is_null($dirBridge)) {
+ if(is_null(self::$dirBridge)) {
throw new \LogicException(__CLASS__ . ' class need to know bridge path !');
}
- return $dirBridge;
+ return self::$dirBridge;
}
/**
@@ -76,9 +64,8 @@ EOD;
* @return array List of the bridges
*/
static public function listBridges(){
- $pathDirBridge = self::getDir();
$listBridge = array();
- $dirFiles = scandir($pathDirBridge);
+ $dirFiles = scandir(self::getDir());
if($dirFiles !== false) {
foreach($dirFiles as $fileName) {
@@ -92,14 +79,10 @@ EOD;
}
static public function isWhitelisted($whitelist, $name){
- if(in_array($name, $whitelist)
+ return in_array($name, $whitelist)
|| in_array($name . '.php', $whitelist)
- || in_array($name . 'Bridge', $whitelist) // DEPRECATED
- || in_array($name . 'Bridge.php', $whitelist) // DEPRECATED
- || (count($whitelist) === 1 && trim($whitelist[0]) === '*')) {
- return true;
- } else {
- return false;
- }
+ || in_array($name . 'bridge', $whitelist) // DEPRECATED
+ || in_array($name . 'bridge.php', $whitelist) // DEPRECATED
+ || (count($whitelist) === 1 && trim($whitelist[0]) === '*');
}
}
diff --git a/lib/Exceptions.php b/lib/Exceptions.php
index ff8ec86..252d1e3 100644
--- a/lib/Exceptions.php
+++ b/lib/Exceptions.php
@@ -2,64 +2,6 @@
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',
- );
- }
-}
-
-/**
* Returns an URL that automatically populates a new issue on GitHub based
* on the information provided
*
diff --git a/lib/FeedExpander.php b/lib/FeedExpander.php
index ff20949..9702ce3 100644
--- a/lib/FeedExpander.php
+++ b/lib/FeedExpander.php
@@ -18,7 +18,7 @@ abstract class FeedExpander extends BridgeAbstract {
*/
$content = getContents($url)
or returnServerError('Could not request ' . $url);
- $rssContent = simplexml_load_string($content);
+ $rssContent = simplexml_load_string(trim($content));
debugMessage('Detecting feed format/version');
switch(true) {
@@ -102,12 +102,12 @@ abstract class FeedExpander extends BridgeAbstract {
if(!isset($content->link)) {
$this->uri = '';
} elseif (count($content->link) === 1) {
- $this->uri = $content->link[0]['href'];
+ $this->uri = (string)$content->link[0]['href'];
} else {
$this->uri = '';
foreach($content->link as $link) {
if(strtolower($link['rel']) === 'alternate') {
- $this->uri = $link['href'];
+ $this->uri = (string)$link['href'];
break;
}
}
diff --git a/lib/validation.php b/lib/validation.php
index 7bcbbf5..fdcb51c 100644
--- a/lib/validation.php
+++ b/lib/validation.php
@@ -21,19 +21,14 @@ function validateData(&$data, $parameters){
$validateNumberValue = function($value){
$filteredValue = filter_var($value, FILTER_VALIDATE_INT);
- if($filteredValue === false && !empty($value))
+ if($filteredValue === false)
return null;
return $filteredValue;
};
$validateCheckboxValue = function($value){
- $filteredValue = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
-
- if(is_null($filteredValue))
- return null;
-
- return $filteredValue;
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
};
$validateListValue = function($value, $expectedValues){
@@ -85,7 +80,7 @@ function validateData(&$data, $parameters){
break;
}
- if(is_null($data[$name])) {
+ if(is_null($data[$name]) && isset($set[$name]['required']) && $set[$name]['required']) {
echo 'Parameter \'' . $name . '\' is invalid!' . PHP_EOL;
return false;
}
diff --git a/static/select.js b/static/select.js
new file mode 100644
index 0000000..792b92d
--- /dev/null
+++ b/static/select.js
@@ -0,0 +1,10 @@
+function select(){
+ var fragment = window.location.hash.substr(1);
+ var bridge = document.getElementById(fragment);
+
+ if(bridge !== null) {
+ bridge.getElementsByClassName('showmore-box')[0].checked = true;
+ }
+}
+
+document.addEventListener('DOMContentLoaded', select); \ No newline at end of file
diff --git a/static/style.css b/static/style.css
index d477588..c7a278d 100644
--- a/static/style.css
+++ b/static/style.css
@@ -52,6 +52,18 @@ header > p.status {
color: red;
}
+input[type="text"] {
+
+ background-color: white;
+ color: #404552;
+ border: 0px;
+ border-bottom: 2px solid #2196F3;
+ font-size: 1.1em;
+ margin-left: 8px;
+ padding-left: 4px;
+
+}
+
.searchbar {
width: 50%;
@@ -64,6 +76,7 @@ header > p.status {
width: 100%;
margin: auto;
font-size: 1.4em;
+ text-align: center;
}
@@ -73,6 +86,30 @@ header > p.status {
}
+.searchbar input[type="text"]:focus::-webkit-input-placeholder {
+
+ opacity: 0;
+
+}
+
+.searchbar input[type="text"]:focus::-moz-placeholder {
+
+ opacity: 0;
+
+}
+
+.searchbar input[type="text"]:focus:-moz-placeholder {
+
+ opacity: 0;
+
+}
+
+.searchbar input[type="text"]:focus:-ms-input-placeholder {
+
+ opacity: 0;
+
+}
+
.searchbar > h3 {
font-size: 150%;
@@ -188,18 +225,6 @@ form {
}
-input[type="text"] {
-
- background-color: white;
- color: #404552;
- border: 0px;
- border-bottom: 2px solid #2196F3;
- font-size: 1.1em;
- margin-left: 8px;
- padding-left: 4px;
-
-}
-
form {
display: none;