diff options
author | Johannes Schauer <josch@debian.org> | 2018-03-27 15:05:30 +0200 |
---|---|---|
committer | Johannes Schauer <josch@debian.org> | 2018-03-27 15:05:30 +0200 |
commit | 96ca5535d146ed4a14d1ca66495e7ce1d38cf35b (patch) | |
tree | 0b0c8dc557b7e8bc45c4cc0a204cec86d5f22ddc | |
parent | 397a196e4b0b351fb7fda6581a927d1afa02f413 (diff) |
Import upstream version 2018-03-11
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 & 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 === @@ -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'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> ' + . $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(); + } + } } @@ -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; |