summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohannes 'josch' Schauer <josch@debian.org>2018-07-20 18:46:30 +0200
committerJohannes 'josch' Schauer <josch@debian.org>2018-07-20 18:46:30 +0200
commit438ebcdc58e6cb4a6490d0d84803c8eb9aad5627 (patch)
tree043a33a0b1ca1c035265190b9ef32334ebc039a7
Import rss-bridge_2018-07-17.orig.tar.gz
[dgit import orig rss-bridge_2018-07-17.orig.tar.gz]
-rw-r--r--.dockerignore8
-rw-r--r--.gitattributes22
-rw-r--r--.gitignore238
-rw-r--r--.travis.yml24
-rw-r--r--CHANGELOG.md263
-rw-r--r--CONTRIBUTING.md47
-rw-r--r--Dockerfile5
-rw-r--r--README.md142
-rw-r--r--UNLICENSE25
-rw-r--r--bridges/ABCTabsBridge.php42
-rw-r--r--bridges/AcrimedBridge.php25
-rw-r--r--bridges/AllocineFRBridge.php87
-rw-r--r--bridges/AmazonBridge.php94
-rw-r--r--bridges/AmazonPriceTrackerBridge.php149
-rw-r--r--bridges/AnimeUltimeBridge.php135
-rw-r--r--bridges/Arte7Bridge.php100
-rw-r--r--bridges/AskfmBridge.php74
-rw-r--r--bridges/BandcampBridge.php63
-rw-r--r--bridges/BastaBridge.php34
-rw-r--r--bridges/BlaguesDeMerdeBridge.php31
-rw-r--r--bridges/BloombergBridge.php65
-rw-r--r--bridges/BooruprojectBridge.php45
-rw-r--r--bridges/CADBridge.php45
-rw-r--r--bridges/CNETBridge.php93
-rw-r--r--bridges/CastorusBridge.php118
-rw-r--r--bridges/ChristianDailyReporterBridge.php25
-rw-r--r--bridges/CollegeDeFranceBridge.php84
-rw-r--r--bridges/CommonDreamsBridge.php26
-rw-r--r--bridges/ContainerLinuxReleasesBridge.php93
-rw-r--r--bridges/CopieDoubleBridge.php35
-rw-r--r--bridges/CourrierInternationalBridge.php55
-rw-r--r--bridges/CpasbienBridge.php74
-rw-r--r--bridges/CryptomeBridge.php45
-rw-r--r--bridges/DailymotionBridge.php123
-rw-r--r--bridges/DanbooruBridge.php67
-rw-r--r--bridges/DansTonChatBridge.php28
-rw-r--r--bridges/DauphineLibereBridge.php56
-rw-r--r--bridges/DealabsBridge.php589
-rw-r--r--bridges/DemoBridge.php46
-rw-r--r--bridges/DemonoidBridge.php166
-rw-r--r--bridges/DeveloppezDotComBridge.php47
-rw-r--r--bridges/DiceBridge.php120
-rw-r--r--bridges/DilbertBridge.php36
-rw-r--r--bridges/DiscogsBridge.php112
-rw-r--r--bridges/DollbooruBridge.php9
-rw-r--r--bridges/DribbbleBridge.php91
-rw-r--r--bridges/DuckDuckGoBridge.php42
-rw-r--r--bridges/ETTVBridge.php142
-rw-r--r--bridges/EZTVBridge.php67
-rw-r--r--bridges/EliteDangerousGalnetBridge.php35
-rw-r--r--bridges/ElloBridge.php146
-rw-r--r--bridges/ElsevierBridge.php75
-rw-r--r--bridges/EstCeQuonMetEnProdBridge.php37
-rw-r--r--bridges/EtsyBridge.php83
-rw-r--r--bridges/FB2Bridge.php281
-rw-r--r--bridges/FDroidBridge.php54
-rw-r--r--bridges/FacebookBridge.php575
-rw-r--r--bridges/FeedExpanderExampleBridge.php62
-rw-r--r--bridges/FierPandaBridge.php24
-rw-r--r--bridges/FilterBridge.php77
-rw-r--r--bridges/FlickrBridge.php120
-rw-r--r--bridges/FootitoBridge.php75
-rw-r--r--bridges/FourchanBridge.php78
-rw-r--r--bridges/FuturaSciencesBridge.php173
-rw-r--r--bridges/GBAtempBridge.php157
-rw-r--r--bridges/GelbooruBridge.php35
-rw-r--r--bridges/GiphyBridge.php76
-rw-r--r--bridges/GithubIssueBridge.php192
-rw-r--r--bridges/GithubSearchBridge.php50
-rw-r--r--bridges/GizmodoBridge.php36
-rw-r--r--bridges/GoComicsBridge.php61
-rw-r--r--bridges/GooglePlusPostBridge.php111
-rw-r--r--bridges/GoogleSearchBridge.php64
-rw-r--r--bridges/GrandComicsDatabaseBridge.php61
-rw-r--r--bridges/HDWallpapersBridge.php83
-rw-r--r--bridges/HentaiHavenBridge.php37
-rw-r--r--bridges/HotUKDealsBridge.php1397
-rw-r--r--bridges/IPBBridge.php310
-rw-r--r--bridges/IdenticaBridge.php52
-rw-r--r--bridges/InstagramBridge.php149
-rw-r--r--bridges/JapanExpoBridge.php100
-rw-r--r--bridges/JustETFBridge.php353
-rw-r--r--bridges/KATBridge.php123
-rw-r--r--bridges/KernelBugTrackerBridge.php150
-rw-r--r--bridges/KonachanBridge.php11
-rw-r--r--bridges/KoreusBridge.php22
-rw-r--r--bridges/KununuBridge.php249
-rw-r--r--bridges/LWNprevBridge.php265
-rw-r--r--bridges/LeBonCoinBridge.php222
-rw-r--r--bridges/LeMondeInformatiqueBridge.php44
-rw-r--r--bridges/LegifranceJOBridge.php68
-rw-r--r--bridges/LesJoiesDuCodeBridge.php45
-rw-r--r--bridges/LichessBridge.php31
-rw-r--r--bridges/LinkedInCompanyBridge.php37
-rw-r--r--bridges/LolibooruBridge.php11
-rw-r--r--bridges/MangareaderBridge.php249
-rw-r--r--bridges/MilbooruBridge.php11
-rw-r--r--bridges/MixCloudBridge.php53
-rw-r--r--bridges/MoebooruBridge.php56
-rw-r--r--bridges/MoinMoinBridge.php327
-rw-r--r--bridges/MondeDiploBridge.php26
-rw-r--r--bridges/MsnMondeBridge.php35
-rw-r--r--bridges/MspabooruBridge.php12
-rw-r--r--bridges/MydealsBridge.php144
-rw-r--r--bridges/NasaApodBridge.php44
-rw-r--r--bridges/NeuviemeArtBridge.php57
-rw-r--r--bridges/NextInpactBridge.php34
-rw-r--r--bridges/NextgovBridge.php74
-rw-r--r--bridges/NiceMatinBridge.php32
-rw-r--r--bridges/NotAlwaysBridge.php57
-rw-r--r--bridges/NovelUpdatesBridge.php69
-rw-r--r--bridges/OpenClassroomsBridge.php49
-rw-r--r--bridges/ParuVenduImmoBridge.php102
-rw-r--r--bridges/PcGamerBridge.php23
-rw-r--r--bridges/PickyWallpapersBridge.php101
-rw-r--r--bridges/PinterestBridge.php121
-rw-r--r--bridges/PixivBridge.php73
-rw-r--r--bridges/RTBFBridge.php66
-rw-r--r--bridges/RadioMelodieBridge.php30
-rw-r--r--bridges/RainbowSixSiegeBridge.php36
-rw-r--r--bridges/ReadComicsBridge.php44
-rw-r--r--bridges/Releases3DSBridge.php136
-rw-r--r--bridges/ReporterreBridge.php47
-rw-r--r--bridges/Rue89Bridge.php25
-rw-r--r--bridges/Rule34Bridge.php12
-rw-r--r--bridges/Rule34pahealBridge.php10
-rw-r--r--bridges/SafebooruBridge.php12
-rw-r--r--bridges/SakugabooruBridge.php11
-rw-r--r--bridges/ScmbBridge.php39
-rw-r--r--bridges/ScoopItBridge.php42
-rw-r--r--bridges/SensCritiqueBridge.php97
-rw-r--r--bridges/SexactuBridge.php88
-rw-r--r--bridges/ShanaprojectBridge.php123
-rw-r--r--bridges/Shimmie2Bridge.php39
-rw-r--r--bridges/SoundcloudBridge.php64
-rw-r--r--bridges/SteamBridge.php157
-rw-r--r--bridges/StripeAPIChangeLogBridge.php23
-rw-r--r--bridges/SupInfoBridge.php57
-rw-r--r--bridges/SuperSmashBlogBridge.php45
-rw-r--r--bridges/SuperbWallpapersBridge.php70
-rw-r--r--bridges/TagBoardBridge.php49
-rw-r--r--bridges/TbibBridge.php12
-rw-r--r--bridges/TebeoBridge.php38
-rw-r--r--bridges/TheCodingLoveBridge.php46
-rw-r--r--bridges/TheHackerNewsBridge.php80
-rw-r--r--bridges/ThePirateBayBridge.php174
-rw-r--r--bridges/TheTVDBBridge.php205
-rw-r--r--bridges/Torrent9Bridge.php102
-rw-r--r--bridges/TwitterBridge.php327
-rw-r--r--bridges/UnsplashBridge.php77
-rw-r--r--bridges/UsbekEtRicaBridge.php110
-rw-r--r--bridges/ViadeoCompanyBridge.php37
-rw-r--r--bridges/VkBridge.php339
-rw-r--r--bridges/WallpaperStopBridge.php107
-rw-r--r--bridges/WeLiveSecurityBridge.php45
-rw-r--r--bridges/WebfailBridge.php149
-rw-r--r--bridges/WhydBridge.php56
-rw-r--r--bridges/WikiLeaksBridge.php129
-rw-r--r--bridges/WikipediaBridge.php304
-rw-r--r--bridges/WordPressBridge.php76
-rw-r--r--bridges/WordPressPluginUpdateBridge.php87
-rw-r--r--bridges/WorldOfTanksBridge.php52
-rw-r--r--bridges/XbooruBridge.php12
-rw-r--r--bridges/YGGTorrentBridge.php143
-rw-r--r--bridges/YandereBridge.php11
-rw-r--r--bridges/YoutubeBridge.php230
-rw-r--r--bridges/ZDNetBridge.php302
-rw-r--r--bridges/ZenodoBridge.php55
-rw-r--r--cache/.gitkeep0
-rw-r--r--caches/FileCache.php121
-rw-r--r--config.default.ini.php44
-rw-r--r--formats/AtomFormat.php107
-rw-r--r--formats/HtmlFormat.php115
-rw-r--r--formats/JsonFormat.php25
-rw-r--r--formats/MrssFormat.php118
-rw-r--r--formats/PlaintextFormat.php25
-rw-r--r--index.php270
-rw-r--r--lib/Authentication.php31
-rw-r--r--lib/Bridge.php88
-rw-r--r--lib/BridgeAbstract.php285
-rw-r--r--lib/BridgeInterface.php87
-rw-r--r--lib/Cache.php53
-rw-r--r--lib/CacheInterface.php7
-rw-r--r--lib/Configuration.php120
-rw-r--r--lib/Exceptions.php154
-rw-r--r--lib/FeedExpander.php208
-rw-r--r--lib/Format.php73
-rw-r--r--lib/FormatAbstract.php106
-rw-r--r--lib/FormatInterface.php11
-rw-r--r--lib/RssBridge.php55
-rw-r--r--lib/contents.php100
-rw-r--r--lib/error.php28
-rw-r--r--lib/html.php360
-rw-r--r--lib/validation.php95
-rw-r--r--phpcs.xml77
-rw-r--r--scalingo.json6
-rw-r--r--static/HtmlFormat.css119
-rw-r--r--static/search.js27
-rw-r--r--static/select.js10
-rw-r--r--static/style.css310
-rw-r--r--vendor/simplehtmldom/simple_html_dom.php1742
201 files changed, 22002 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..15154f0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,8 @@
+.git
+cache/*
+DEBUG
+Dockerfile
+whitelist.txt
+phpcs.xml
+CHANGELOG.md
+CONTRIBUTING.md \ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..412eeda
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,22 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+# Custom for Visual Studio
+*.cs diff=csharp
+*.sln merge=union
+*.csproj merge=union
+*.vbproj merge=union
+*.fsproj merge=union
+*.dbproj merge=union
+
+# Standard to msysgit
+*.doc diff=astextplain
+*.DOC diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot diff=astextplain
+*.DOT diff=astextplain
+*.pdf diff=astextplain
+*.PDF diff=astextplain
+*.rtf diff=astextplain
+*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a848b5f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,238 @@
+#################
+## Eclipse
+#################
+vendor/*
+data/
+*.pydevproject
+.project
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.classpath
+.settings/
+.loadpath
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# CDT-specific
+.cproject
+
+# PDT-specific
+.buildpath
+
+
+#################
+## Visual Studio
+#################
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+
+[Dd]ebug/
+[Rr]elease/
+x64/
+build/
+[Bb]in/
+[Oo]bj/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+*_i.c
+*_p.c
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.log
+*.scc
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+*.ncrunch*
+.*crunch*.local.xml
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.Publish.xml
+*.pubxml
+
+# NuGet Packages Directory
+## TODO: If you have NuGet Package Restore enabled, uncomment the next line
+#packages/
+
+# Windows Azure Build Output
+csx
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+sql/
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.[Pp]ublish.xml
+*.pfx
+*.publishsettings
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file to a newer
+# Visual Studio version. Backup files are not needed, because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+App_Data/*.mdf
+App_Data/*.ldf
+
+#################
+## Other ide stuff
+#################
+.idea/*
+[#]*[#]
+
+#############
+## Windows detritus
+#############
+
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Mac crap
+.DS_Store
+
+
+#############
+## Python
+#############
+
+*.py[co]
+
+# Packages
+*.egg
+*.egg-info
+dist/
+build/
+eggs/
+parts/
+var/
+sdist/
+develop-eggs/
+.installed.cfg
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+
+#Translations
+*.mo
+
+#Mr Developer
+.mr.developer.cfg
+
+##############
+## RSS-Bridge
+##############
+/cache
+/whitelist.txt
+DEBUG
+config.ini.php
+
+######################
+## VisualStudioCode ##
+######################
+.vscode/*
+
+#Builder
+.buildconfig
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..cd5e2d9
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,24 @@
+dist: trusty
+sudo: false
+language: php
+
+install:
+ - pear channel-update pear.php.net
+ - pear install PHP_CodeSniffer
+
+script:
+ - phpenv rehash
+ - phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p
+
+matrix:
+ fast_finish: true
+
+ include:
+ - php: 5.6
+ - php: 7.0
+ - php: hhvm
+ - php: nightly
+
+ allow_failures:
+ - php: hhvm
+ - php: nightly
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..467040e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,263 @@
+rss-bridge Changelog
+===
+
+RSS-Bridge 2017-08-19
+==
+
+## General changes
+* whitelist: Do case-insensitive whitelist matching
+* [FeedExpander] Fix Serialization of 'SimpleXMLElement' is not allowed
+* [FeedExpander] Remove whitespace from source content
+* [index] Add GET parameter 'q' for search queries
+ - **Example**: You can now add `&q=Twitter` to load into the search field
+* [index] Check permissions for cache folder and whitelist file
+* [index] Show bridge options when loading with URL fragment
+ - **Example**: You can now add `#bridge-Twitter` to load the card with all
+parameters visible
+* [style] Center search cursor and hide placeholder
+* [validation] Fix error on undefined optional numeric value
+
+## Modified bridges
+* [DanbooruBridge] Allow descendant classes to override tag collection
+* [DribbbleBridge] Add dribble bridge listing last dribble popular shots (#558)
+* [FacebookBridge] Fix &amp; in URLs
+* [GelbooruBridge] Fix bridge not getting tags correctly
+* [GoComicsBridge] Fix for page structure changes (#568)
+* [LeBonCoinBridge] Fix bridge is marked executable
+* [LWNprevBridge] Fix everchanging url
+* [YoutubeBridge] Fix error on certain keywords
+* [YoutubeBridge] Fix issues loading playlists
+
+## Removed bridges
+* VineBridge
+
+RSS-Bridge 2017-08-03
+==
+
+## Important changes
+* RSS-Bridge now has [contribution guidelines](CONTRIBUTING.md)
+* [phpcs rules](phpcs.xml) follow the [contribution guidelines](CONTRIBUTING.md)
+
+## 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
+
+## 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
+
+## New bridges
+* AmazonBridge
+* DiceBridge
+* EtsyBridge
+* FB2Bridge
+* FilterBridge
+* FlickrBridge
+* GithubSearchBridge
+* GoComicsBridge
+* KATBridge
+* KernelBugTrackerBridge
+* MixCloudBridge
+* MoinMoinBridge
+* RainbowSixSiegeBridge
+* SteamBridge
+* TheTVDBBridge
+* Torrent9Bridge
+* UsbekEtRicaBridge
+* WikiLeaksBridge
+* WordPressPluginUpdateBridge
+
+Alpha 0.2
+===
+
+## Important changes
+* RSS-Bridge has been [UNLICENSED](UNLICENSE)
+* RSS-Bridge is now a community-managed project on [GitHub](https://github.com/rss-bridge/rss-bridge)
+* RSS-Bridge now has a [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
+* RSS-Bridge now supports [Travis-CI](https://travis-ci.org)
+
+## General changes
+* Added [CHANGELOG](CHANGELOG.md) (this file)
+* Added [PHP Simple HTML DOM Parser](http://simplehtmldom.sourceforge.net) to [vendor](vendor/simplehtmldom/)
+* Added cache purging function (cache will be force-purged after 24 hours or as defined by bridge)
+* Added new format [MrssFormat](formats/MrssFormat.php)
+* Added parameter `author` - for display of the feed author name - to all formats
+* Added new abstraction of the BridgeInterface:
+ - [FeedExpander](https://github.com/RSS-Bridge/rss-bridge/wiki/Bridge-API)
+* Added optional support for proxy usage on each individual bridge
+* Added support for [custom bridge parameter](https://github.com/RSS-Bridge/rss-bridge/wiki/BridgeAbstract#format-specifications) (text, number, list, checkbox)
+* Changed design of the welcome screen
+* Changed design of HtmlFormat
+* Changed behavior of debug mode:
+ - Enable debug mode by placing a file called "DEBUG" in the root folder
+ - Debug mode automatically disables cache file loading
+* Changed implementation of bridges - see [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
+ - Changed comment-style metadata to constants
+ - Added support for multiple utilizations per bridge
+ - Changed the parameter loading algorithm to be loaded by RSS-Bridge core
+* Improved checks for PHP version, configuration and extensions
+* Many bug fixes
+
+## Modified Bridges
+* FlickrExploreBridge
+* GoogleSearchBridge
+* TwitterBridge
+
+## New Bridges
+* ABCTabsBridge
+* AcrimedBridge
+* AllocineFRBridge
+* AnimeUltimeBridge
+* Arte7Bridge
+* AskfmBridge
+* BandcampBridge
+* BastaBridge
+* BlaguesDeMerdeBridge
+* BooruprojectBridge
+* CADBridge
+* CNETBridge
+* CastorusBridge
+* CollegeDeFranceBridge
+* CommonDreamsBridge
+* CopieDoubleBridge
+* CourrierInternationalBridge
+* CpasbienBridge
+* CryptomeBridge
+* DailymotionBridge
+* DanbooruBridge
+* DansTonChatBridge
+* DauphineLibereBridge
+* DemoBridge
+* DeveloppezDotComBridge
+* DilbertBridge
+* DollbooruBridge
+* DuckDuckGoBridge
+* EZTVBridge
+* EliteDangerousGalnetBridge
+* ElsevierBridge
+* EstCeQuonMetEnProdBridge
+* FacebookBridge
+* FierPandaBridge
+* FlickrTagBridge
+* FootitoBridge
+* FourchanBridge
+* FuturaSciencesBridge
+* GBAtempBridge
+* GelbooruBridge
+* GiphyBridge
+* GithubIssueBridge
+* GizmodoBridge
+* GooglePlusPostBridge
+* HDWallpapersBridge
+* HentaiHavenBridge
+* IdenticaBridge
+* InstagramBridge
+* IsoHuntBridge
+* JapanExpoBridge
+* KonachanBridge
+* KoreusBridge
+* KununuBridge
+* LWNprevBridge
+* LeBonCoinBridge
+* LegifranceJOBridge
+* LeMondeInformatiqueBridge
+* LesJoiesDuCodeBridge
+* LichessBridge
+* LinkedInCompanyBridge
+* LolibooruBridge
+* MangareaderBridge
+* MilbooruBridge
+* MoebooruBridge
+* MondeDiploBridge
+* MsnMondeBridge
+* MspabooruBridge
+* NasaApodBridge
+* NeuviemeArtBridge
+* NextInpactBridge
+* NextgovBridge
+* NiceMatinBridge
+* NovelUpdatesBridge
+* OpenClassroomsBridge
+* ParuVenduImmoBridge
+* PickyWallpapersBridge
+* PinterestBridge
+* PlanetLibreBridge
+* RTBFBridge
+* ReadComicsBridge
+* Releases3DSBridge
+* ReporterreBridge
+* Rue89Bridge
+* Rule34Bridge
+* Rule34pahealBridge
+* SafebooruBridge
+* SakugabooruBridge
+* ScmbBridge
+* ScoopItBridge
+* SensCritiqueBridge
+* SexactuBridge
+* ShanaprojectBridge
+* Shimmie2Bridge
+* SoundcloudBridge
+* StripeAPIChangeLogBridge
+* SuperbWallpapersBridge
+* T411Bridge
+* TagBoardBridge
+* TbibBridge
+* TheCodingLoveBridge
+* TheHackerNewsBridge
+* ThePirateBayBridge
+* UnsplashBridge
+* ViadeoCompanyBridge
+* VineBridge
+* VkBridge
+* WallpaperStopBridge
+* WebfailBridge
+* WeLiveSecurityBridge
+* WhydBridge
+* WikipediaBridge
+* WordPressBridge
+* WorldOfTanksBridge
+* XbooruBridge
+* YandereBridge
+* YoutubeBridge
+* ZDNetBridge
+
+Alpha 0.1
+===
+* First tagged version.
+* Includes refactoring.
+* Unstable. \ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..e03f926
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,47 @@
+### Pull request policy
+Fix one issue per pull request.
+Squash commits before opening a pull request.
+Respect the coding style policy.
+Name your PR like the following :
+
+* When correcting a single bridge, use `[BridgeName] Feature`.
+* When fixing a problem in a specific file, use `[FileName] Feature`.
+* When fixing a general problem, use `category : feature`.
+
+Note that all pull-requests should pass the unit tests before they can be merged.
+
+### Coding style
+
+Use `camelCase` for variables and methods.
+Use `UPPERCASE` for constants.
+Use `PascalCase` for class names. When creating a bridge, your class and PHP file should be named `MyImplementationBridge`.
+Use tabs for indentation.
+Add an empty line at the end of your file.
+
+Use `''` to encapsulate strings, including in arrays.
+Prefer lines shorter than 80 chars, no line longer than 120 chars.
+PHP constants should be in lower case (`true, false, null`...)
+
+
+* Add spaces between the logical operator and your expressions (not needed for the `!` operator).
+* Use `||` and `&&` instead of `or` and `and`.
+* Add space between your condition and the opening bracket/closing bracket.
+* Don't put a space between `if` and your bracket.
+* Use `elseif` instead of `else if`.
+* Add new lines in your conditions if they are containing more than one line.
+* Example :
+
+```PHP
+if($a == true && $b) {
+ print($a);
+} else if(!$b) {
+
+ $a = !$a;
+ $b = $b >> $a;
+ print($b);
+
+} else {
+ print($b);
+}
+```
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..35caac8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,5 @@
+FROM ulsmith/alpine-apache-php7
+
+COPY ./ /app/public/
+
+RUN chown -R apache:root /app/public \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3ca495c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,142 @@
+rss-bridge
+===
+[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge)
+
+rss-bridge is a PHP project capable of generating ATOM feeds for websites which don't have one.
+
+Supported sites/pages (main)
+===
+
+* `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
+
+Output format
+===
+Output format can take several forms:
+
+* `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
+===
+
+Welcome screen:
+
+![Screenshot](https://github.com/RSS-Bridge/rss-bridge/wiki/images/screenshot_rss-bridge_welcome.png)
+
+RSS-Bridge hashtag (#rss-bridge) search on Twitter, in ATOM format (as displayed by Firefox):
+
+![Screenshot](https://github.com/RSS-Bridge/rss-bridge/wiki/images/screenshot_twitterbridge_atom.png)
+
+Requirements
+===
+
+ * PHP 5.6, e.g. `AddHandler application/x-httpd-php56 .php` in `.htaccess`
+ * `openssl` extension enabled in PHP config (`php.ini`)
+ * `curl` extension enabled in PHP config (`php.ini`)
+
+Enabling/Disabling bridges
+===
+
+By default, the script creates `whitelist.txt` and adds the main bridges (see above). `whitelist.txt` is ignored by git, you can edit it:
+ * to enable extra bridges (one bridge per line)
+ * to disable main bridges (remove the line)
+ * to enable all bridges (just one wildcard `*` as file content)
+
+New bridges are disabled by default, so make sure to check regularly what's new and whitelist what you want!
+
+Deploy
+===
+[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/sebsauvage/rss-bridge)
+
+Authors
+===
+We are RSS Bridge Community, a group of developers continuing the project initiated by sebsauvage, webmaster of [sebsauvage.net](http://sebsauvage.net), author of [Shaarli](http://sebsauvage.net/wiki/doku.php?id=php:shaarli) and [ZeroBin](http://sebsauvage.net/wiki/doku.php?id=php:zerobin).
+
+Patch/contributors :
+
+ * Yves ASTIER ([Draeli](https://github.com/Draeli)) : PHP optimizations, fixes, dynamic brigde/format list with all stuff behind and extend cache system. Mail : contact /at\ yves-astier.com
+ * [Mitsukarenai](https://github.com/Mitsukarenai) : Initial inspiration, collaborator
+ * [ArthurHoaro](https://github.com/ArthurHoaro)
+ * [BoboTiG](https://github.com/BoboTiG)
+ * [Astalaseven](https://github.com/Astalaseven)
+ * [qwertygc](https://github.com/qwertygc)
+ * [Djuuu](https://github.com/Djuuu)
+ * [Anadrark](https://github.com/Anadrark])
+ * [Grummfy](https://github.com/Grummfy)
+ * [Polopollo](https://github.com/Polopollo)
+ * [16mhz](https://github.com/16mhz)
+ * [kranack](https://github.com/kranack)
+ * [logmanoriginal](https://github.com/logmanoriginal)
+ * [polo2ro](https://github.com/polo2ro)
+ * [Riduidel](https://github.com/Riduidel)
+ * [superbaillot.net](http://superbaillot.net/)
+ * [vinzv](https://github.com/vinzv)
+ * [teromene](https://github.com/teromene)
+ * [nel50n](https://github.com/nel50n)
+ * [nyutag](https://github.com/nyutag)
+ * [ORelio](https://github.com/ORelio)
+ * [Pitchoule](https://github.com/Pitchoule)
+ * [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
+ * [aledeg](https://github.com/aledeg)
+ * [alexAubin](https://github.com/alexAubin)
+ * [cnlpete](https://github.com/cnlpete)
+ * [corenting](https://github.com/corenting)
+ * [Daiyousei](https://github.com/Daiyousei)
+ * [erwang](https://github.com/erwang)
+ * [gsurrel](https://github.com/gsurrel)
+ * [kraoc](https://github.com/kraoc)
+ * [lagaisse](https://github.com/lagaisse)
+ * [az5he6ch](https://github.com/az5he6ch)
+ * [niawag](https://github.com/niawag)
+ * [JeremyRand](https://github.com/JeremyRand)
+ * [mro](https://github.com/mro)
+
+Licenses
+===
+Code is [Public Domain](UNLICENSE).
+
+Including `PHP Simple HTML DOM Parser` under the [MIT License](http://opensource.org/licenses/MIT)
+
+
+Technical notes
+===
+ * There is a cache so that source services won't ban you even if you hammer the rss-bridge with requests. Each bridge can have a different duration for the cache. The `cache` subdirectory will be automatically created and cached objects older than 24 hours get purged.
+ * To implement a new Bridge, [follow the specifications](https://github.com/RSS-Bridge/rss-bridge/wiki/Bridge-API) and take a look at existing Bridges for examples.
+ * To enable debug mode (disabling cache and enabling error reporting), create an empty file named `DEBUG` in the root directory (next to `index.php`).
+ * For more information refer to the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki)
+
+Rant
+===
+
+*Dear so-called "social" websites.*
+
+Your catchword is "share", but you don't want us to share. You want to keep us within your walled gardens. That's why you've been removing RSS links from webpages, hiding them deep on your website, or removed feeds entirely, replacing it with crippled or demented proprietary API. **FUCK YOU.**
+
+You're not social when you hamper sharing by removing feeds. You're happy to have customers creating content for your ecosystem, but you don't want this content out - a content you do not even own. Google Takeout is just a gimmick. We want our data to flow, we want RSS or ATOM feeds.
+
+We want to share with friends, using open protocols: RSS, ATOM, XMPP, whatever. Because no one wants to have *your* service with *your* applications using *your* API force-feeding them. Friends must be free to choose whatever software and service they want.
+
+We are rebuilding bridges you have wilfully destroyed.
+
+Get your shit together: Put RSS/ATOM back in.
diff --git a/UNLICENSE b/UNLICENSE
new file mode 100644
index 0000000..a84c395
--- /dev/null
+++ b/UNLICENSE
@@ -0,0 +1,25 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org>
+
diff --git a/bridges/ABCTabsBridge.php b/bridges/ABCTabsBridge.php
new file mode 100644
index 0000000..2e451e2
--- /dev/null
+++ b/bridges/ABCTabsBridge.php
@@ -0,0 +1,42 @@
+<?php
+class ABCTabsBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'kranack';
+ const NAME = 'ABC Tabs Bridge';
+ const URI = 'https://www.abc-tabs.com/';
+ const DESCRIPTION = 'Returns 22 newest tabs';
+
+ public function collectData(){
+ $html = '';
+ $html = getSimpleHTMLDOM(static::URI.'tablatures/nouveautes.html')
+ or returnClientError('No results for this query.');
+
+ $table = $html->find('table#myTable', 0)->children(1);
+
+ foreach ($table->find('tr') as $tab) {
+ $item = array();
+ $item['author'] = $tab->find('td', 1)->plaintext
+ . ' - '
+ . $tab->find('td', 2)->plaintext;
+
+ $item['title'] = $tab->find('td', 1)->plaintext
+ . ' - '
+ . $tab->find('td', 2)->plaintext;
+
+ $item['content'] = 'Le '
+ . $tab->find('td', 0)->plaintext
+ . '<br> Par: '
+ . $tab->find('td', 5)->plaintext
+ . '<br> Type: '
+ . $tab->find('td', 3)->plaintext;
+
+ $item['id'] = static::URI
+ . $tab->find('td', 2)->find('a', 0)->getAttribute('href');
+
+ $item['uri'] = static::URI
+ . $tab->find('td', 2)->find('a', 0)->getAttribute('href');
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/AcrimedBridge.php b/bridges/AcrimedBridge.php
new file mode 100644
index 0000000..8b40d1d
--- /dev/null
+++ b/bridges/AcrimedBridge.php
@@ -0,0 +1,25 @@
+<?php
+class AcrimedBridge extends FeedExpander {
+
+ const MAINTAINER = 'qwertygc';
+ const NAME = 'Acrimed Bridge';
+ const URI = 'http://www.acrimed.org/';
+ const CACHE_TIMEOUT = 4800; //2hours
+ const DESCRIPTION = 'Returns the newest articles';
+
+ public function collectData(){
+ $this->collectExpandableDatas(static::URI . 'spip.php?page=backend');
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+ $article = sanitize($articlePage->find('article.article1', 0)->innertext);
+ $article = defaultLinkTo($article, static::URI);
+ $item['content'] = $article;
+
+ return $item;
+ }
+
+}
diff --git a/bridges/AllocineFRBridge.php b/bridges/AllocineFRBridge.php
new file mode 100644
index 0000000..959d0ef
--- /dev/null
+++ b/bridges/AllocineFRBridge.php
@@ -0,0 +1,87 @@
+<?php
+class AllocineFRBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'superbaillot.net';
+ const NAME = 'Allo Cine Bridge';
+ const CACHE_TIMEOUT = 25200; // 7h
+ const URI = 'http://www.allocine.fr/';
+ const DESCRIPTION = 'Bridge for allocine.fr';
+ const PARAMETERS = array( array(
+ 'category' => array(
+ 'name' => 'category',
+ 'type' => 'list',
+ 'required' => true,
+ 'exampleValue' => 'Faux Raccord',
+ 'title' => 'Select your category',
+ 'values' => array(
+ 'Faux Raccord' => 'faux-raccord',
+ 'Top 5' => 'top-5',
+ 'Tueurs en Séries' => 'tueurs-en-serie'
+ )
+ )
+ ));
+
+ public function getURI(){
+ if(!is_null($this->getInput('category'))) {
+
+ switch($this->getInput('category')) {
+ case 'faux-raccord':
+ $uri = static::URI . 'video/programme-12284/saison-32180/';
+ break;
+ case 'top-5':
+ $uri = static::URI . 'video/programme-12299/saison-29561/';
+ break;
+ case 'tueurs-en-serie':
+ $uri = static::URI . 'video/programme-12286/saison-22938/';
+ break;
+ }
+
+ return $uri;
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('category'))) {
+ return self::NAME . ' : '
+ .array_search(
+ $this->getInput('category'),
+ self::PARAMETERS[$this->queriedContext]['category']['values']
+ );
+ }
+
+ return parent::getName();
+ }
+
+ public function collectData(){
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request ' . $this->getURI() . ' !');
+
+ $category = array_search(
+ $this->getInput('category'),
+ self::PARAMETERS[$this->queriedContext]['category']['values']
+ );
+
+ foreach($html->find('.media-meta-list figure.media-meta-fig') as $element) {
+ $item = array();
+
+ $title = $element->find('div.titlebar h3.title a', 0);
+ $content = trim($element->innertext);
+ $figCaption = strpos($content, $category);
+
+ if($figCaption !== false) {
+ $content = str_replace('src="/', 'src="' . static::URI, $content);
+ $content = str_replace('href="/', 'href="' . static::URI, $content);
+ $content = str_replace('src=\'/', 'src=\'' . static::URI, $content);
+ $content = str_replace('href=\'/', 'href=\'' . static::URI, $content);
+ $item['content'] = $content;
+ $item['title'] = trim($title->innertext);
+ $item['uri'] = static::URI . $title->href;
+ $this->items[] = $item;
+ }
+ }
+ }
+
+}
diff --git a/bridges/AmazonBridge.php b/bridges/AmazonBridge.php
new file mode 100644
index 0000000..cbc6119
--- /dev/null
+++ b/bridges/AmazonBridge.php
@@ -0,0 +1,94 @@
+<?php
+
+class AmazonBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Alexis CHEMEL';
+ const NAME = 'Amazon';
+ const URI = 'https://www.amazon.com/';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Returns products from Amazon search';
+
+ const PARAMETERS = array(array(
+ 'q' => array(
+ 'name' => 'Keyword',
+ 'required' => true,
+ ),
+ 'sort' => array(
+ 'name' => 'Sort by',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => array(
+ 'Relevance' => 'relevanceblender',
+ 'Price: Low to High' => 'price-asc-rank',
+ 'Price: High to Low' => 'price-desc-rank',
+ 'Average Customer Review' => 'review-rank',
+ 'Newest Arrivals' => 'date-desc-rank',
+ ),
+ 'defaultValue' => 'relevanceblender',
+ ),
+ 'tld' => array(
+ 'name' => 'Country',
+ 'type' => 'list',
+ 'required' => true,
+ 'values' => array(
+ 'Australia' => 'com.au',
+ 'Brazil' => 'com.br',
+ 'Canada' => 'ca',
+ 'China' => 'cn',
+ 'France' => 'fr',
+ 'Germany' => 'de',
+ 'India' => 'in',
+ 'Italy' => 'it',
+ 'Japan' => 'co.jp',
+ 'Mexico' => 'com.mx',
+ 'Netherlands' => 'nl',
+ 'Spain' => 'es',
+ 'United Kingdom' => 'co.uk',
+ 'United States' => 'com',
+ ),
+ 'defaultValue' => 'com',
+ ),
+ ));
+
+ public function getName(){
+ if(!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) {
+ return 'Amazon.'.$this->getInput('tld').': '.$this->getInput('q');
+ }
+
+ return parent::getName();
+ }
+
+ public function collectData() {
+
+ $uri = 'https://www.amazon.'.$this->getInput('tld').'/';
+ $uri .= 's/?field-keywords='.urlencode($this->getInput('q')).'&sort='.$this->getInput('sort');
+
+ $html = getSimpleHTMLDOM($uri)
+ or returnServerError('Could not request Amazon.');
+
+ foreach($html->find('li.s-result-item') as $element) {
+
+ $item = array();
+
+ // Title
+ $title = $element->find('h2', 0);
+
+ $item['title'] = html_entity_decode($title->innertext, ENT_QUOTES);
+
+ // Url
+ $uri = $title->parent()->getAttribute('href');
+ $uri = substr($uri, 0, strrpos($uri, '/'));
+
+ $item['uri'] = substr($uri, 0, strrpos($uri, '/'));
+
+ // Content
+ $image = $element->find('img', 0);
+ $price = $element->find('span.s-price', 0);
+ $price = ($price) ? $price->innertext : '';
+
+ $item['content'] = '<img src="'.$image->getAttribute('src').'" /><br />'.$price;
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/AmazonPriceTrackerBridge.php b/bridges/AmazonPriceTrackerBridge.php
new file mode 100644
index 0000000..dd352af
--- /dev/null
+++ b/bridges/AmazonPriceTrackerBridge.php
@@ -0,0 +1,149 @@
+<?php
+
+class AmazonPriceTrackerBridge extends BridgeAbstract {
+ const MAINTAINER = 'captn3m0';
+ const NAME = 'Amazon Price Tracker';
+ const URI = 'https://www.amazon.com/';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Tracks price for a single product on Amazon';
+
+ const PARAMETERS = array(
+ array(
+ 'asin' => array(
+ 'name' => 'ASIN',
+ 'required' => true,
+ 'exampleValue' => 'B071GB1VMQ',
+ // https://stackoverflow.com/a/12827734
+ 'pattern' => 'B[\dA-Z]{9}|\d{9}(X|\d)',
+ ),
+ 'tld' => array(
+ 'name' => 'Country',
+ 'type' => 'list',
+ 'required' => true,
+ 'values' => array(
+ 'Australia' => 'com.au',
+ 'Brazil' => 'com.br',
+ 'Canada' => 'ca',
+ 'China' => 'cn',
+ 'France' => 'fr',
+ 'Germany' => 'de',
+ 'India' => 'in',
+ 'Italy' => 'it',
+ 'Japan' => 'co.jp',
+ 'Mexico' => 'com.mx',
+ 'Netherlands' => 'nl',
+ 'Spain' => 'es',
+ 'United Kingdom' => 'co.uk',
+ 'United States' => 'com',
+ ),
+ 'defaultValue' => 'com',
+ ),
+ ));
+
+ protected $title;
+
+ /**
+ * Generates domain name given a amazon TLD
+ */
+ private function getDomainName() {
+ return 'https://www.amazon.' . $this->getInput('tld');
+ }
+
+ /**
+ * Generates URI for a Amazon product page
+ */
+ public function getURI() {
+ if (!is_null($this->getInput('asin'))) {
+ return $this->getDomainName() . '/dp/' . $this->getInput('asin') . '/';
+ }
+ return parent::getURI();
+ }
+
+ /**
+ * Scrapes the product title from the html page
+ * returns the default title if scraping fails
+ */
+ private function getTitle($html) {
+ $titleTag = $html->find('#productTitle', 0);
+
+ if (!$titleTag) {
+ return $this->getDefaultTitle();
+ } else {
+ return trim(html_entity_decode($titleTag->innertext, ENT_QUOTES));
+ }
+ }
+
+ /**
+ * Title used by the feed if none could be found
+ */
+ private function getDefaultTitle() {
+ return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('asin');
+ }
+
+ /**
+ * Returns name for the feed
+ * Uses title (already scraped) if it has one
+ */
+ public function getName() {
+ if (isset($this->title)) {
+ return $this->title;
+ } else {
+ return parent::getName();
+ }
+ }
+
+ /**
+ * Returns a generated image tag for the product
+ */
+ private function getImage($html) {
+ $imageSrc = $html->find('#main-image-container img', 0);
+
+ if ($imageSrc) {
+ $imageSrc = $imageSrc ? $imageSrc->getAttribute('data-old-hires') : '';
+ return <<<EOT
+<img width="300" style="max-width:300;max-height:300" src="$imageSrc" alt="{$this->title}" />
+EOT;
+ }
+ }
+
+ /**
+ * Return \simple_html_dom object
+ * for the entire html of the product page
+ */
+ private function getHtml() {
+ $uri = $this->getURI();
+
+ return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request Amazon.');
+ }
+
+ /**
+ * Scrape method for Amazon product page
+ * @return [type] [description]
+ */
+ public function collectData() {
+ $html = $this->getHtml();
+ $this->title = $this->getTitle($html);
+ $imageTag = $this->getImage($html);
+
+ $asinData = $html->find('#cerberus-data-metrics', 0);
+
+ // <div id="cerberus-data-metrics" style="display: none;"
+ // data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0"
+ // data-asin-currency-code="USD" data-substitute-count="-1" ... />
+ $currency = $asinData->getAttribute('data-asin-currency-code');
+ $shipping = $asinData->getAttribute('data-asin-shipping');
+ $price = $asinData->getAttribute('data-asin-price');
+
+ $item = array(
+ 'title' => $this->title,
+ 'uri' => $this->getURI(),
+ 'content' => "$imageTag<br/>Price: $price $currency",
+ );
+
+ if ($shipping !== '0') {
+ $item['content'] .= "<br>Shipping: $shipping $currency</br>";
+ }
+
+ $this->items[] = $item;
+ }
+}
diff --git a/bridges/AnimeUltimeBridge.php b/bridges/AnimeUltimeBridge.php
new file mode 100644
index 0000000..6c5427e
--- /dev/null
+++ b/bridges/AnimeUltimeBridge.php
@@ -0,0 +1,135 @@
+<?php
+class AnimeUltimeBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = 'Anime-Ultime';
+ const URI = 'http://www.anime-ultime.net/';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the 10 newest releases posted on Anime-Ultime';
+ const PARAMETERS = array( array(
+ 'type' => array(
+ 'name' => 'Type',
+ 'type' => 'list',
+ 'values' => array(
+ 'Everything' => '',
+ 'Anime' => 'A',
+ 'Drama' => 'D',
+ 'Tokusatsu' => 'T'
+ )
+ )
+ ));
+
+ private $filter = 'Releases';
+
+ public function collectData(){
+
+ //Add type filter if provided
+ $typeFilter = array_search(
+ $this->getInput('type'),
+ self::PARAMETERS[$this->queriedContext]['type']['values']
+ );
+
+ //Build date and filters for making requests
+ $thismonth = date('mY') . $typeFilter;
+ $lastmonth = date('mY', mktime(0, 0, 0, date('n') - 1, 1, date('Y'))) . $typeFilter;
+
+ //Process each HTML page until having 10 releases
+ $processedOK = 0;
+ foreach (array($thismonth, $lastmonth) as $requestFilter) {
+
+ //Retrive page contents
+ $url = self::URI . 'history-0-1/' . $requestFilter;
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Could not request Anime-Ultime: ' . $url);
+
+ //Relases are sorted by day : process each day individually
+ foreach($html->find('div.history', 0)->find('h3') as $daySection) {
+
+ //Retrieve day and build date information
+ $dateString = $daySection->plaintext;
+ $day = intval(substr($dateString, strpos($dateString, ' ') + 1, 2));
+ $item_date = strtotime(str_pad($day, 2, '0', STR_PAD_LEFT)
+ . '-'
+ . substr($requestFilter, 0, 2)
+ . '-'
+ . substr($requestFilter, 2, 4));
+
+ //<h3>day</h3><br /><table><tr> <-- useful data in table rows
+ $release = $daySection->next_sibling()->next_sibling()->first_child();
+
+ //Process each release of that day, ignoring first table row: contains table headers
+ while(!is_null($release = $release->next_sibling())) {
+ if(count($release->find('td')) > 0) {
+
+ //Retrieve metadata from table columns
+ $item_link_element = $release->find('td', 0)->find('a', 0);
+ $item_uri = self::URI . $item_link_element->href;
+ $item_name = html_entity_decode($item_link_element->plaintext);
+ $item_episode = html_entity_decode(
+ str_pad(
+ $release->find('td', 1)->plaintext,
+ 2,
+ '0',
+ STR_PAD_LEFT
+ )
+ );
+
+ $item_fansub = $release->find('td', 2)->plaintext;
+ $item_type = $release->find('td', 4)->plaintext;
+
+ if(!empty($item_uri)) {
+
+ // Retrieve description from description page and
+ // convert relative image src info absolute image src
+ $html_item = getContents($item_uri)
+ or returnServerError('Could not request Anime-Ultime: ' . $item_uri);
+ $item_description = substr(
+ $html_item,
+ strpos($html_item, 'class="principal_contain" align="center">') + 41
+ );
+ $item_description = substr($item_description,
+ 0,
+ strpos($item_description, '<div id="table">')
+ );
+ $item_description = str_replace(
+ 'src="images', 'src="' . self::URI . 'images',
+ $item_description
+ );
+ $item_description = str_replace("\r", '', $item_description);
+ $item_description = str_replace("\n", '', $item_description);
+ $item_description = utf8_encode($item_description);
+
+ //Build and add final item
+ $item = array();
+ $item['uri'] = $item_uri;
+ $item['title'] = $item_name . ' ' . $item_type . ' ' . $item_episode;
+ $item['author'] = $item_fansub;
+ $item['timestamp'] = $item_date;
+ $item['content'] = $item_description;
+ $this->items[] = $item;
+ $processedOK++;
+
+ //Stop processing once limit is reached
+ if ($processedOK >= 10)
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public function getName() {
+ if(!is_null($this->getInput('type'))) {
+ $typeFilter = array_search(
+ $this->getInput('type'),
+ self::PARAMETERS[$this->queriedContext]['type']['values']
+ );
+
+ return 'Latest ' . $typeFilter . ' - Anime-Ultime Bridge';
+ }
+
+ return parent::getName();
+ }
+
+}
diff --git a/bridges/Arte7Bridge.php b/bridges/Arte7Bridge.php
new file mode 100644
index 0000000..16952dc
--- /dev/null
+++ b/bridges/Arte7Bridge.php
@@ -0,0 +1,100 @@
+<?php
+class Arte7Bridge extends BridgeAbstract {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Arte +7';
+ 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)' => 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'
+ )
+ )
+ ),
+ 'Catégorie (Allemand)' => array(
+ 'catde' => array(
+ 'type' => 'list',
+ 'name' => 'Catégorie',
+ 'values' => array(
+ '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'
+ )
+ )
+ )
+ );
+
+ public function collectData(){
+ switch($this->queriedContext) {
+ case 'Catégorie (Français)':
+ $category = $this->getInput('catfr');
+ $lang = 'fr';
+ break;
+ case 'Catégorie (Allemand)':
+ $category = $this->getInput('catde');
+ $lang = 'de';
+ break;
+ }
+
+ $url = 'https://api.arte.tv/api/opa/v3/videos?sort=-lastModified&limit=10&language='
+ . $lang
+ . ($category != null ? '&category.code=' . $category : '');
+
+ $header = array(
+ 'Authorization: Bearer ' . self::API_TOKEN
+ );
+
+ $input = getContents($url, $header) or die('Could not request ARTE.');
+ $input_json = json_decode($input, true);
+
+ foreach($input_json['videos'] as $element) {
+
+ $item = array();
+ $item['uri'] = $element['url'];
+ $item['id'] = $element['id'];
+
+ $item['timestamp'] = strtotime($element['videoRightsBegin']);
+ $item['title'] = $element['title'];
+
+ if(!empty($element['subtitle']))
+ $item['title'] = $element['title'] . ' | ' . $element['subtitle'];
+
+ $item['duration'] = round((int)$element['durationSeconds'] / 60);
+ $item['content'] = $element['teaserText']
+ . '<br><br>'
+ . $item['duration']
+ . 'min<br><a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $element['mainImage']['url']
+ . '" /></a>';
+
+ $this->items[] = $item;
+ }
+ }
+
+}
diff --git a/bridges/AskfmBridge.php b/bridges/AskfmBridge.php
new file mode 100644
index 0000000..e227461
--- /dev/null
+++ b/bridges/AskfmBridge.php
@@ -0,0 +1,74 @@
+<?php
+class AskfmBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'az5he6ch';
+ const NAME = 'Ask.fm Answers';
+ const URI = 'https://ask.fm/';
+ const CACHE_TIMEOUT = 300; //5 min
+ const DESCRIPTION = 'Returns answers from an Ask.fm user';
+ const PARAMETERS = array(
+ 'Ask.fm username' => array(
+ 'u' => array(
+ 'name' => 'Username',
+ 'required' => true
+ )
+ )
+ );
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Requested username can\'t be found.');
+
+ foreach($html->find('div.streamItem-answer') as $element) {
+ $item = array();
+ $item['uri'] = self::URI . $element->find('a.streamItemsAge', 0)->href;
+ $question = trim($element->find('h1.streamItemContent-question', 0)->innertext);
+
+ $item['title'] = trim(
+ htmlspecialchars_decode($element->find('h1.streamItemContent-question', 0)->plaintext,
+ ENT_QUOTES
+ )
+ );
+
+ $answer = trim($element->find('p.streamItemContent-answer', 0)->innertext);
+
+ // Doesn't work, DOM parser doesn't seem to like data-hint, dunno why
+ #$item['update'] = $element->find('a.streamitemsage',0)->data-hint;
+
+ // This probably should be cleaned up, especially for YouTube embeds
+ $visual = $element->find('div.streamItemContent-visual', 0)->innertext;
+ //Fix tracking links, also doesn't work
+ foreach($element->find('a') as $link) {
+ if(strpos($link->href, 'l.ask.fm') !== false) {
+
+ // Too slow
+ #$link->href = str_replace('#_=_', '', get_headers($link->href, 1)['Location']);
+
+ $link->href = $link->plaintext;
+ }
+ }
+
+ $content = '<p>' . $question . '</p><p>' . $answer . '</p><p>' . $visual . '</p>';
+ // Fix relative links without breaking // scheme used by YouTube stuff
+ $content = preg_replace('#href="\/(?!\/)#', 'href="' . self::URI, $content);
+ $item['content'] = $content;
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('u'))) {
+ return self::NAME . ' : ' . $this->getInput('u');
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('u'))) {
+ return self::URI . urlencode($this->getInput('u')) . '/answers/more?page=0';
+ }
+
+ return parent::getURI();
+ }
+}
diff --git a/bridges/BandcampBridge.php b/bridges/BandcampBridge.php
new file mode 100644
index 0000000..0527da0
--- /dev/null
+++ b/bridges/BandcampBridge.php
@@ -0,0 +1,63 @@
+<?php
+class BandcampBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'sebsauvage';
+ const NAME = 'Bandcamp Tag';
+ const URI = 'https://bandcamp.com/';
+ const CACHE_TIMEOUT = 600; // 10min
+ const DESCRIPTION = 'New bandcamp release by tag';
+ const PARAMETERS = array( array(
+ 'tag' => array(
+ 'name' => 'tag',
+ 'type' => 'text',
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('No results for this query.');
+
+ foreach($html->find('li.item') as $release) {
+ $script = $release->find('div.art', 0)->getAttribute('onclick');
+ $uri = ltrim($script, "return 'url(");
+ $uri = rtrim($uri, "')");
+
+ $item = array();
+ $item['author'] = $release->find('div.itemsubtext', 0)->plaintext
+ . ' - '
+ . $release->find('div.itemtext', 0)->plaintext;
+
+ $item['title'] = $release->find('div.itemsubtext', 0)->plaintext
+ . ' - '
+ . $release->find('div.itemtext', 0)->plaintext;
+
+ $item['content'] = '<img src="'
+ . $uri
+ . '"/><br/>'
+ . $release->find('div.itemsubtext', 0)->plaintext
+ . ' - '
+ . $release->find('div.itemtext', 0)->plaintext;
+
+ $item['id'] = $release->find('a', 0)->getAttribute('href');
+ $item['uri'] = $release->find('a', 0)->getAttribute('href');
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('tag'))) {
+ return self::URI . 'tag/' . urlencode($this->getInput('tag')) . '?sort_field=date';
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('tag'))) {
+ return $this->getInput('tag') . ' - Bandcamp Tag';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/BastaBridge.php b/bridges/BastaBridge.php
new file mode 100644
index 0000000..17d3da7
--- /dev/null
+++ b/bridges/BastaBridge.php
@@ -0,0 +1,34 @@
+<?php
+class BastaBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'qwertygc';
+ const NAME = 'Bastamag Bridge';
+ const URI = 'http://www.bastamag.net/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ public function collectData(){
+ // Replaces all relative image URLs by absolute URLs.
+ // Relative URLs always start with 'local/'!
+ function replaceImageUrl($content){
+ return preg_replace('/src=["\']{1}([^"\']+)/ims', 'src=\'' . self::URI . '$1\'', $content);
+ }
+
+ $html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend')
+ or returnServerError('Could not request Bastamag.');
+
+ $limit = 0;
+
+ foreach($html->find('item') as $element) {
+ if($limit < 10) {
+ $item = array();
+ $item['title'] = $element->find('title', 0)->innertext;
+ $item['uri'] = $element->find('guid', 0)->plaintext;
+ $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);
+ $item['content'] = replaceImageUrl(getSimpleHTMLDOM($item['uri'])->find('div.texte', 0)->innertext);
+ $this->items[] = $item;
+ $limit++;
+ }
+ }
+ }
+}
diff --git a/bridges/BlaguesDeMerdeBridge.php b/bridges/BlaguesDeMerdeBridge.php
new file mode 100644
index 0000000..3ae59a1
--- /dev/null
+++ b/bridges/BlaguesDeMerdeBridge.php
@@ -0,0 +1,31 @@
+<?php
+class BlaguesDeMerdeBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'superbaillot.net';
+ const NAME = 'Blagues De Merde';
+ const URI = 'http://www.blaguesdemerde.fr/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Blagues De Merde';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request BDM.');
+
+ foreach($html->find('article.joke_contener') as $element) {
+ $item = array();
+ $temp = $element->find('a');
+
+ if(isset($temp[2])) {
+ $item['content'] = trim($element->find('div.joke_text_contener', 0)->innertext);
+ $uri = $temp[2]->href;
+ $item['uri'] = $uri;
+ $item['title'] = substr($uri, (strrpos($uri, '/') + 1));
+ $date = $element->find('li.bdm_date', 0)->innertext;
+ $time = mktime(0, 0, 0, substr($date, 3, 2), substr($date, 0, 2), substr($date, 6, 4));
+ $item['timestamp'] = $time;
+ $item['author'] = $element->find('li.bdm_pseudo', 0)->innertext;
+ $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/BooruprojectBridge.php b/bridges/BooruprojectBridge.php
new file mode 100644
index 0000000..6815d37
--- /dev/null
+++ b/bridges/BooruprojectBridge.php
@@ -0,0 +1,45 @@
+<?php
+require_once('GelbooruBridge.php');
+
+class BooruprojectBridge extends GelbooruBridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Booruproject';
+ const URI = 'http://booru.org/';
+ const DESCRIPTION = 'Returns images from given page of booruproject';
+ const PARAMETERS = array(
+ 'global' => array(
+ 'p' => array(
+ 'name' => 'page',
+ 'type' => 'number'
+ ),
+ 't' => array(
+ 'name' => 'tags'
+ )
+ ),
+ 'Booru subdomain (subdomain.booru.org)' => array(
+ 'i' => array(
+ 'name' => 'Subdomain',
+ 'required' => true
+ )
+ )
+ );
+
+ const PIDBYPAGE = 20;
+
+ public function getURI(){
+ if(!is_null($this->getInput('i'))) {
+ return 'http://' . $this->getInput('i') . '.booru.org/';
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('i'))) {
+ return static::NAME . ' ' . $this->getInput('i');
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/CADBridge.php b/bridges/CADBridge.php
new file mode 100644
index 0000000..e88cbbb
--- /dev/null
+++ b/bridges/CADBridge.php
@@ -0,0 +1,45 @@
+<?php
+class CADBridge extends FeedExpander {
+ const MAINTAINER = 'nyutag';
+ const NAME = 'CAD Bridge';
+ const URI = 'http://www.cad-comic.com/';
+ const CACHE_TIMEOUT = 7200; //2h
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ public function collectData(){
+ $this->collectExpandableDatas('http://cdn2.cad-comic.com/rss.xml', 10);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $item['content'] = $this->extractCADContent($item['uri']);
+ return $item;
+ }
+
+ private function extractCADContent($url) {
+ $html3 = getSimpleHTMLDOMCached($url);
+
+ // The request might fail due to missing https support or wrong URL
+ if($html3 == false)
+ return 'Daily comic not released yet';
+
+ $htmlpart = explode('/', $url);
+
+ switch ($htmlpart[3]) {
+ case 'cad':
+ preg_match_all('/http:\/\/cdn2\.cad-comic\.com\/comics\/cad-\S*png/', $html3, $url2);
+ break;
+ case 'sillies':
+ preg_match_all('/http:\/\/cdn2\.cad-comic\.com\/comics\/sillies-\S*gif/', $html3, $url2);
+ break;
+ default:
+ return 'Daily comic not released yet';
+ }
+ $img = implode($url2[0]);
+ $html3->clear();
+ unset($html3);
+ if ($img == '')
+ return 'Daily comic not released yet';
+ return '<img src="' . $img . '"/>';
+ }
+}
diff --git a/bridges/CNETBridge.php b/bridges/CNETBridge.php
new file mode 100644
index 0000000..eefb705
--- /dev/null
+++ b/bridges/CNETBridge.php
@@ -0,0 +1,93 @@
+<?php
+class CNETBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = 'CNET News';
+ const URI = 'http://www.cnet.com/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns the newest articles. <br /> You may specify a
+topic found in some section URLs, else all topics are selected.';
+
+ const PARAMETERS = array( array(
+ 'topic' => array(
+ 'name' => 'Topic name'
+ )
+ ));
+
+ public function collectData(){
+
+ function extractFromDelimiters($string, $start, $end){
+ if(strpos($string, $start) !== false) {
+ $section_retrieved = substr($string, strpos($string, $start) + strlen($start));
+ $section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
+ return $section_retrieved;
+ }
+
+ return false;
+ }
+
+ function stripWithDelimiters($string, $start, $end){
+ while(strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ }
+
+ return $string;
+ }
+
+ function cleanArticle($article_html){
+ $article_html = '<p>' . substr($article_html, strpos($article_html, '<p>') + 3);
+ $article_html = stripWithDelimiters($article_html, '<span class="credit">', '</span>');
+ $article_html = stripWithDelimiters($article_html, '<script', '</script>');
+ $article_html = stripWithDelimiters($article_html, '<div class="shortcode related-links', '</div>');
+ $article_html = stripWithDelimiters($article_html, '<a class="clickToEnlarge">', '</a>');
+ return $article_html;
+ }
+
+ $pageUrl = self::URI . (empty($this->getInput('topic')) ? '' : 'topics/' . $this->getInput('topic') . '/');
+ $html = getSimpleHTMLDOM($pageUrl) or returnServerError('Could not request CNET: ' . $pageUrl);
+ $limit = 0;
+
+ foreach($html->find('div.assetBody') as $element) {
+ if($limit < 8) {
+ $article_title = trim($element->find('h2', 0)->plaintext);
+ $article_uri = self::URI . ($element->find('a', 0)->href);
+ $article_timestamp = strtotime($element->find('time.assetTime', 0)->plaintext);
+ $article_author = trim($element->find('a[rel=author]', 0)->plaintext);
+
+ if(!empty($article_title) && !empty($article_uri) && strpos($article_uri, '/news/') !== false) {
+ $article_html = getSimpleHTMLDOM($article_uri)
+ or returnServerError('Could not request CNET: ' . $article_uri);
+ $article_content = trim(
+ cleanArticle(
+ extractFromDelimiters(
+ $article_html,
+ '<div class="articleContent',
+ '<footer>'
+ )
+ )
+ );
+
+ $item = array();
+ $item['uri'] = $article_uri;
+ $item['title'] = $article_title;
+ $item['author'] = $article_author;
+ $item['timestamp'] = $article_timestamp;
+ $item['content'] = $article_content;
+ $this->items[] = $item;
+ $limit++;
+ }
+ }
+ }
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('topic'))) {
+ $topic = $this->getInput('topic');
+ return 'CNET News Bridge' . (empty($topic) ? '' : ' - ' . $topic);
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/CastorusBridge.php b/bridges/CastorusBridge.php
new file mode 100644
index 0000000..3ed1331
--- /dev/null
+++ b/bridges/CastorusBridge.php
@@ -0,0 +1,118 @@
+<?php
+class CastorusBridge extends BridgeAbstract {
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Castorus Bridge';
+ const URI = 'http://www.castorus.com';
+ const CACHE_TIMEOUT = 600; // 10min
+ const DESCRIPTION = 'Returns the latest changes';
+
+ const PARAMETERS = array(
+ 'Get latest changes' => array(),
+ 'Get latest changes via ZIP code' => array(
+ 'zip' => array(
+ 'name' => 'ZIP code',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => '74910, 74',
+ 'title' => 'Insert ZIP code (complete or partial)'
+ )
+ ),
+ 'Get latest changes via city name' => array(
+ 'city' => array(
+ 'name' => 'City name',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'Seyssel, Seys',
+ 'title' => 'Insert city name (complete or partial)'
+ )
+ )
+ );
+
+ // Extracts the title from an actitiy
+ private function extractActivityTitle($activity){
+ $title = $activity->find('a', 0);
+
+ if(!$title)
+ returnServerError('Cannot find title!');
+
+ return htmlspecialchars(trim($title->plaintext));
+ }
+
+ // Extracts the url from an actitiy
+ private function extractActivityUrl($activity){
+ $url = $activity->find('a', 0);
+
+ if(!$url)
+ returnServerError('Cannot find url!');
+
+ return self::URI . $url->href;
+ }
+
+ // Extracts the time from an activity
+ private function extractActivityTime($activity){
+ // Unfortunately the time is part of the parent node,
+ // so we have to clear all child nodes first
+ $nodes = $activity->find('*');
+
+ if(!$nodes)
+ returnServerError('Cannot find nodes!');
+
+ foreach($nodes as $node) {
+ $node->outertext = '';
+ }
+
+ return strtotime($activity->innertext);
+ }
+
+ // Extracts the price change
+ private function extractActivityPrice($activity){
+ $price = $activity->find('span', 1);
+
+ if(!$price)
+ returnServerError('Cannot find price!');
+
+ return $price->innertext;
+ }
+
+ public function collectData(){
+ $zip_filter = trim($this->getInput('zip'));
+ $city_filter = trim($this->getInput('city'));
+
+ $html = getSimpleHTMLDOM(self::URI);
+
+ if(!$html)
+ returnServerError('Could not load data from ' . self::URI . '!');
+
+ $activities = $html->find('div#activite/li');
+
+ if(!$activities)
+ returnServerError('Failed to find activities!');
+
+ foreach($activities as $activity) {
+ $item = array();
+
+ $item['title'] = $this->extractActivityTitle($activity);
+ $item['uri'] = $this->extractActivityUrl($activity);
+ $item['timestamp'] = $this->extractActivityTime($activity);
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '">'
+ . $item['title']
+ . '</a><br><p>'
+ . $this->extractActivityPrice($activity)
+ . '</p>';
+
+ if(isset($zip_filter)
+ && !(substr($item['title'], 0, strlen($zip_filter)) === $zip_filter)) {
+ continue; // Skip this item
+ }
+
+ if(isset($city_filter)
+ && !(substr($item['title'], strpos($item['title'], ' ') + 1, strlen($city_filter)) === $city_filter)) {
+ continue; // Skip this item
+ }
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/ChristianDailyReporterBridge.php b/bridges/ChristianDailyReporterBridge.php
new file mode 100644
index 0000000..b8cbf3c
--- /dev/null
+++ b/bridges/ChristianDailyReporterBridge.php
@@ -0,0 +1,25 @@
+<?php
+class ChristianDailyReporterBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'rogerdc';
+ const NAME = 'Christian Daily Reporter Unofficial RSS';
+ const URI = 'https://www.christiandailyreporter.com/';
+ const DESCRIPTION = 'The Unofficial Christian Daily Reporter RSS';
+ // const CACHE_TIMEOUT = 86400; // 1 day
+
+
+ public function collectData() {
+ $uri = 'https://www.christiandailyreporter.com/';
+
+ $html = getSimpleHTMLDOM($uri)
+ or returnServerError('Could not request Christian Daily Reporter.');
+ foreach($html->find('div.top p a,div.column p a') as $element) {
+ $item = array();
+ // Title
+ $item['title'] = $element->innertext;
+ // URL
+ $item['uri'] = $element->href;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/CollegeDeFranceBridge.php b/bridges/CollegeDeFranceBridge.php
new file mode 100644
index 0000000..1f81683
--- /dev/null
+++ b/bridges/CollegeDeFranceBridge.php
@@ -0,0 +1,84 @@
+<?php
+class CollegeDeFranceBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'pit-fgfjiudghdf';
+ const NAME = 'CollegeDeFrance';
+ const URI = 'http://www.college-de-france.fr/';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the latest audio and video from CollegeDeFrance';
+
+ public function collectData(){
+ $months = array(
+ '01' => 'janv.',
+ '02' => 'févr.',
+ '03' => 'mars',
+ '04' => 'avr.',
+ '05' => 'mai',
+ '06' => 'juin',
+ '07' => 'juil.',
+ '08' => 'août',
+ '09' => 'sept.',
+ '10' => 'oct.',
+ '11' => 'nov.',
+ '12' => 'déc.'
+ );
+
+ // The "API" used by the site returns a list of partial HTML in this form
+ /* <li>
+ * <a href="/site/thomas-romer/guestlecturer-2016-04-15-14h30.htm" data-target="after">
+ * <span class="date"><span class="list-icon list-icon-video"></span>
+ * <span class="list-icon list-icon-audio"></span>15 avr. 2016</span>
+ * <span class="lecturer">Christopher Hays</span>
+ * <span class='title'>Imagery of Divine Suckling in the Hebrew Bible and the Ancient Near East</span>
+ * </a>
+ * </li>
+ */
+ $html = getSimpleHTMLDOM(self::URI
+ . 'components/search-audiovideo.jsp?fulltext=&siteid=1156951719600&lang=FR&type=all')
+ or returnServerError('Could not request CollegeDeFrance.');
+
+ foreach($html->find('a[data-target]') as $element) {
+ $item = array();
+ $item['title'] = $element->find('.title', 0)->plaintext;
+
+ // Most relative URLs contains an hour in addition to the date, so let's use it
+ // <a href="/site/yann-lecun/course-2016-04-08-11h00.htm" data-target="after">
+ //
+ // Sometimes there's an __1, perhaps it signifies an update
+ // "/site/patrick-boucheron/seminar-2016-05-03-18h00__1.htm"
+ //
+ // But unfortunately some don't have any hours info
+ // <a href="/site/institut-physique/
+ // The-Mysteries-of-Decoherence-Sebastien-Gleyzes-[Video-3-35].htm" data-target="after">
+ $timezone = new DateTimeZone('Europe/Paris');
+
+ // strpos($element->href, '201') will break in 2020 but it'll
+ // probably break prior to then due to site changes anyway
+ $d = DateTime::createFromFormat(
+ '!Y-m-d-H\hi',
+ substr($element->href, strpos($element->href, '201'), 16),
+ $timezone
+ );
+
+ if(!$d) {
+ $d = DateTime::createFromFormat(
+ '!d m Y',
+ trim(str_replace(
+ array_values($months),
+ array_keys($months),
+ $element->find('.date', 0)->plaintext
+ )),
+ $timezone
+ );
+ }
+
+ $item['timestamp'] = $d->format('U');
+ $item['content'] = $element->find('.lecturer', 0)->innertext
+ . ' - '
+ . $element->find('.title', 0)->innertext;
+
+ $item['uri'] = self::URI . $element->href;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/CommonDreamsBridge.php b/bridges/CommonDreamsBridge.php
new file mode 100644
index 0000000..e4dcb63
--- /dev/null
+++ b/bridges/CommonDreamsBridge.php
@@ -0,0 +1,26 @@
+<?php
+class CommonDreamsBridge extends FeedExpander {
+
+ const MAINTAINER = 'nyutag';
+ const NAME = 'CommonDreams Bridge';
+ const URI = 'http://www.commondreams.org/';
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ public function collectData(){
+ $this->collectExpandableDatas('http://www.commondreams.org/rss.xml', 10);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $item['content'] = $this->extractContent($item['uri']);
+ return $item;
+ }
+
+ private function extractContent($url){
+ $html3 = getSimpleHTMLDOMCached($url);
+ $text = $html3->find('div[class=field--type-text-with-summary]', 0)->innertext;
+ $html3->clear();
+ unset ($html3);
+ return $text;
+ }
+}
diff --git a/bridges/ContainerLinuxReleasesBridge.php b/bridges/ContainerLinuxReleasesBridge.php
new file mode 100644
index 0000000..06c8ac4
--- /dev/null
+++ b/bridges/ContainerLinuxReleasesBridge.php
@@ -0,0 +1,93 @@
+<?php
+class ContainerLinuxReleasesBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'captn3m0';
+ const NAME = 'Core OS Container Linux Releases Bridge';
+ const URI = 'https://coreos.com/releases/';
+ const DESCRIPTION = 'Returns the releases notes for Container Linux';
+
+ const STABLE = 'stable';
+ const BETA = 'beta';
+ const ALPHA = 'alpha';
+
+ const PARAMETERS = [
+ [
+ 'channel' => [
+ 'name' => 'Release Channel',
+ 'type' => 'list',
+ 'required' => true,
+ 'defaultValue' => self::STABLE,
+ 'values' => [
+ 'Stable' => self::STABLE,
+ 'Beta' => self::BETA,
+ 'Alpha' => self::ALPHA,
+ ],
+ ]
+ ]
+ ];
+
+ public function getReleaseFeed($jsonUrl) {
+ $json = getContents($jsonUrl)
+ or returnServerError('Could not request Core OS Website.');
+ return json_decode($json, true);
+ }
+
+ public function collectData() {
+ $data = $this->getReleaseFeed($this->getJsonUri());
+
+ foreach ($data as $releaseVersion => $release) {
+ $item = [];
+
+ $item['uri'] = "https://coreos.com/releases/#$releaseVersion";
+ $item['title'] = $releaseVersion;
+
+ $content = $release['release_notes'];
+ $content .= <<<EOT
+
+Major Software:
+* Kernel: {$release['major_software']['kernel'][0]}
+* Docker: {$release['major_software']['docker'][0]}
+* etcd: {$release['major_software']['etcd'][0]}
+EOT;
+ $item['timestamp'] = strtotime($release['release_date']);
+
+ // Based on https://gist.github.com/jbroadway/2836900
+ // Links
+ $regex = '/\[([^\[]+)\]\(([^\)]+)\)/';
+ $replacement = '<a href=\'\2\'>\1</a>';
+ $item['content'] = preg_replace($regex, $replacement, $content);
+
+ // Headings
+ $regex = '/^(.*)\:\s?$/m';
+ $replacement = '<h3>\1</h3>';
+ $item['content'] = preg_replace($regex, $replacement, $item['content']);
+
+ // Lists
+ $regex = '/\n\s*[\*|\-](.*)/';
+ $item['content'] = preg_replace_callback ($regex, function($regs) {
+ $item = $regs[1];
+ return sprintf ('<ul><li>%s</li></ul>', trim ($item));
+ }, $item['content']);
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function getJsonUri() {
+ $channel = $this->getInput('channel');
+
+ return "https://coreos.com/releases/releases-$channel.json";
+ }
+
+ public function getURI() {
+ return self::URI;
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('channel'))) {
+ return 'Container Linux Releases: ' . $this->getInput('channel') . ' Channel';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/CopieDoubleBridge.php b/bridges/CopieDoubleBridge.php
new file mode 100644
index 0000000..3545c6f
--- /dev/null
+++ b/bridges/CopieDoubleBridge.php
@@ -0,0 +1,35 @@
+<?php
+class CopieDoubleBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'superbaillot.net';
+ const NAME = 'CopieDouble';
+ const URI = 'http://www.copie-double.com/';
+ const CACHE_TIMEOUT = 14400; // 4h
+ const DESCRIPTION = 'CopieDouble';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request CopieDouble.');
+
+ $table = $html->find('table table', 2);
+
+ foreach($table->find('tr') as $element) {
+ $td = $element->find('td', 0);
+
+ if($td->class === 'couleur_1') {
+ $item = array();
+ $title = $td->innertext;
+ $pos = strpos($title, '<a');
+ $title = substr($title, 0, $pos);
+ $item['title'] = $title;
+ } elseif(strpos($element->innertext, '/images/suivant.gif') === false) {
+ $a = $element->find('a', 0);
+ $item['uri'] = self::URI . $a->href;
+ $content = str_replace('src="/', 'src="/' . self::URI, $element->find('td', 0)->innertext);
+ $content = str_replace('href="/', 'href="' . self::URI, $content);
+ $item['content'] = $content;
+ $this->items[] = $item;
+ }
+ }
+ }
+}
diff --git a/bridges/CourrierInternationalBridge.php b/bridges/CourrierInternationalBridge.php
new file mode 100644
index 0000000..1e7c93e
--- /dev/null
+++ b/bridges/CourrierInternationalBridge.php
@@ -0,0 +1,55 @@
+<?php
+class CourrierInternationalBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'Courrier International Bridge';
+ const URI = 'http://CourrierInternational.com/';
+ const CACHE_TIMEOUT = 300; // 5 min
+ const DESCRIPTION = 'Courrier International bridge';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Error.');
+
+ $element = $html->find('article');
+ $article_count = 1;
+
+ foreach($element as $article) {
+ $item = array();
+
+ $item['uri'] = $article->parent->getAttribute('href');
+
+ if(strpos($item['uri'], 'http') === false) {
+ $item['uri'] = self::URI . $item['uri'];
+ }
+
+ $page = getSimpleHTMLDOMCached($item['uri']);
+
+ $content = $page->find('.article-text', 0);
+
+ if(!$content) {
+ $content = $page->find('.depeche-text', 0);
+ }
+
+ $item['content'] = sanitize($content);
+ $item['title'] = strip_tags($article->find('.title', 0));
+
+ $dateTime = date_parse($page->find('time', 0));
+
+ $item['timestamp'] = mktime(
+ $dateTime['hour'],
+ $dateTime['minute'],
+ $dateTime['second'],
+ $dateTime['month'],
+ $dateTime['day'],
+ $dateTime['year']
+ );
+
+ $this->items[] = $item;
+ $article_count ++;
+
+ if($article_count > 5)
+ break;
+ }
+ }
+}
diff --git a/bridges/CpasbienBridge.php b/bridges/CpasbienBridge.php
new file mode 100644
index 0000000..f9b4b50
--- /dev/null
+++ b/bridges/CpasbienBridge.php
@@ -0,0 +1,74 @@
+<?php
+class CpasbienBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'lagaisse';
+ const NAME = 'Cpasbien Bridge';
+ const URI = 'http://www.cpasbien.cm';
+ const CACHE_TIMEOUT = 86400; // 24h
+ const DESCRIPTION = 'Returns latest torrents from a request query';
+
+ const PARAMETERS = array( array(
+ 'q' => array(
+ 'name' => 'Search',
+ 'required' => true,
+ 'title' => 'Type your search'
+ )
+ ));
+
+ public function collectData(){
+ $request = str_replace(' ', '-', trim($this->getInput('q')));
+ $html = getSimpleHTMLDOM(self::URI . '/recherche/' . urlencode($request) . '.html')
+ or returnServerError('No results for this query.');
+
+ foreach($html->find('#gauche', 0)->find('div') as $episode) {
+ if($episode->getAttribute('class') == 'ligne0'
+ || $episode->getAttribute('class') == 'ligne1') {
+
+ $urlepisode = $episode->find('a', 0)->getAttribute('href');
+ $htmlepisode = getSimpleHTMLDOMCached($urlepisode, 86400 * 366 * 30);
+
+ $item = array();
+ $item['author'] = $episode->find('a', 0)->text();
+ $item['title'] = $episode->find('a', 0)->text();
+ $item['pubdate'] = $this->getCachedDate($urlepisode);
+ $textefiche = $htmlepisode->find('#textefiche', 0)->find('p', 1);
+
+ if(isset($textefiche)) {
+ $item['content'] = $textefiche->text();
+ } else {
+ $p = $htmlepisode->find('#textefiche', 0)->find('p');
+ if(!empty($p)) {
+ $item['content'] = $htmlepisode->find('#textefiche', 0)->find('p', 0)->text();
+ }
+ }
+
+ $item['id'] = $episode->find('a', 0)->getAttribute('href');
+ $item['uri'] = self::URI . $htmlepisode->find('#telecharger', 0)->getAttribute('href');
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('q'))) {
+ return $this->getInput('q') . ' : ' . self::NAME;
+ }
+
+ return parent::getName();
+ }
+
+ private function getCachedDate($url){
+ debugMessage('getting pubdate from url ' . $url . '');
+
+ // Initialize cache
+ $cache = Cache::create('FileCache');
+ $cache->setPath(CACHE_DIR . '/pages');
+
+ $params = [$url];
+ $cache->setParameters($params);
+
+ // Get cachefile timestamp
+ $time = $cache->getTime();
+ return ($time !== false ? $time : time());
+ }
+}
diff --git a/bridges/CryptomeBridge.php b/bridges/CryptomeBridge.php
new file mode 100644
index 0000000..8a3936f
--- /dev/null
+++ b/bridges/CryptomeBridge.php
@@ -0,0 +1,45 @@
+<?php
+class CryptomeBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'BoboTiG';
+ const NAME = 'Cryptome';
+ const URI = 'https://cryptome.org/';
+ const CACHE_TIMEOUT = 21600; //6h
+ const DESCRIPTION = 'Returns the N most recent documents.';
+
+ const PARAMETERS = array( array(
+ 'n' => array(
+ 'name' => 'number of elements',
+ 'type' => 'number',
+ 'defaultValue' => 20,
+ 'exampleValue' => 10
+ )
+ ));
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request Cryptome.');
+
+ $number = $this->getInput('n');
+
+ /* number of documents */
+ if(!empty($number)) {
+ $num = min($number, 20);
+ }
+
+ foreach($html->find('pre') as $element) {
+ for($i = 0; $i < $num; ++$i) {
+ $item = array();
+ $item['uri'] = self::URI . substr($element->find('a', $i)->href, 20);
+ $item['title'] = substr($element->find('b', $i)->plaintext, 22);
+ $item['content'] = preg_replace(
+ '#http://cryptome.org/#',
+ self::URI,
+ $element->find('b', $i)->innertext
+ );
+ $this->items[] = $item;
+ }
+ break;
+ }
+ }
+}
diff --git a/bridges/DailymotionBridge.php b/bridges/DailymotionBridge.php
new file mode 100644
index 0000000..d075041
--- /dev/null
+++ b/bridges/DailymotionBridge.php
@@ -0,0 +1,123 @@
+<?php
+class DailymotionBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Dailymotion Bridge';
+ const URI = 'https://www.dailymotion.com/';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the 5 newest videos by username/playlist or search';
+
+ const PARAMETERS = array (
+ 'By username' => array(
+ 'u' => array(
+ 'name' => 'username',
+ 'required' => true
+ )
+ ),
+ 'By playlist id' => array(
+ 'p' => array(
+ 'name' => 'playlist id',
+ 'required' => true
+ )
+ ),
+ 'From search results' => array(
+ 's' => array(
+ 'name' => 'Search keyword',
+ 'required' => true
+ ),
+ 'pa' => array(
+ 'name' => 'Page',
+ 'type' => 'number'
+ )
+ )
+ );
+
+ protected function getMetadata($id){
+ $metadata = array();
+ $html2 = getSimpleHTMLDOM(self::URI . 'video/' . $id);
+ if(!$html2) {
+ return $metadata;
+ }
+
+ $metadata['title'] = $html2->find('meta[property=og:title]', 0)->getAttribute('content');
+ $metadata['timestamp'] = strtotime(
+ $html2->find('meta[property=video:release_date]', 0)->getAttribute('content')
+ );
+ $metadata['thumbnailUri'] = $html2->find('meta[property=og:image]', 0)->getAttribute('content');
+ $metadata['uri'] = $html2->find('meta[property=og:url]', 0)->getAttribute('content');
+ return $metadata;
+ }
+
+ public function collectData(){
+ $html = '';
+ $limit = 5;
+ $count = 0;
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request Dailymotion.');
+
+ foreach($html->find('div.media a.preview_link') as $element) {
+ if($count < $limit) {
+ $item = array();
+ $item['id'] = str_replace('/video/', '', strtok($element->href, '_'));
+ $metadata = $this->getMetadata($item['id']);
+ if(empty($metadata)) {
+ continue;
+ }
+ $item['uri'] = $metadata['uri'];
+ $item['title'] = $metadata['title'];
+ $item['timestamp'] = $metadata['timestamp'];
+
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $metadata['thumbnailUri']
+ . '" /></a><br><a href="'
+ . $item['uri']
+ . '">'
+ . $item['title']
+ . '</a>';
+
+ $this->items[] = $item;
+ $count++;
+ }
+ }
+ }
+
+ public function getName(){
+ switch($this->queriedContext) {
+ case 'By username':
+ $specific = $this->getInput('u');
+ break;
+ case 'By playlist id':
+ $specific = strtok($this->getInput('p'), '_');
+ break;
+ case 'From search results':
+ $specific = $this->getInput('s');
+ break;
+ default: return parent::getName();
+ }
+
+ return $specific . ' : Dailymotion Bridge';
+ }
+
+ public function getURI(){
+ $uri = self::URI;
+ switch($this->queriedContext) {
+ case 'By username':
+ $uri .= 'user/' . urlencode($this->getInput('u')) . '/1';
+ break;
+ case 'By playlist id':
+ $uri .= 'playlist/' . urlencode(strtok($this->getInput('p'), '_'));
+ break;
+ case 'From search results':
+ $uri .= 'search/' . urlencode($this->getInput('s'));
+ if($this->getInput('pa')) {
+ $uri .= '/' . $this->getInput('pa');
+ }
+ break;
+ default: return parent::getURI();
+ }
+ return $uri;
+ }
+}
diff --git a/bridges/DanbooruBridge.php b/bridges/DanbooruBridge.php
new file mode 100644
index 0000000..82f2167
--- /dev/null
+++ b/bridges/DanbooruBridge.php
@@ -0,0 +1,67 @@
+<?php
+class DanbooruBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Danbooru';
+ const URI = 'http://donmai.us/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns images from given page';
+
+ const PARAMETERS = array(
+ 'global' => array(
+ 'p' => array(
+ 'name' => 'page',
+ 'defaultValue' => 1,
+ 'type' => 'number'
+ ),
+ 't' => array(
+ 'name' => 'tags'
+ )
+ ),
+ 0 => array()
+ );
+
+ const PATHTODATA = 'article';
+ const IDATTRIBUTE = 'data-id';
+ const TAGATTRIBUTE = 'alt';
+
+ protected function getFullURI(){
+ return $this->getURI()
+ . 'posts?&page=' . $this->getInput('p')
+ . '&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());
+
+ $item = array();
+ $item['uri'] = $element->find('a', 0)->href;
+ $item['postid'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
+ $item['timestamp'] = time();
+ $thumbnailUri = $element->find('img', 0)->src;
+ $item['tags'] = $this->getTags($element);
+ $item['title'] = $this->getName() . ' | ' . $item['postid'];
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $thumbnailUri
+ . '" /></a><br>Tags: '
+ . $item['tags'];
+
+ return $item;
+ }
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getFullURI())
+ or returnServerError('Could not request ' . $this->getName());
+
+ foreach($html->find(static::PATHTODATA) as $element) {
+ $this->items[] = $this->getItemFromElement($element);
+ }
+ }
+}
diff --git a/bridges/DansTonChatBridge.php b/bridges/DansTonChatBridge.php
new file mode 100644
index 0000000..0983bff
--- /dev/null
+++ b/bridges/DansTonChatBridge.php
@@ -0,0 +1,28 @@
+<?php
+class DansTonChatBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Astalaseven';
+ const NAME = 'DansTonChat Bridge';
+ const URI = 'https://danstonchat.com/';
+ const CACHE_TIMEOUT = 21600; //6h
+ const DESCRIPTION = 'Returns latest quotes from DansTonChat.';
+
+ public function collectData(){
+
+ $html = getSimpleHTMLDOM(self::URI . 'latest.html')
+ or returnServerError('Could not request DansTonChat.');
+
+ foreach($html->find('div.item') as $element) {
+ $item = array();
+ $item['uri'] = $element->find('a', 0)->href;
+ $titleContent = $element->find('h3 a', 0);
+ if($titleContent) {
+ $item['title'] = 'DansTonChat ' . html_entity_decode($titleContent->plaintext, ENT_QUOTES);
+ } else {
+ $item['title'] = 'DansTonChat';
+ }
+ $item['content'] = $element->find('div.item-content a', 0)->innertext;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/DauphineLibereBridge.php b/bridges/DauphineLibereBridge.php
new file mode 100644
index 0000000..7547d74
--- /dev/null
+++ b/bridges/DauphineLibereBridge.php
@@ -0,0 +1,56 @@
+<?php
+class DauphineLibereBridge extends FeedExpander {
+
+ const MAINTAINER = 'qwertygc';
+ const NAME = 'Dauphine Bridge';
+ const URI = 'http://www.ledauphine.com/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ const PARAMETERS = array( array(
+ 'u' => array(
+ 'name' => 'Catégorie de l\'article',
+ 'type' => 'list',
+ 'values' => array(
+ 'À la une' => '',
+ 'France Monde' => 'france-monde',
+ 'Faits Divers' => 'faits-divers',
+ 'Économie et Finance' => 'economie-et-finance',
+ 'Politique' => 'politique',
+ 'Sport' => 'sport',
+ 'Ain' => 'ain',
+ 'Alpes-de-Haute-Provence' => 'haute-provence',
+ 'Hautes-Alpes' => 'hautes-alpes',
+ 'Ardèche' => 'ardeche',
+ 'Drôme' => 'drome',
+ 'Isère Sud' => 'isere-sud',
+ 'Savoie' => 'savoie',
+ 'Haute-Savoie' => 'haute-savoie',
+ 'Vaucluse' => 'vaucluse'
+ )
+ )
+ ));
+
+ public function collectData(){
+ $url = self::URI . 'rss';
+
+ if(empty($this->getInput('u'))) {
+ $url = self::URI . $this->getInput('u') . '/rss';
+ }
+
+ $this->collectExpandableDatas($url, 10);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $item['content'] = $this->extractContent($item['uri']);
+ return $item;
+ }
+
+ private function extractContent($url){
+ $html2 = getSimpleHTMLDOMCached($url);
+ $text = $html2->find('div.column', 0)->innertext;
+ $text = preg_replace('@<script[^>]*?>.*?</script>@si', '', $text);
+ return $text;
+ }
+}
diff --git a/bridges/DealabsBridge.php b/bridges/DealabsBridge.php
new file mode 100644
index 0000000..fb88f37
--- /dev/null
+++ b/bridges/DealabsBridge.php
@@ -0,0 +1,589 @@
+<?php
+class DealabsBridge extends PepperBridgeAbstract {
+
+ const NAME = 'Dealabs Bridge';
+ const URI = 'https://www.dealabs.com/';
+ const DESCRIPTION = 'Affiche les Deals de Dealabs';
+ 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(
+ 'group' => 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',
+ )
+ ),
+ 'order' => 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'
+ )
+ )
+ )
+ );
+
+ public $lang = array(
+ 'bridge-uri' => SELF::URI,
+ 'bridge-name' => SELF::NAME,
+ 'context-keyword' => 'Recherche par Mot(s) clé(s)',
+ 'context-group' => 'Deals par groupe',
+ 'uri-group' => '/groupe/',
+ 'request-error' => 'Could not request Dealabs',
+ 'no-results' => 'Il n&#039;y a rien à afficher pour le moment :(',
+ 'relative-date-indicator' => array(
+ 'il y a',
+ ),
+ 'price' => 'Prix',
+ 'shipping' => 'Livraison',
+ 'origin' => 'Origine',
+ 'discount' => 'Réduction',
+ 'title-keyword' => 'Recherche',
+ 'title-group' => 'Groupe',
+ 'local-months' => array(
+ 'janvier',
+ 'février',
+ 'mars',
+ 'avril',
+ 'mai',
+ 'juin',
+ 'juillet',
+ 'août',
+ 'septembre',
+ 'octobre',
+ 'novembre',
+ 'décembre'
+ ),
+ 'local-time-relative' => array(
+ 'il y a ',
+ 'min',
+ 'h',
+ 'jour',
+ 'jours',
+ 'mois',
+ 'ans',
+ 'et '
+ ),
+ 'date-prefixes' => array(
+ 'Actualisé ',
+ ),
+ 'relative-date-alt-prefixes' => array(
+ 'Actualisé ',
+ ),
+ 'relative-date-ignore-suffix' => array(
+ ),
+
+ 'localdeal' => array(
+ 'Local',
+ 'Pays d\'expédition'
+ ),
+ );
+
+
+
+}
+
+class PepperBridgeAbstract extends BridgeAbstract {
+
+ const CACHE_TIMEOUT = 3600;
+
+ public function collectData(){
+ switch($this->queriedContext) {
+ case $this->i8n('context-keyword'):
+ return $this->collectDataKeywords();
+ break;
+ case $this->i8n('context-group'):
+ return $this->collectDataGroup();
+ break;
+ }
+ }
+
+ /**
+ * Get the Deal data from the choosen group in the choosed order
+ */
+ public function collectDataGroup()
+ {
+
+ $group = $this->getInput('group');
+ $order = $this->getInput('order');
+
+ $url = $this->i8n('bridge-uri')
+ . $this->i8n('uri-group') . $group . $order;
+ $this->collectDeals($url);
+ }
+
+ /**
+ * Get the Deal data from the choosen keywords and parameters
+ */
+ public function collectDataKeywords()
+ {
+ $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 = $this->i8n('bridge-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($this->i8n('request-error'));
+ $list = $html->find('article[id]');
+
+ // Deal Image Link CSS Selector
+ $selectorImageLink = implode(
+ ' ', /* Notice this is a space! */
+ array(
+ 'cept-thread-image-link',
+ 'imgFrame',
+ 'imgFrame--noBorder',
+ 'thread-listImgCell',
+ )
+ );
+
+ // Deal Link CSS Selector
+ $selectorLink = implode(
+ ' ', /* Notice this is a space! */
+ array(
+ 'cept-tt',
+ 'thread-link',
+ 'linkPlain',
+ )
+ );
+
+ // 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--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 && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) {
+ $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->getPrice($deal)
+ . $this->getDiscount($deal)
+ . $this->getShipsFrom($deal)
+ . $this->getShippingCost($deal)
+ . $this->GetSource($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;
+ // In case of a Local deal, there is no date, but we can use
+ // this case for other reason (like date not in the last field)
+ if ($this->contains($itemDate, $this->i8n('localdeal'))) {
+ $item['timestamp'] = time();
+ } else if ($this->contains($itemDate, $this->i8n('relative-date-indicator'))) {
+ $item['timestamp'] = $this->relativeDateToTimestamp($itemDate);
+ } else {
+ $item['timestamp'] = $this->parseDate($itemDate);
+ }
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ /**
+ * Check if the string $str contains any of the string of the array $arr
+ * @return boolean true if the string matched anything otherwise false
+ */
+ private function contains($str, array $arr)
+ {
+ foreach ($arr as $a) {
+ if (stripos($str, $a) !== false) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the Price from a Deal if it exists
+ * @return string String of the deal price
+ */
+ private function getPrice($deal)
+ {
+ if ($deal->find(
+ 'span[class*=thread-price]', 0) != null) {
+ return '<div>'.$this->i8n('price') .' : '
+ . $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 getShippingCost($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>'. $this->i8n('shipping') .' : '
+ . $deal->find('span[class*=cept-shipping-price]', 0)->children(0)->innertext
+ . '</div>';
+ } else {
+ return '<div>'. $this->i8n('shipping') .' : '
+ . $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 GetSource($deal)
+ {
+ if ($deal->find('a[class=text--color-greyShade]', 0) != null) {
+ return '<div>'. $this->i8n('origin') .' : '
+ . $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 getDiscount($deal)
+ {
+ if ($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) {
+ $discountHtml = $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0);
+ if ($discountHtml != null) {
+ $discount = $discountHtml->plaintext;
+ } else {
+ $discount = '';
+ }
+ return '<div>'. $this->i8n('discount') .' : <span style="text-decoration: line-through;">'
+ . $deal->find(
+ 'span[class*=mute--text text--lineThrough]', 0
+ )->plaintext
+ . '</span>&nbsp;'
+ . $discount
+ . '</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 exists
+ * @return string String of the deal originating country
+ */
+ private function getShipsFrom($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 local date into a timestamp
+ * @return int timestamp of the input date
+ */
+ private function parseDate($string)
+ {
+ $month_local = $this->i8n('local-months');
+ $month_en = array(
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+ );
+
+ // A date can be prfixed with some words, we remove theme
+ $string = $this->removeDatePrefixes($string);
+ // We translate the local months name in the english one
+ $date_str = trim(str_replace($month_local, $month_en, $string));
+
+ // If the date does not contain any year, we add the current year
+ if (!preg_match('/[0-9]{4}/', $string)) {
+ $date_str .= ' ' . date('Y');
+ }
+
+ // Add the Hour and minutes
+ $date_str .= ' 00:00';
+
+ $date = DateTime::createFromFormat('j F Y H:i', $date_str);
+ return $date->getTimestamp();
+ }
+
+ /**
+ * Remove the prefix of a date if it has one
+ * @return the date without prefiux
+ */
+ private function removeDatePrefixes($string)
+ {
+ $string = str_replace($this->i8n('date-prefixes'), array(), $string);
+ return $string;
+ }
+
+ /**
+ * Remove the suffix of a relative date if it has one
+ * @return the relative date without suffixes
+ */
+ private function removeRelativeDateSuffixes($string)
+ {
+ if (count($this->i8n('relative-date-ignore-suffix')) > 0) {
+ $string = preg_replace($this->i8n('relative-date-ignore-suffix'), '', $string);
+ }
+ return $string;
+ }
+
+ /**
+ * Transforms a relative local date into a timestamp
+ * @return int timestamp of the input date
+ */
+ private function relativeDateToTimestamp($str) {
+ $date = new DateTime();
+
+ // In case of update date, replace it by the regular relative date first word
+ $str = str_replace($this->i8n('relative-date-alt-prefixes'), $this->i8n('local-time-relative')[0], $str);
+
+ $str = $this->removeRelativeDateSuffixes($str);
+
+ $search = $this->i8n('local-time-relative');
+
+ $replace = array(
+ '-',
+ 'minute',
+ 'hour',
+ 'day',
+ 'month',
+ 'year',
+ ''
+ );
+
+ $date->modify(str_replace($search, $replace, $str));
+ return $date->getTimestamp();
+ }
+
+ /**
+ * Returns the RSS Feed title according to the parameters
+ * @return string the RSS feed Tiyle
+ */
+ public function getName(){
+ switch($this->queriedContext) {
+ case $this->i8n('context-keyword'):
+ return $this->i8n('bridge-name') . ' - '. $this->i8n('title-keyword') .' : '. $this->getInput('q');
+ break;
+ case $this->i8n('context-group'):
+ $values = $this->getParameters()[$this->i8n('context-group')]['group']['values'];
+ $group = array_search($this->getInput('group'), $values);
+ return $this->i8n('bridge-name') . ' - '. $this->i8n('title-group'). ' : '. $group;
+ break;
+ default: // Return default value
+ return static::NAME;
+ }
+ }
+
+
+
+ /**
+ * This is some "localisation" function that returns the needed content using
+ * the "$lang" class variable in the local class
+ * @return various the local content needed
+ */
+ public function i8n($key)
+ {
+ if (array_key_exists($key, $this->lang)) {
+ return $this->lang[$key];
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/bridges/DemoBridge.php b/bridges/DemoBridge.php
new file mode 100644
index 0000000..f48b451
--- /dev/null
+++ b/bridges/DemoBridge.php
@@ -0,0 +1,46 @@
+<?php
+class DemoBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'DemoBridge';
+ const URI = 'http://github.com/rss-bridge/rss-bridge';
+ const DESCRIPTION = 'Bridge used for demos';
+
+ const PARAMETERS = array(
+ 'testCheckbox' => array(
+ 'testCheckbox' => array(
+ 'type' => 'checkbox',
+ 'name' => 'test des checkbox'
+ )
+ ),
+ 'testList' => array(
+ 'testList' => array(
+ 'type' => 'list',
+ 'name' => 'test des listes',
+ 'values' => array(
+ 'Test' => 'test',
+ 'Test 2' => 'test2'
+ )
+ )
+ ),
+ 'testNumber' => array(
+ 'testNumber' => array(
+ 'type' => 'number',
+ 'name' => 'test des numéros',
+ 'exampleValue' => '1515632'
+ )
+ )
+ );
+
+ public function collectData(){
+
+ $item = array();
+ $item['author'] = 'Me!';
+ $item['title'] = 'Test';
+ $item['content'] = 'Awesome content !';
+ $item['id'] = 'Lalala';
+ $item['uri'] = 'http://example.com/test';
+
+ $this->items[] = $item;
+ }
+}
diff --git a/bridges/DemonoidBridge.php b/bridges/DemonoidBridge.php
new file mode 100644
index 0000000..bfbb785
--- /dev/null
+++ b/bridges/DemonoidBridge.php
@@ -0,0 +1,166 @@
+<?php
+class DemonoidBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'metaMMA';
+ const NAME = 'Demonoid';
+ const URI = 'https://www.demonoid.pw/';
+ const DESCRIPTION = 'Returns results from search';
+
+ const PARAMETERS = array(array(
+ 'q' => array(
+ 'name' => 'keywords',
+ 'exampleValue' => 'keyword1 keyword2…',
+ 'required' => true,
+ ),
+ 'category' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => array(
+ 'All' => 0,
+ 'Movies' => 1,
+ 'Music' => 2,
+ 'TV' => 3,
+ 'Games' => 4,
+ 'Applications' => 5,
+ 'Pictures' => 8,
+ 'Anime' => 9,
+ 'Comics' => 10,
+ 'Books' => 11,
+ 'Audiobooks' => 17
+ )
+ )
+ ), array(
+ 'catOnly' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => array(
+ 'All' => 0,
+ 'Movies' => 1,
+ 'Music' => 2,
+ 'TV' => 3,
+ 'Games' => 4,
+ 'Applications' => 5,
+ 'Pictures' => 8,
+ 'Anime' => 9,
+ 'Comics' => 10,
+ 'Books' => 11,
+ 'Audiobooks' => 17
+ )
+ )
+ ), array(
+ 'userid' => array(
+ 'name' => 'user id',
+ 'exampleValue' => '00000',
+ 'required' => true,
+ 'type' => 'number'
+ ),
+ 'category' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => array(
+ 'All' => 0,
+ 'Movies' => 1,
+ 'Music' => 2,
+ 'TV' => 3,
+ 'Games' => 4,
+ 'Applications' => 5,
+ 'Pictures' => 8,
+ 'Anime' => 9,
+ 'Comics' => 10,
+ 'Books' => 11,
+ 'Audiobooks' => 17
+ )
+ )
+ )
+ );
+
+ public function collectData() {
+
+ if(!empty($this->getInput('q'))) {
+
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'files/?category=' .
+ rawurlencode($this->getInput('category')) .
+ '&subcategory=All&quality=All&seeded=2&external=2&query=' .
+ urlencode($this->getInput('q')) .
+ '&uid=0&sort='
+ ) or returnServerError('Could not request Demonoid.');
+
+ } elseif(!empty($this->getInput('catOnly'))) {
+
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'files/?uid=0&category=' .
+ rawurlencode($this->getInput('catOnly')) .
+ '&subcategory=0&language=0&seeded=2&quality=0&query=&sort='
+ ) or returnServerError('Could not request Demonoid.');
+
+ } elseif(!empty($this->getInput('userid'))) {
+
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'files/?uid=' .
+ rawurlencode($this->getInput('userid')) .
+ '&seeded=2'
+ ) or returnServerError('Could not request Demonoid.');
+
+ } else {
+ returnServerError('Invalid parameters !');
+ }
+
+ if(preg_match('~No torrents found~', $html)) {
+ return;
+ }
+
+ $table = $html->find('td[class=ctable_content_no_pad]', 0);
+ $cursorCount = 4;
+ $elementCount = 0;
+ while($elementCount != 40) {
+ $elementCount++;
+ $currentElement = $table->find('tr', $cursorCount);
+ if(preg_match('~items total~', $currentElement)) {
+ break;
+ }
+ $item = array();
+ //Do we have a date ?
+ if(preg_match('~Added.*?(.*)~', $currentElement->plaintext, $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+~', $currentElement->plaintext, $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']);
+ }
+ $cursorCount++;
+ }
+
+ $content = $table->find('tr', $cursorCount)->find('a', 1);
+ $cursorCount++;
+ $torrentInfo = $table->find('tr', $cursorCount);
+ $item['timestamp'] = $timestamp;
+ $item['title'] = $content->plaintext;
+ $item['id'] = self::URI . $content->href;
+ $item['uri'] = self::URI . $content->href;
+ $item['author'] = $torrentInfo->find('a[class=user]', 0)->plaintext;
+ $item['seeders'] = $torrentInfo->find('font[class=green]', 0)->plaintext;
+ $item['leechers'] = $torrentInfo->find('font[class=red]', 0)->plaintext;
+ $item['size'] = $torrentInfo->find('td', 3)->plaintext;
+ $item['content'] = 'Uploaded by ' . $item['author']
+ . ' , Size ' . $item['size']
+ . '<br>seeders: '
+ . $item['seeders']
+ . ' | leechers: '
+ . $item['leechers']
+ . '<br><a href="'
+ . $item['id']
+ . '">info page</a>';
+
+ $this->items[] = $item;
+
+ $cursorCount++;
+ }
+ }
+}
diff --git a/bridges/DeveloppezDotComBridge.php b/bridges/DeveloppezDotComBridge.php
new file mode 100644
index 0000000..5719cf3
--- /dev/null
+++ b/bridges/DeveloppezDotComBridge.php
@@ -0,0 +1,47 @@
+<?php
+class DeveloppezDotComBridge extends FeedExpander {
+
+ const MAINTAINER = 'polopollo';
+ const NAME = 'Developpez.com Actus (FR)';
+ const URI = 'https://www.developpez.com/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns the 15 newest posts from DeveloppezDotCom (full text).';
+
+ public function collectData(){
+ $this->collectExpandableDatas(self::URI . 'index/rss', 15);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $item['content'] = $this->extractContent($item['uri']);
+ return $item;
+ }
+
+ // F***ing quotes from Microsoft Word badly encoded, here was the trick:
+ // http://stackoverflow.com/questions/1262038/how-to-replace-microsoft-encoded-quotes-in-php
+ private function convertSmartQuotes($string)
+ {
+ $search = array(chr(145),
+ chr(146),
+ chr(147),
+ chr(148),
+ chr(151));
+
+ $replace = array(
+ "'",
+ "'",
+ '"',
+ '"',
+ '-'
+ );
+
+ return str_replace($search, $replace, $string);
+ }
+
+ private function extractContent($url){
+ $articleHTMLContent = getSimpleHTMLDOMCached($url);
+ $text = $this->convertSmartQuotes($articleHTMLContent->find('div.content', 0)->innertext);
+ $text = utf8_encode($text);
+ return trim($text);
+ }
+}
diff --git a/bridges/DiceBridge.php b/bridges/DiceBridge.php
new file mode 100644
index 0000000..dc6ea15
--- /dev/null
+++ b/bridges/DiceBridge.php
@@ -0,0 +1,120 @@
+<?php
+class DiceBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'rogerdc';
+ const NAME = 'Dice Unofficial RSS';
+ const URI = 'https://www.dice.com/';
+ const DESCRIPTION = 'The Unofficial Dice RSS';
+ // const CACHE_TIMEOUT = 86400; // 1 day
+
+ const PARAMETERS = array(array(
+ 'for_one' => array(
+ 'name' => 'With at least one of the words',
+ 'required' => false,
+ ),
+ 'for_all' => array(
+ 'name' => 'With all of the words',
+ 'required' => false,
+ ),
+ 'for_exact' => array(
+ 'name' => 'With the exact phrase',
+ 'required' => false,
+ ),
+ 'for_none' => array(
+ 'name' => 'With none of these words',
+ 'required' => false,
+ ),
+ 'for_jt' => array(
+ 'name' => 'Within job title',
+ 'required' => false,
+ ),
+ 'for_com' => array(
+ 'name' => 'Within company name',
+ 'required' => false,
+ ),
+ 'for_loc' => array(
+ 'name' => 'City, State, or ZIP code',
+ 'required' => false,
+ ),
+ 'radius' => array(
+ 'name' => 'Radius in miles',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => array(
+ 'Exact Location' => 'El',
+ 'Within 5 miles' => '5',
+ 'Within 10 miles' => '10',
+ 'Within 20 miles' => '20',
+ 'Within 30 miles' => '0',
+ 'Within 40 miles' => '40',
+ 'Within 50 miles' => '50',
+ 'Within 75 miles' => '75',
+ 'Within 100 miles' => '100',
+ ),
+ 'defaultValue' => '0',
+ ),
+ 'jtype' => array(
+ 'name' => 'Job type',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => array(
+ 'Full-Time' => 'Full Time',
+ 'Part-Time' => 'Part Time',
+ 'Contract - Independent' => 'Contract Independent',
+ 'Contract - W2' => 'Contract W2',
+ 'Contract to Hire - Independent' => 'C2H Independent',
+ 'Contract to Hire - W2' => 'C2H W2',
+ 'Third Party - Contract - Corp-to-Corp' => 'Contract Corp-To-Corp',
+ 'Third Party - Contract to Hire - Corp-to-Corp' => 'C2H Corp-To-Corp',
+ ),
+ 'defaultValue' => 'Full Time',
+ ),
+ 'telecommute' => array(
+ 'name' => 'Telecommute',
+ 'type' => 'checkbox',
+ ),
+ ));
+
+ public function collectData() {
+ $uri = 'https://www.dice.com/jobs/advancedResult.html';
+ $uri .= '?for_one=' . urlencode($this->getInput('for_one'));
+ $uri .= '&for_all=' . urlencode($this->getInput('for_all'));
+ $uri .= '&for_exact=' . urlencode($this->getInput('for_exact'));
+ $uri .= '&for_none=' . urlencode($this->getInput('for_none'));
+ $uri .= '&for_jt=' . urlencode($this->getInput('for_jt'));
+ $uri .= '&for_com=' . urlencode($this->getInput('for_com'));
+ $uri .= '&for_loc=' . urlencode($this->getInput('for_loc'));
+ if ($this->getInput('jtype')) {
+ $uri .= '&jtype=' . urlencode($this->getInput('jtype'));
+ }
+ $uri .= '&sort=date&limit=100';
+ $uri .= '&radius=' . urlencode($this->getInput('radius'));
+ if ($this->getInput('telecommute')) {
+ $uri .= '&telecommute=true';
+ }
+
+ $html = getSimpleHTMLDOM($uri)
+ or returnServerError('Could not request Dice.');
+ foreach($html->find('div.complete-serp-result-div') as $element) {
+ $item = array();
+ // Title
+ $masterLink = $element->find('a[id^=position]', 0);
+ $item['title'] = $masterLink->title;
+ // URL
+ $uri = $masterLink->href;
+ // $uri = substr($uri, 0, strrpos($uri, '?'));
+ $item['uri'] = substr($uri, 0, strrpos($uri, '?'));
+ // ID
+ $item['id'] = $masterLink->value;
+ // Image
+ $image = $element->find('img', 0);
+ if ($image)
+ $item['image'] = $image->getAttribute('src');
+ // Content
+ $shortdesc = $element->find('.shortdesc', '0');
+ $shortdesc = ($shortdesc) ? $shortdesc->innertext : '';
+ $item['content'] = $shortdesc;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/DilbertBridge.php b/bridges/DilbertBridge.php
new file mode 100644
index 0000000..959a91a
--- /dev/null
+++ b/bridges/DilbertBridge.php
@@ -0,0 +1,36 @@
+<?php
+class DilbertBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'kranack';
+ const NAME = 'Dilbert Daily Strip';
+ const URI = 'http://dilbert.com';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'The Unofficial Dilbert Daily Comic Strip';
+
+ public function collectData(){
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request Dilbert: ' . $this->getURI());
+
+ foreach($html->find('section.comic-item') as $element) {
+
+ $img = $element->find('img', 0);
+ $link = $element->find('a', 0);
+ $comic = $img->src;
+ $title = $link->alt;
+ $url = $link->href;
+ $date = substr($url, 25);
+ if (empty($title))
+ $title = 'Dilbert Comic Strip on ' . $date;
+ $date = strtotime($date);
+
+ $item = array();
+ $item['uri'] = $url;
+ $item['title'] = $title;
+ $item['author'] = 'Scott Adams';
+ $item['timestamp'] = $date;
+ $item['content'] = '<img src="' . $comic . '" alt="' . $img->alt . '" />';
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/DiscogsBridge.php b/bridges/DiscogsBridge.php
new file mode 100644
index 0000000..9fe4f51
--- /dev/null
+++ b/bridges/DiscogsBridge.php
@@ -0,0 +1,112 @@
+<?php
+
+class DiscogsBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'DiscogsBridge';
+ const URI = 'https://www.discogs.com/';
+ const DESCRIPTION = 'Returns releases from discogs';
+ const PARAMETERS = array(
+ 'Artist Releases' => array(
+ 'artistid' => array(
+ 'name' => 'Artist ID',
+ 'type' => 'number',
+ )
+ ),
+ 'Label Releases' => array(
+ 'labelid' => array(
+ 'name' => 'Label ID',
+ 'type' => 'number',
+ )
+ ),
+ 'User Wantlist' => array(
+ 'username_wantlist' => array(
+ 'name' => 'Username',
+ 'type' => 'text',
+ )
+ ),
+ 'User Folder' => array(
+ 'username_folder' => array(
+ 'name' => 'Username',
+ 'type' => 'text',
+ ),
+ 'folderid' => array(
+ 'name' => 'Folder ID',
+ 'type' => 'number',
+ )
+ )
+ );
+
+ public function collectData() {
+
+ if(!empty($this->getInput('artistid')) || !empty($this->getInput('labelid'))) {
+
+ if(!empty($this->getInput('artistid'))) {
+ $data = getContents('https://api.discogs.com/artists/'
+ . $this->getInput('artistid')
+ . '/releases?sort=year&sort_order=desc')
+ or returnServerError('Unable to query discogs !');
+ } elseif(!empty($this->getInput('labelid'))) {
+ $data = getContents('https://api.discogs.com/labels/'
+ . $this->getInput('labelid')
+ . '/releases?sort=year&sort_order=desc')
+ or returnServerError('Unable to query discogs !');
+ }
+
+ $jsonData = json_decode($data, true);
+ foreach($jsonData['releases'] as $release) {
+
+ $item = array();
+ $item['author'] = $release['artist'];
+ $item['title'] = $release['title'];
+ $item['id'] = $release['id'];
+ $resId = array_key_exists('main_release', $release) ? $release['main_release'] : $release['id'];
+ $item['uri'] = self::URI . $this->getInput('artistid') . '/release/' . $resId;
+ $item['timestamp'] = DateTime::createFromFormat('Y', $release['year'])->getTimestamp();
+ $item['content'] = $item['author'] . ' - ' . $item['title'];
+ $this->items[] = $item;
+ }
+
+ } elseif(!empty($this->getInput('username_wantlist')) || !empty($this->getInput('username_folder'))) {
+
+ if(!empty($this->getInput('username_wantlist'))) {
+ $data = getContents('https://api.discogs.com/users/'
+ . $this->getInput('username_wantlist')
+ . '/wants?sort=added&sort_order=desc')
+ or returnServerError('Unable to query discogs !');
+ $jsonData = json_decode($data, true)['wants'];
+
+ } elseif(!empty($this->getInput('username_folder'))) {
+ $data = getContents('https://api.discogs.com/users/'
+ . $this->getInput('username_folder')
+ . '/collection/folders/'
+ . $this->getInput('folderid')
+ .'/releases?sort=added&sort_order=desc')
+ or returnServerError('Unable to query discogs !');
+ $jsonData = json_decode($data, true)['releases'];
+ }
+ foreach($jsonData as $element) {
+
+ $infos = $element['basic_information'];
+ $item = array();
+ $item['title'] = $infos['title'];
+ $item['author'] = $infos['artists'][0]['name'];
+ $item['id'] = $infos['artists'][0]['id'];
+ $item['uri'] = self::URI . $infos['artists'][0]['id'] . '/release/' . $infos['id'];
+ $item['timestamp'] = strtotime($element['date_added']);
+ $item['content'] = $item['author'] . ' - ' . $item['title'];
+ $this->items[] = $item;
+
+ }
+ }
+
+ }
+
+ public function getURI() {
+ return self::URI;
+ }
+
+ public function getName() {
+ return static::NAME;
+ }
+}
diff --git a/bridges/DollbooruBridge.php b/bridges/DollbooruBridge.php
new file mode 100644
index 0000000..5ed4119
--- /dev/null
+++ b/bridges/DollbooruBridge.php
@@ -0,0 +1,9 @@
+<?php
+require_once('Shimmie2Bridge.php');
+
+class DollbooruBridge extends Shimmie2Bridge {
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Dollbooru';
+ const URI = 'http://dollbooru.org/';
+ const DESCRIPTION = 'Returns images from given page';
+}
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/DuckDuckGoBridge.php b/bridges/DuckDuckGoBridge.php
new file mode 100644
index 0000000..8533be5
--- /dev/null
+++ b/bridges/DuckDuckGoBridge.php
@@ -0,0 +1,42 @@
+<?php
+class DuckDuckGoBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Astalaseven';
+ const NAME = 'DuckDuckGo';
+ const URI = 'https://duckduckgo.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns results from DuckDuckGo.';
+
+ const SORT_DATE = '+sort:date';
+ const SORT_RELEVANCE = '';
+
+ const PARAMETERS = array( array(
+ 'u' => array(
+ 'name' => 'keyword',
+ 'required' => true
+ ),
+ 'sort' => array(
+ 'name' => 'sort by',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => array(
+ 'date' => self::SORT_DATE,
+ 'relevance' => self::SORT_RELEVANCE
+ ),
+ 'defaultValue' => self::SORT_DATE
+ )
+ ));
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI . 'html/?kd=-1&q=' . $this->getInput('u') . $this->getInput('sort'))
+ or returnServerError('Could not request DuckDuckGo.');
+
+ foreach($html->find('div.results_links') as $element) {
+ $item = array();
+ $item['uri'] = $element->find('a', 0)->href;
+ $item['title'] = $element->find('a', 1)->innertext;
+ $item['content'] = $element->find('div.snippet', 0)->plaintext;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/ETTVBridge.php b/bridges/ETTVBridge.php
new file mode 100644
index 0000000..ab90bf7
--- /dev/null
+++ b/bridges/ETTVBridge.php
@@ -0,0 +1,142 @@
+<?php
+class ETTVBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'GregThib';
+ const NAME = 'ETTV';
+ const URI = 'https://www.ettv.tv/';
+ const DESCRIPTION = 'Returns list of 20 latest torrents for a specific search.';
+ const CACHE_TIMEOUT = 14400; // 4 hours
+
+ const PARAMETERS = array( array(
+ 'query' => array(
+ 'name' => 'Keywords',
+ 'required' => true
+ ),
+ 'cat' => array(
+ 'type' => 'list',
+ 'name' => 'Category',
+ 'values' => array(
+ '(ALL TYPES)' => '0',
+ 'Anime: Movies' => '73',
+ 'Anime: Dubbed/Subbed' => '74',
+ 'Anime: Others' => '75',
+ 'Books: Ebooks' => '53',
+ 'Books: Magazines' => '54',
+ 'Books: Comics' => '55',
+ 'Books: Audio' => '56',
+ 'Books: Others' => '68',
+ 'Games: Windows' => '57',
+ 'Games: Android' => '58',
+ 'Games: Others' => '71',
+ 'Movies: HD 1080p' => '1',
+ 'Movies: HD 720p' => '2',
+ 'Movies: UltraHD/4K' => '3',
+ 'Movies: XviD' => '42',
+ 'Movies: X264/H264' => '47',
+ 'Movies: 3D' => '49',
+ 'Movies: Dubs/Dual Audio' => '51',
+ 'Movies: CAM/TS' => '65',
+ 'Movies: BluRay Disc/Remux' => '66',
+ 'Movies: DVDR' => '67',
+ 'Movies: HEVC/x265' => '76',
+ 'Music: MP3' => '59',
+ 'Music: FLAC' => '60',
+ 'Music: Music Videos' => '61',
+ 'Music: Others' => '69',
+ 'Software: Windows' => '62',
+ 'Software: Android' => '63',
+ 'Software: Mac' => '64',
+ 'Software: Others' => '70',
+ 'TV: HD/X264/H264' => '41',
+ 'TV: SD/X264/H264' => '5',
+ 'TV: TV Packs' => '7',
+ 'TV: SD/XVID' => '50',
+ 'TV: Sport' => '72',
+ 'TV: HEVC/x265' => '77',
+ 'Unsorted: Unsorted' => '78'
+ ),
+ 'defaultValue' => '(ALL TYPES)'
+ ),
+ 'status' => array(
+ 'type' => 'list',
+ 'name' => 'Status',
+ 'values' => array(
+ 'Active Transfers' => '0',
+ 'Included Dead' => '1',
+ 'Only Dead' => '2'
+ ),
+ 'defaultValue' => 'Included Dead'
+ ),
+ 'lang' => array(
+ 'type' => 'list',
+ 'name' => 'Lang',
+ 'values' => array(
+ '(ALL)' => '0',
+ 'Arabic' => '17',
+ 'Chinese ' => '10',
+ 'Danish' => '13',
+ 'Dutch' => '11',
+ 'English' => '1',
+ 'Finnish' => '18',
+ 'French' => '2',
+ 'German' => '3',
+ 'Greek' => '15',
+ 'Hindi' => '8',
+ 'Italian' => '4',
+ 'Japanese' => '5',
+ 'Korean' => '9',
+ 'Polish' => '14',
+ 'Russian' => '7',
+ 'Spanish' => '6',
+ 'Turkish' => '16'
+ ),
+ 'defaultValue' => '(ALL)'
+ )
+ ));
+
+ public function collectData(){
+ // No control on inputs, because all have defaultValue set
+ $query_str = 'torrents-search.php';
+ $query_str .= '?search=' . urlencode('+'.str_replace(' ', ' +', $this->getInput('query')));
+ $query_str .= '&cat=' . $this->getInput('cat');
+ $query_str .= 'incldead&=' . $this->getInput('status');
+ $query_str .= '&lang=' . $this->getInput('lang');
+ $query_str .= '&sort=id&order=desc';
+
+ // Get results page
+ $html = getSimpleHTMLDOM(self::URI . $query_str)
+ or returnServerError('Could not request ' . $this->getName());
+
+ // Loop on each entry
+ foreach($html->find('table.table tr') as $element) {
+ if($element->parent->tag == 'thead') continue;
+ $entry = $element->find('td', 1)->find('a', 0);
+
+ // retrieve result page to get more details
+ $link = rtrim(self::URI, '/') . $entry->href;
+ $page = getSimpleHTMLDOM($link)
+ or returnServerError('Could not request page ' . $link);
+
+ // get details & download links
+ $details = $page->find('fieldset.download table', 0); // WHAT?? It should be the second one…
+ $dllinks = $page->find('div#downloadbox table', 0);
+
+ // fill item
+ $item = array();
+ $item['author'] = $details->children(6)->children(1)->plaintext;
+ $item['title'] = $entry->title;
+ $item['uri'] = $dllinks->children(0)->children(0)->children(0)->href;
+ $item['timestamp'] = strtotime($details->children(7)->children(1)->plaintext);
+ $item['content'] = '';
+ $item['content'] .= '<br/><b>Name: </b>' . $details->children(0)->children(1)->innertext;
+ $item['content'] .= '<br/><b>Lang: </b>' . $details->children(3)->children(1)->innertext;
+ $item['content'] .= '<br/><b>Size: </b>' . $details->children(4)->children(1)->innertext;
+ $item['content'] .= '<br/><b>Hash: </b>' . $details->children(5)->children(1)->innertext;
+ foreach($dllinks->children(0)->children(1)->find('a') as $dl) {
+ $item['content'] .= '<br/>' . $dl->outertext;
+ }
+ $item['content'] .= '<br/><br/>' . $details->children(1)->children(0)->innertext;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/EZTVBridge.php b/bridges/EZTVBridge.php
new file mode 100644
index 0000000..c016ff3
--- /dev/null
+++ b/bridges/EZTVBridge.php
@@ -0,0 +1,67 @@
+<?php
+class EZTVBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'alexAubin';
+ const NAME = 'EZTV';
+ const URI = 'https://eztv.ch/';
+ const DESCRIPTION = 'Returns list of *recent* torrents for a specific show
+on EZTV. Get showID from URLs in https://eztv.ch/shows/showID/show-full-name.';
+
+ const PARAMETERS = array( array(
+ 'i' => array(
+ 'name' => 'Show ids',
+ 'exampleValue' => 'showID1,showID2,…',
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+
+ // Make timestamp from relative released time in table
+ function makeTimestamp($relativeReleaseTime){
+
+ $relativeDays = 0;
+ $relativeHours = 0;
+
+ foreach(explode(' ', $relativeReleaseTime) as $relativeTimeElement) {
+ if(substr($relativeTimeElement, -1) == 'd') $relativeDays = substr($relativeTimeElement, 0, -1);
+ if(substr($relativeTimeElement, -1) == 'h') $relativeHours = substr($relativeTimeElement, 0, -1);
+ }
+ return mktime(date('h') - $relativeHours, 0, 0, date('m'), date('d') - $relativeDays, date('Y'));
+ }
+
+ // Loop on show ids
+ $showList = explode(',', $this->getInput('i'));
+ foreach($showList as $showID) {
+
+ // Get show page
+ $html = getSimpleHTMLDOM(self::URI . 'shows/' . rawurlencode($showID) . '/')
+ or returnServerError('Could not request EZTV for id "' . $showID . '"');
+
+ // Loop on each element that look like an episode entry...
+ foreach($html->find('.forum_header_border') as $element) {
+
+ // Filter entries that are not episode entries
+ $ep = $element->find('td', 1);
+ if(empty($ep)) continue;
+ $epinfo = $ep->find('.epinfo', 0);
+ $released = $element->find('td', 3);
+ if(empty($epinfo)) continue;
+ if(empty($released->plaintext)) continue;
+
+ // Filter entries that are older than 1 week
+ if($released->plaintext == '&gt;1 week') continue;
+
+ // Fill item
+ $item = array();
+ $item['uri'] = self::URI . $epinfo->href;
+ $item['id'] = $item['uri'];
+ $item['timestamp'] = makeTimestamp($released->plaintext);
+ $item['title'] = $epinfo->plaintext;
+ $item['content'] = $epinfo->alt;
+ if(isset($item['title']))
+ $this->items[] = $item;
+ }
+ }
+ }
+}
diff --git a/bridges/EliteDangerousGalnetBridge.php b/bridges/EliteDangerousGalnetBridge.php
new file mode 100644
index 0000000..86a1bbf
--- /dev/null
+++ b/bridges/EliteDangerousGalnetBridge.php
@@ -0,0 +1,35 @@
+<?php
+class EliteDangerousGalnetBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'corenting';
+ const NAME = 'Elite: Dangerous Galnet';
+ const URI = 'https://community.elitedangerous.com/galnet/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Returns the latest page of news from Galnet';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Error while downloading the website content');
+
+ foreach($html->find('div.article') as $element) {
+ $item = array();
+
+ $uri = $element->find('h3 a', 0)->href;
+ $uri = self::URI . substr($uri, strlen('/galnet/'));
+ $item['uri'] = $uri;
+
+ $title = $element->find('h3 a', 0)->plaintext;
+ $item['title'] = substr($title, 1); //remove the space between icon and title
+
+ $content = $element->find('p', -1)->innertext;
+ $item['content'] = $content;
+
+ $date = $element->find('p.small', 0)->innertext;
+ $article_year = substr($date, -4) - 1286; //Convert E:D date to actual date
+ $date = substr($date, 0, -4) . $article_year;
+ $item['timestamp'] = strtotime($date);
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/ElloBridge.php b/bridges/ElloBridge.php
new file mode 100644
index 0000000..c0e266e
--- /dev/null
+++ b/bridges/ElloBridge.php
@@ -0,0 +1,146 @@
+<?php
+class ElloBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'Ello Bridge';
+ const URI = 'https://ello.co/';
+ const CACHE_TIMEOUT = 4800; //2hours
+ const DESCRIPTION = 'Returns the newest posts for Ello';
+
+ const PARAMETERS = array(
+ 'By User' => array(
+ 'u' => array(
+ 'name' => 'Username',
+ 'required' => true,
+ 'title' => 'Username'
+ )
+ ),
+ 'Search' => array(
+ 's' => array(
+ 'name' => 'Search',
+ 'required' => true,
+ 'title' => 'Search'
+ )
+ )
+ );
+
+ public function collectData() {
+
+ $header = array(
+ 'Authorization: Bearer ' . $this->getAPIKey()
+ );
+
+ if(!empty($this->getInput('u'))) {
+ $postData = getContents(self::URI . 'api/v2/users/~' . urlencode($this->getInput('u')) . '/posts', $header) or
+ returnServerError('Unable to query Ello API.');
+ } else {
+ $postData = getContents(self::URI . 'api/v2/posts?terms=' . urlencode($this->getInput('s')), $header) or
+ returnServerError('Unable to query Ello API.');
+ }
+
+ $postData = json_decode($postData);
+ $count = 0;
+ foreach($postData->posts as $post) {
+
+ $item = array();
+ $item['author'] = $this->getUsername($post, $postData);
+ $item['timestamp'] = strtotime($post->created_at);
+ $item['title'] = $this->findText($post->summary);
+ $item['content'] = $this->getPostContent($post->body);
+ $item['enclosures'] = $this->getEnclosures($post, $postData);
+ $content = $post->body;
+
+ $this->items[] = $item;
+ $count += 1;
+
+ }
+
+ }
+
+ public function findText($path) {
+
+ foreach($path as $summaryElement) {
+
+ if($summaryElement->kind == 'text') {
+ return $summaryElement->data;
+ }
+
+ }
+
+ return '';
+
+ }
+
+ public function getPostContent($path) {
+
+ $content = '';
+ foreach($path as $summaryElement) {
+
+ if($summaryElement->kind == 'text') {
+ $content .= $summaryElement->data;
+ } elseif ($summaryElement->kind == 'image') {
+ $alt = '';
+ if(property_exists($summaryElement->data, 'alt')) {
+ $alt = $summaryElement->data->alt;
+ }
+ $content .= '<img src="' . $summaryElement->data->url . '" alt="' . $alt . '" />';
+ }
+
+ }
+
+ return $content;
+
+ }
+
+ public function getEnclosures($post, $postData) {
+
+ $assets = [];
+ foreach($post->links->assets as $asset) {
+ foreach($postData->linked->assets as $assetLink) {
+ if($asset == $assetLink->id) {
+ $assets[] = $assetLink->attachment->original->url;
+ break;
+ }
+ }
+ }
+
+ return $assets;
+
+ }
+
+ public function getUsername($post, $postData) {
+
+ foreach($postData->linked->users as $user) {
+ if($user->id == $post->links->author->id) {
+ return $user->username;
+ }
+ }
+
+ }
+
+ public function getAPIKey() {
+ $cache = Cache::create('FileCache');
+ $cache->setPath(CACHE_DIR);
+ $cache->setParameters(['key']);
+ $key = $cache->loadData();
+
+ if($key == null) {
+ $keyInfo = getContents(self::URI . 'api/webapp-token') or
+ returnServerError('Unable to get token.');
+ $key = json_decode($keyInfo)->token->access_token;
+ $cache->saveData($key);
+ }
+
+ return $key;
+
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('u'))) {
+ return $this->getInput('u') . ' - Ello Bridge';
+ }
+
+ return parent::getName();
+ }
+
+}
diff --git a/bridges/ElsevierBridge.php b/bridges/ElsevierBridge.php
new file mode 100644
index 0000000..f6ba7dd
--- /dev/null
+++ b/bridges/ElsevierBridge.php
@@ -0,0 +1,75 @@
+<?php
+class ElsevierBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Pierre Mazière';
+ const NAME = 'Elsevier journals recent articles';
+ const URI = 'http://www.journals.elsevier.com/';
+ const CACHE_TIMEOUT = 43200; //12h
+ const DESCRIPTION = 'Returns the recent articles published in Elsevier journals';
+
+ const PARAMETERS = array( array(
+ 'j' => array(
+ 'name' => 'Journal name',
+ 'required' => true,
+ 'exampleValue' => 'academic-pediactrics',
+ 'title' => 'Insert html-part of your journal'
+ )
+ ));
+
+ // Extracts the list of names from an article as string
+ private function extractArticleName($article){
+ $names = $article->find('small', 0);
+ if($names)
+ return trim($names->plaintext);
+ return '';
+ }
+
+ // Extracts the timestamp from an article
+ private function extractArticleTimestamp($article){
+ $time = $article->find('.article-info', 0);
+ if($time) {
+ $timestring = trim($time->plaintext);
+ /*
+ The format depends on the age of an article:
+ - Available online 29 July 2016
+ - July 2016
+ - May–June 2016
+ */
+ if(preg_match('/\S*(\d+\s\S+\s\d{4})/ims', $timestring, $matches)) {
+ return strtotime($matches[0]);
+ } elseif (preg_match('/[A-Za-z]+\-([A-Za-z]+\s\d{4})/ims', $timestring, $matches)) {
+ return strtotime($matches[0]);
+ } elseif (preg_match('/([A-Za-z]+\s\d{4})/ims', $timestring, $matches)) {
+ return strtotime($matches[0]);
+ } else {
+ return 0;
+ }
+ }
+ return 0;
+ }
+
+ // Extracts the content from an article
+ private function extractArticleContent($article){
+ $content = $article->find('.article-content', 0);
+ if($content) {
+ return trim($content->plaintext);
+ }
+ return '';
+ }
+
+ public function collectData(){
+ $uri = self::URI . $this->getInput('j') . '/recent-articles/';
+ $html = getSimpleHTMLDOM($uri)
+ or returnServerError('No results for Elsevier journal ' . $this->getInput('j'));
+
+ foreach($html->find('.pod-listing') as $article) {
+ $item = array();
+ $item['uri'] = $article->find('.pod-listing-header>a', 0)->getAttribute('href') . '?np=y';
+ $item['title'] = $article->find('.pod-listing-header>a', 0)->plaintext;
+ $item['author'] = $this->extractArticleName($article);
+ $item['timestamp'] = $this->extractArticleTimestamp($article);
+ $item['content'] = $this->extractArticleContent($article);
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/EstCeQuonMetEnProdBridge.php b/bridges/EstCeQuonMetEnProdBridge.php
new file mode 100644
index 0000000..db9d1d5
--- /dev/null
+++ b/bridges/EstCeQuonMetEnProdBridge.php
@@ -0,0 +1,37 @@
+<?php
+class EstCeQuonMetEnProdBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = 'Est-ce qu\'on met en prod aujourd\'hui ?';
+ const URI = 'https://www.estcequonmetenprodaujourdhui.info/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Should we put a website in production today? (French)';
+
+ public function collectData(){
+ function extractFromDelimiters($string, $start, $end){
+ if(strpos($string, $start) !== false) {
+ $section_retrieved = substr($string, strpos($string, $start) + strlen($start));
+ $section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
+ return $section_retrieved;
+ }
+
+ return false;
+ }
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request EstCeQuonMetEnProd: ' . $this->getURI());
+
+ $item = array();
+ $item['uri'] = $this->getURI() . '#' . date('Y-m-d');
+ $item['title'] = $this->getName();
+ $item['author'] = 'Nicolas Hoffmann';
+ $item['timestamp'] = strtotime('today midnight');
+ $item['content'] = str_replace(
+ 'src="/',
+ 'src="' . $this->getURI(),
+ trim(extractFromDelimiters($html->outertext, '<body role="document">', '<br /><br />'))
+ );
+
+ $this->items[] = $item;
+ }
+}
diff --git a/bridges/EtsyBridge.php b/bridges/EtsyBridge.php
new file mode 100644
index 0000000..311d910
--- /dev/null
+++ b/bridges/EtsyBridge.php
@@ -0,0 +1,83 @@
+<?php
+class EtsyBridge extends BridgeAbstract {
+
+ const NAME = 'Etsy search';
+ const URI = 'https://www.etsy.com';
+ const DESCRIPTION = 'Returns feeds for search results';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = array(
+ array(
+ 'query' => array(
+ 'name' => 'Search query',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert your search term here',
+ 'exampleValue' => 'Enter your search term'
+ ),
+ 'queryextension' => array(
+ 'name' => 'Query extension',
+ 'type' => 'text',
+ 'requied' => false,
+ 'title' => 'Insert additional query parts here
+(anything after ?search=<your search query>)',
+ 'exampleValue' => '&explicit=1&locationQuery=2921044'
+ ),
+ 'showimage' => array(
+ 'name' => 'Show image in content',
+ 'type' => 'checkbox',
+ 'requrired' => false,
+ 'title' => 'Activate to show the image in the content',
+ 'defaultValue' => false
+ )
+ )
+ );
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Failed to receive ' . $this->getURI());
+
+ $results = $html->find('div.block-grid-item');
+
+ foreach($results as $result) {
+ // Skip banner cards (ads for categories)
+ if($result->find('a.banner-card'))
+ continue;
+
+ $item = array();
+
+ $item['title'] = $result->find('a', 0)->title;
+ $item['uri'] = $result->find('a', 0)->href;
+ $item['author'] = $result->find('div.card-shop-name', 0)->plaintext;
+
+ $item['content'] = '<p>'
+ . $result->find('div.card-price', 0)->plaintext
+ . '</p><p>'
+ . $result->find('div.card-title', 0)->plaintext
+ . '</p>';
+
+ $image = $result->find('img.placeholder', 0)->src;
+
+ if($this->getInput('showimage')) {
+ $item['content'] .= '<img src="' . $image . '">';
+ }
+
+ $item['enclosures'] = array($image);
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('query'))) {
+ $uri = self::URI . '/search?q=' . urlencode($this->getInput('query'));
+
+ if(!is_null($this->getInput('queryextension'))) {
+ $uri .= $this->getInput('queryextension');
+ }
+
+ return $uri;
+ }
+
+ return parent::getURI();
+ }
+}
diff --git a/bridges/FB2Bridge.php b/bridges/FB2Bridge.php
new file mode 100644
index 0000000..1aeb30d
--- /dev/null
+++ b/bridges/FB2Bridge.php
@@ -0,0 +1,281 @@
+<?php
+class FB2Bridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'Facebook Alternate';
+ const URI = 'https://www.facebook.com/';
+ const CACHE_TIMEOUT = 1000;
+ const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
+ please insert the parameter as follow : myExamplePage/132621766841117';
+
+ const PARAMETERS = array( array(
+ 'u' => array(
+ 'name' => 'Username',
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+
+ function extractFromDelimiters($string, $start, $end){
+ if(strpos($string, $start) !== false) {
+ $section_retrieved = substr($string, strpos($string, $start) + strlen($start));
+ $section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
+ return $section_retrieved;
+ }
+
+ return false;
+ }
+
+ //Utility function for cleaning a Facebook link
+ $unescape_fb_link = function($matches){
+ if(is_array($matches) && count($matches) > 1) {
+ $link = $matches[1];
+ if(strpos($link, '/') === 0)
+ $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 . '"';
+ }
+ };
+
+ //Utility function for converting facebook emoticons
+ $unescape_fb_emote = function($matches){
+ static $facebook_emoticons = array(
+ 'smile' => ':)',
+ 'frown' => ':(',
+ 'tongue' => ':P',
+ 'grin' => ':D',
+ 'gasp' => ':O',
+ 'wink' => ';)',
+ 'pacman' => ':<',
+ 'grumpy' => '>_<',
+ 'unsure' => ':/',
+ 'cry' => ':\'(',
+ 'kiki' => '^_^',
+ 'glasses' => '8-)',
+ 'sunglasses' => 'B-)',
+ 'heart' => '<3',
+ 'devil' => ']:D',
+ 'angel' => '0:)',
+ 'squint' => '-_-',
+ 'confused' => 'o_O',
+ 'upset' => 'xD',
+ 'colonthree' => ':3',
+ 'like' => '&#x1F44D;');
+ $len = count($matches);
+ if ($len > 1)
+ for ($i = 1; $i < $len; $i++)
+ foreach ($facebook_emoticons as $name => $emote)
+ if ($matches[$i] === $name)
+ return $emote;
+ return $matches[0];
+ };
+
+ if($this->getInput('u') !== null) {
+ $page = 'https://touch.facebook.com/' . $this->getInput('u');
+ $cookies = $this->getCookies($page);
+ $pageID = $this->getPageID($page, $cookies);
+
+ if($pageID === null) {
+ echo <<<EOD
+Unable to get the page id. You should consider getting the ID by hand, then importing it into FB2Bridge
+EOD;
+ die();
+ } elseif($pageID == -1) {
+ echo <<<EOD
+This page is not accessible without being logged in.
+EOD;
+ die();
+ }
+ }
+
+ //Build the string for the first request
+ $requestString = 'https://touch.facebook.com/pages_reaction_units/more/?page_id='
+ . $pageID
+ . '&cursor={"card_id"%3A"videos"%2C"has_next_page"%3Atrue}&surface=mobile_page_home&unit_count=8';
+
+ $fileContent = getContents($requestString);
+
+ $articleIndex = 0;
+ $maxArticle = 3;
+
+ $html = $this->buildContent($fileContent);
+ $author = $this->getInput('u');
+
+ foreach($html->find('article') as $content) {
+
+ $item = array();
+
+ $item['uri'] = 'http://touch.facebook.com'
+ . $content->find("div[class='_52jc _5qc4 _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href');
+
+ if($content->find('header', 0) !== null) {
+ $content->find('header', 0)->innertext = '';
+ }
+
+ if($content->find('footer', 0) !== null) {
+ $content->find('footer', 0)->innertext = '';
+ }
+
+ //Remove html nodes, keep only img, links, basic formatting
+ $content = strip_tags($content, '<a><img><i><u><br><p>');
+
+ //Adapt link hrefs: convert relative links into absolute links and bypass external link redirection
+ $content = preg_replace_callback('/ href=\"([^"]+)\"/i', $unescape_fb_link, $content);
+
+ //Clean useless html tag properties and fix link closing tags
+ foreach (array(
+ 'onmouseover',
+ 'onclick',
+ 'target',
+ 'ajaxify',
+ 'tabindex',
+ 'class',
+ 'style',
+ 'data-[^=]*',
+ 'aria-[^=]*',
+ 'role',
+ 'rel',
+ 'id') as $property_name)
+ $content = preg_replace('/ ' . $property_name . '=\"[^"]*\"/i', '', $content);
+ $content = preg_replace('/<\/a [^>]+>/i', '</a>', $content);
+
+ //Convert textual representation of emoticons eg
+ // "<i><u>smile emoticon</u></i>" back to ASCII emoticons eg ":)"
+ $content = preg_replace_callback('/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i', $unescape_fb_emote, $content);
+
+ $item['content'] = $content;
+
+ $title = $author;
+ if (strlen($title) > 24)
+ $title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...';
+ $title = $title . ' | ' . strip_tags($content);
+ if (strlen($title) > 64)
+ $title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
+
+ $item['title'] = $title;
+ $item['author'] = $author;
+
+ array_push($this->items, $item);
+ }
+ }
+
+
+ // Currently not used. Is used to get more than only 3 elements, as they appear on another page.
+ private function computeNextLink($string, $pageID){
+
+ $regex = implode(
+ '',
+ array(
+ '/timeline_unit',
+ "\\\\\\\\u00253A1",
+ "\\\\\\\\u00253A([0-9]*)",
+ "\\\\\\\\u00253A([0-9]*)",
+ "\\\\\\\\u00253A([0-9]*)",
+ "\\\\\\\\u00253A([0-9]*)/"
+ )
+ );
+
+ preg_match($regex, $string, $result);
+
+ return implode(
+ '',
+ array(
+ 'https://touch.facebook.com/pages_reaction_units/more/?page_id=',
+ $pageID,
+ '&cursor=%7B%22timeline_cursor%22%3A%22timeline_unit%3A1%3A',
+ $result[1],
+ '%3A',
+ $result[2],
+ '%3A',
+ $result[3],
+ '%3A',
+ $result[4],
+ '%22%2C%22timeline_section_cursor%22%3A%7B%7D%2C%22',
+ 'has_next_page%22%3Atrue%7D&surface=mobile_page_home&unit_count=3'
+ )
+ );
+ }
+
+ //Builds the HTML from the encoded JS that Facebook provides.
+ private function buildContent($pageContent){
+ // The html ends with:
+ // /div>","replaceifexists
+ $regex = '/\\"html\\":(\".+\/div>"),"replace/';
+ preg_match($regex, $pageContent, $result);
+ return str_get_html(html_entity_decode(json_decode($result[1])));
+ }
+
+
+ //Builds the cookie from the page, as Facebook sometimes refuses to give
+ //the page if no cookie is provided.
+ private function getCookies($pageURL){
+
+ $ctx = stream_context_create(array(
+ 'http' => array(
+ 'user_agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
+ 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
+ )
+ )
+ );
+ $a = file_get_contents($pageURL, 0, $ctx);
+
+ //First request to get the cookie
+ $cookies = '';
+ foreach($http_response_header as $hdr) {
+ if(strpos($hdr, 'Set-Cookie') !== false) {
+ $cLine = explode(':', $hdr)[1];
+ $cLine = explode(';', $cLine)[0];
+ $cookies .= ';' . $cLine;
+ }
+ }
+
+ return substr($cookies, 1);
+ }
+
+ //Get the page ID from the Facebook page.
+ private function getPageID($page, $cookies){
+
+ $context = stream_context_create(array(
+ 'http' => array(
+ 'user_agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
+ 'header' => 'Cookie: ' . $cookies
+ )
+ )
+ );
+
+ $pageContent = file_get_contents($page, 0, $context);
+
+ if(strpos($pageContent, 'signup-button') != false) {
+ return -1;
+ }
+
+ //Get the page ID if we don't have a captcha
+ $regex = '/page_id=([0-9]*)&/';
+ preg_match($regex, $pageContent, $matches);
+
+ if(count($matches) > 0) {
+ return $matches[1];
+ }
+
+ //Get the page ID if we do have a captcha
+ $regex = '/"pageID":"([0-9]*)"/';
+ preg_match($regex, $pageContent, $matches);
+
+ return $matches[1];
+
+ }
+
+ public function getName(){
+ return (isset($this->name) ? $this->name . ' - ' : '') . 'Facebook Bridge';
+ }
+
+ public function getURI(){
+ return 'http://facebook.com';
+ }
+
+ public function getCacheDuration(){
+ return 60 * 60 * 3; // 5 minutes
+ }
+}
diff --git a/bridges/FDroidBridge.php b/bridges/FDroidBridge.php
new file mode 100644
index 0000000..a1a37ef
--- /dev/null
+++ b/bridges/FDroidBridge.php
@@ -0,0 +1,54 @@
+<?php
+class FDroidBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Mitsukarenai';
+ const NAME = 'F-Droid Bridge';
+ const URI = 'https://f-droid.org/';
+ const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours
+ const DESCRIPTION = 'Returns latest added/updated apps on the open-source Android apps repository F-Droid';
+
+ const PARAMETERS = array( array(
+ 'u' => array(
+ 'name' => 'Widget selection',
+ 'type' => 'list',
+ 'required' => true,
+ 'values' => array(
+ 'Latest added apps' => 'added',
+ 'Latest updated apps' => 'updated'
+ )
+ )
+ ));
+
+ public function collectData(){
+ $url = self::URI;
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Could not request F-Droid.');
+
+ // targetting the corresponding widget based on user selection
+ // "updated" is the 4th widget on the page, "added" is the 5th
+
+ switch($this->getInput('u')) {
+ case 'updated':
+ $html_widget = $html->find('div.sidebar-widget', 4);
+ break;
+ default:
+ $html_widget = $html->find('div.sidebar-widget', 5);
+ break;
+ }
+
+ // and now extracting app info from the selected widget (and yeah turns out icons are of heterogeneous sizes)
+
+ foreach($html_widget->find('a') as $element) {
+ $item = array();
+ $item['uri'] = self::URI . $element->href;
+ $item['title'] = $element->find('h4', 0)->plaintext;
+ $item['icon'] = $element->find('img', 0)->src;
+ $item['summary'] = $element->find('span.package-summary', 0)->plaintext;
+ $item['content'] = '
+ <a href="'.$item['uri'].'">
+ <img alt="" style="max-height:128px" src="'.$item['icon'].'">
+ </a><br>'.$item['summary'];
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/FacebookBridge.php b/bridges/FacebookBridge.php
new file mode 100644
index 0000000..c8ea0d7
--- /dev/null
+++ b/bridges/FacebookBridge.php
@@ -0,0 +1,575 @@
+<?php
+class FacebookBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene, logmanoriginal';
+ const NAME = 'Facebook';
+ const URI = 'https://www.facebook.com/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
+ please insert the parameter as follow : myExamplePage/132621766841117';
+
+ const PARAMETERS = array(
+ 'User' => array(
+ 'u' => array(
+ 'name' => 'Username',
+ 'required' => true
+ ),
+ 'media_type' => array(
+ 'name' => 'Media type',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => array(
+ 'All' => 'all',
+ 'Video' => 'video',
+ 'No Video' => 'novideo'
+ ),
+ 'defaultValue' => 'all'
+ ),
+ 'skip_reviews' => array(
+ 'name' => 'Skip reviews',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'defaultValue' => false,
+ 'title' => 'Feed includes reviews when checked'
+ )
+ ),
+ 'Group' => array(
+ 'g' => array(
+ 'name' => 'Group',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'https://www.facebook.com/groups/743149642484225',
+ 'title' => 'Insert group name or facebook group URL'
+ )
+ )
+ );
+
+ private $authorName = '';
+ private $groupName = '';
+
+ public function getURI() {
+ $uri = self::URI;
+
+ switch($this->queriedContext) {
+
+ case 'Group':
+ $uri .= 'groups/' . $this->sanitizeGroup(filter_var($this->getInput('g'), FILTER_SANITIZE_URL));
+ break;
+
+ }
+
+ return $uri .= '?_fb_noscript=1';
+ }
+
+ public function collectData() {
+
+ switch($this->queriedContext) {
+
+ case 'Group':
+ $this->collectGroupData();
+ break;
+
+ case 'User':
+ $this->collectUserData();
+ break;
+
+ default:
+ returnClientError('Unknown context: "' . $this->queriedContext . '"!');
+
+ }
+
+ }
+
+ #region Group
+
+ private function collectGroupData() {
+
+ $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n");
+
+ $html = getSimpleHTMLDOM($this->getURI(), $header)
+ or returnServerError('Failed loading facebook page: ' . $this->getURI());
+
+ if(!$this->isPublicGroup($html)) {
+ returnClientError('This group is not public! RSS-Bridge only supports public groups!');
+ }
+
+ defaultLinkTo($html, substr(self::URI, 0, strlen(self::URI) - 1));
+
+ $this->groupName = $this->extractGroupName($html);
+
+ $posts = $html->find('div.userContentWrapper')
+ or returnServerError('Failed finding posts!');
+
+ foreach($posts as $post) {
+
+ $item = array();
+
+ $item['uri'] = $this->extractGroupURI($post);
+ $item['title'] = $this->extractGroupTitle($post);
+ $item['author'] = $this->extractGroupAuthor($post);
+ $item['content'] = $this->extractGroupContent($post);
+ $item['timestamp'] = $this->extractGroupTimestamp($post);
+ $item['enclosures'] = $this->extractGroupEnclosures($post);
+
+ $this->items[] = $item;
+
+ }
+
+ }
+
+ private function sanitizeGroup($group) {
+
+ if(filter_var(
+ $group,
+ FILTER_VALIDATE_URL,
+ FILTER_FLAG_HOST_REQUIRED | FILTER_FLAG_PATH_REQUIRED)) {
+ // User provided a URL
+
+ $urlparts = parse_url($group);
+
+ if($urlparts['host'] !== parse_url(self::URI)['host']
+ && 'www.' . $urlparts['host'] !== parse_url(self::URI)['host']) {
+
+ returnClientError('The host you provided is invalid! Received "'
+ . $urlparts['host']
+ . '", expected "'
+ . parse_url(self::URI)['host']
+ . '"!');
+
+ }
+
+ return explode('/', $urlparts['path'])[2];
+
+ } elseif(strpos($group, '/') !== false) {
+ returnClientError('The group you provided is invalid: ' . $group);
+ } else {
+ return $group;
+ }
+
+ }
+
+ private function isPublicGroup($html) {
+
+ // Facebook redirects to the groups about page for non-public groups
+ $about = $html->find('#pagelet_group_about', 0);
+
+ return !($about);
+
+ }
+
+ private function extractGroupName($html) {
+
+ $ogtitle = $html->find('meta[property="og:title"]', 0)
+ or returnServerError('Unable to find group title!');
+
+ return htmlspecialchars_decode($ogtitle->content, ENT_QUOTES);
+
+ }
+
+ private function extractGroupURI($post) {
+
+ $elements = $post->find('a')
+ or returnServerError('Unable to find URI!');
+
+ foreach($elements as $anchor) {
+
+ // Find the one that is a permalink
+ if(strpos($anchor->href, 'permalink') !== false) {
+ return $anchor->href;
+ }
+
+ }
+
+ return null;
+
+ }
+
+ private function extractGroupContent($post) {
+
+ $content = $post->find('div.userContent', 0)
+ or returnServerError('Unable to find user content!');
+
+ return $content->innertext . $content->next_sibling()->innertext;
+
+ }
+
+ private function extractGroupTimestamp($post) {
+
+ $element = $post->find('abbr[data-utime]', 0)
+ or returnServerError('Unable to find timestamp!');
+
+ return $element->getAttribute('data-utime');
+
+ }
+
+ private function extractGroupAuthor($post) {
+
+ $element = $post->find('img', 0)
+ or returnServerError('Unable to find author information!');
+
+ return $element->{'aria-label'};
+
+ }
+
+ private function extractGroupEnclosures($post) {
+
+ $elements = $post->find('div.userContent', 0)->next_sibling()->find('img');
+
+ $enclosures = array();
+
+ foreach($elements as $enclosure) {
+ $enclosures[] = $enclosure->src;
+ }
+
+ return empty($enclosures) ? null : $enclosures;
+
+ }
+
+ private function extractGroupTitle($post) {
+
+ $element = $post->find('h5', 0)
+ or returnServerError('Unable to find title!');
+
+ if(strpos($element->plaintext, 'shared') === false) {
+
+ $content = strip_tags($this->extractGroupContent($post));
+
+ return $this->extractGroupAuthor($post)
+ . ' posted: '
+ . substr(
+ $content,
+ 0,
+ strpos(wordwrap($content, 64), "\n")
+ )
+ . '...';
+
+ }
+
+ return $element->plaintext;
+
+ }
+
+ #endregion
+
+ private function collectUserData(){
+
+ //Extract a string using start and end delimiters
+ function extractFromDelimiters($string, $start, $end){
+ if(strpos($string, $start) !== false) {
+ $section_retrieved = substr($string, strpos($string, $start) + strlen($start));
+ $section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
+ return $section_retrieved;
+ }
+
+ return false;
+ }
+
+ //Utility function for cleaning a Facebook link
+ $unescape_fb_link = function($matches){
+ if(is_array($matches) && count($matches) > 1) {
+ $link = $matches[1];
+ if(strpos($link, '/') === 0)
+ $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 . '"';
+ }
+ };
+
+ //Utility function for converting facebook emoticons
+ $unescape_fb_emote = function($matches){
+ static $facebook_emoticons = array(
+ 'smile' => ':)',
+ 'frown' => ':(',
+ 'tongue' => ':P',
+ 'grin' => ':D',
+ 'gasp' => ':O',
+ 'wink' => ';)',
+ 'pacman' => ':<',
+ 'grumpy' => '>_<',
+ 'unsure' => ':/',
+ 'cry' => ':\'(',
+ 'kiki' => '^_^',
+ 'glasses' => '8-)',
+ 'sunglasses' => 'B-)',
+ 'heart' => '<3',
+ 'devil' => ']:D',
+ 'angel' => '0:)',
+ 'squint' => '-_-',
+ 'confused' => 'o_O',
+ 'upset' => 'xD',
+ 'colonthree' => ':3',
+ 'like' => '&#x1F44D;');
+ $len = count($matches);
+ if ($len > 1)
+ for ($i = 1; $i < $len; $i++)
+ foreach ($facebook_emoticons as $name => $emote)
+ if ($matches[$i] === $name)
+ return $emote;
+ return $matches[0];
+ };
+
+ $html = null;
+
+ //Handle captcha response sent by the viewer
+ if (isset($_POST['captcha_response'])) {
+ if (session_status() == PHP_SESSION_NONE)
+ session_start();
+ if (isset($_SESSION['captcha_fields'], $_SESSION['captcha_action'])) {
+ $captcha_action = $_SESSION['captcha_action'];
+ $captcha_fields = $_SESSION['captcha_fields'];
+ $captcha_fields['captcha_response'] = preg_replace('/[^a-zA-Z0-9]+/', '', $_POST['captcha_response']);
+
+ $header = array("Content-type:
+application/x-www-form-urlencoded\r\nReferer: $captcha_action\r\nCookie: noscript=1\r\n");
+ $opts = array(
+ CURLOPT_POST => 1,
+ CURLOPT_POSTFIELDS => http_build_query($captcha_fields)
+ );
+
+ $html = getContents($captcha_action, $header, $opts);
+
+ if($html === false) {
+ returnServerError('Failed to submit captcha response back to Facebook');
+ }
+ unset($_SESSION['captcha_fields']);
+ $html = str_get_html($html);
+ }
+ unset($_SESSION['captcha_fields']);
+ unset($_SESSION['captcha_action']);
+ }
+
+ //Retrieve page contents
+ if(is_null($html)) {
+ $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n");
+
+ // Check if the user provided a fully qualified URL
+ if (filter_var($this->getInput('u'), FILTER_VALIDATE_URL)) {
+
+ $urlparts = parse_url($this->getInput('u'));
+
+ if($urlparts['host'] !== parse_url(self::URI)['host']) {
+ returnClientError('The host you provided is invalid! Received "'
+ . $urlparts['host']
+ . '", expected "'
+ . parse_url(self::URI)['host']
+ . '"!');
+ }
+
+ if(!array_key_exists('path', $urlparts)
+ || $urlparts['path'] === '/') {
+ returnClientError('The URL you provided doesn\'t contain the user name!');
+ }
+
+ $user = explode('/', $urlparts['path'])[1];
+
+ $html = getSimpleHTMLDOM(self::URI . urlencode($user) . '?_fb_noscript=1', $header)
+ or returnServerError('No results for this query.');
+
+ } else {
+
+ // First character cannot be a forward slash
+ if(strpos($this->getInput('u'), '/') === 0) {
+ returnClientError('Remove leading slash "/" from the username!');
+ }
+
+ if(!strpos($this->getInput('u'), '/')) {
+ $html = getSimpleHTMLDOM(self::URI . urlencode($this->getInput('u')) . '?_fb_noscript=1', $header)
+ or returnServerError('No results for this query.');
+ } else {
+ $html = getSimpleHTMLDOM(self::URI . 'pages/' . $this->getInput('u') . '?_fb_noscript=1', $header)
+ or returnServerError('No results for this query.');
+ }
+
+ }
+ }
+
+ //Handle captcha form?
+ $captcha = $html->find('div.captcha_interstitial', 0);
+ if (!is_null($captcha)) {
+ //Save form for submitting after getting captcha response
+ if (session_status() == PHP_SESSION_NONE)
+ session_start();
+ $captcha_fields = array();
+ foreach ($captcha->find('input, button') as $input)
+ $captcha_fields[$input->name] = $input->value;
+ $_SESSION['captcha_fields'] = $captcha_fields;
+ $_SESSION['captcha_action'] = $captcha->find('form', 0)->action;
+
+ //Show captcha filling form to the viewer, proxying the captcha image
+ $img = base64_encode(getContents($captcha->find('img', 0)->src));
+ http_response_code(500);
+ header('Content-Type: text/html');
+ $message = <<<EOD
+<form method="post" action="?{$_SERVER['QUERY_STRING']}">
+ <h2>Facebook captcha challenge</h2>
+ <p>Unfortunately, rss-bridge cannot fetch the requested page.<br />
+ Facebook wants rss-bridge to resolve the following captcha:</p>
+ <p><img src="data:image/png;base64,{$img}" /></p>
+ <p><b>Response:</b> <input name="captcha_response" placeholder="please fill in" />
+ <input type="submit" value="Submit!" /></p>
+</form>
+EOD;
+ die($message);
+ }
+
+ //No captcha? We can carry on retrieving page contents :)
+ //First, we check wether the page is public or not
+ $loginForm = $html->find('._585r', 0);
+ if($loginForm != null) {
+ returnServerError('You must be logged in to view this page. This is not supported by RSS-Bridge.');
+ }
+
+ $element = $html
+ ->find('#pagelet_timeline_main_column')[0]
+ ->children(0)
+ ->children(0)
+ ->children(0)
+ ->next_sibling()
+ ->children(0);
+
+ if(isset($element)) {
+
+ $author = str_replace(' | Facebook', '', $html->find('title#pageTitle', 0)->innertext);
+ $profilePic = 'https://graph.facebook.com/'
+ . $this->getInput('u')
+ . '/picture?width=200&amp;height=200';
+
+ $this->authorName = $author;
+
+ foreach($element->children() as $cell) {
+ // Manage summary posts
+ if(strpos($cell->class, '_3xaf') !== false) {
+ $posts = $cell->children();
+ } else {
+ $posts = array($cell);
+ }
+
+ // Optionally skip reviews
+ if($this->getInput('skip_reviews')
+ && !is_null($cell->find('#review_composer_container', 0))) {
+ continue;
+ }
+
+ foreach($posts as $post) {
+ // Check media type
+ switch($this->getInput('media_type')) {
+ case 'all': break;
+ case 'video':
+ if(empty($post->find('[aria-label=Video]'))) continue 2;
+ break;
+ case 'novideo':
+ if(!empty($post->find('[aria-label=Video]'))) continue 2;
+ break;
+ default: break;
+ }
+
+ $item = array();
+
+ if(count($post->find('abbr')) > 0) {
+
+ //Retrieve post contents
+ $content = preg_replace(
+ '/(?i)><div class=\"clearfix([^>]+)>(.+?)div\ class=\"userContent\"/i',
+ '',
+ $post);
+
+ $content = preg_replace(
+ '/(?i)><div class=\"_59tj([^>]+)>(.+?)<\/div><\/div><a/i',
+ '',
+ $content);
+
+ $content = preg_replace(
+ '/(?i)><div class=\"_3dp([^>]+)>(.+?)div\ class=\"[^u]+userContent\"/i',
+ '',
+ $content);
+
+ $content = preg_replace(
+ '/(?i)><div class=\"_4l5([^>]+)>(.+?)<\/div>/i',
+ '',
+ $content);
+
+ //Remove html nodes, keep only img, links, basic formatting
+ $content = strip_tags($content, '<a><img><i><u><br><p>');
+
+ //Adapt link hrefs: convert relative links into absolute links and bypass external link redirection
+ $content = preg_replace_callback('/ href=\"([^"]+)\"/i', $unescape_fb_link, $content);
+
+ //Clean useless html tag properties and fix link closing tags
+ foreach (array(
+ 'onmouseover',
+ 'onclick',
+ 'target',
+ 'ajaxify',
+ 'tabindex',
+ 'class',
+ 'style',
+ 'data-[^=]*',
+ 'aria-[^=]*',
+ 'role',
+ 'rel',
+ 'id') as $property_name)
+ $content = preg_replace('/ ' . $property_name . '=\"[^"]*\"/i', '', $content);
+ $content = preg_replace('/<\/a [^>]+>/i', '</a>', $content);
+
+ //Convert textual representation of emoticons eg
+ //"<i><u>smile emoticon</u></i>" back to ASCII emoticons eg ":)"
+ $content = preg_replace_callback(
+ '/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i',
+ $unescape_fb_emote,
+ $content
+ );
+
+ //Retrieve date of the post
+ $date = $post->find('abbr')[0];
+ if(isset($date) && $date->hasAttribute('data-utime')) {
+ $date = $date->getAttribute('data-utime');
+ } else {
+ $date = 0;
+ }
+
+ //Build title from username and content
+ $title = $author;
+ if(strlen($title) > 24)
+ $title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...';
+ $title = $title . ' | ' . strip_tags($content);
+ 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'] = htmlspecialchars_decode($uri);
+ $item['content'] = htmlspecialchars_decode($content);
+ $item['title'] = $title;
+ $item['author'] = $author;
+ $item['timestamp'] = $date;
+ $this->items[] = $item;
+ }
+ }
+ }
+ }
+ }
+
+ public function getName(){
+
+ switch($this->queriedContext) {
+
+ case 'User':
+ if(!empty($this->authorName)) {
+ return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName
+ . ' - Facebook Bridge';
+ }
+ break;
+
+ case 'Group':
+ if(!empty($this->groupName)) {
+ return $this->groupName . ' - Facebook Bridge';
+ }
+ break;
+
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/FeedExpanderExampleBridge.php b/bridges/FeedExpanderExampleBridge.php
new file mode 100644
index 0000000..9d2f178
--- /dev/null
+++ b/bridges/FeedExpanderExampleBridge.php
@@ -0,0 +1,62 @@
+<?php
+class FeedExpanderExampleBridge extends FeedExpander {
+
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'FeedExpander Example';
+ const URI = '#';
+ const DESCRIPTION = 'Example bridge to test FeedExpander';
+
+ const PARAMETERS = array(
+ 'Feed' => array(
+ 'version' => array(
+ 'name' => 'Version',
+ 'type' => 'list',
+ 'required' => true,
+ 'title' => 'Select your feed format/version',
+ 'defaultValue' => 'RSS 2.0',
+ 'values' => array(
+ 'RSS 0.91' => 'rss_0_9_1',
+ 'RSS 1.0' => 'rss_1_0',
+ 'RSS 2.0' => 'rss_2_0',
+ 'ATOM 1.0' => 'atom_1_0'
+ )
+ )
+ )
+ );
+
+ public function collectData(){
+ switch($this->getInput('version')) {
+ case 'rss_0_9_1':
+ parent::collectExpandableDatas('http://static.userland.com/gems/backend/sampleRss.xml');
+ break;
+ case 'rss_1_0':
+ parent::collectExpandableDatas('http://feeds.nature.com/nature/rss/current?format=xml');
+ break;
+ case 'rss_2_0':
+ parent::collectExpandableDatas('http://feeds.rssboard.org/rssboard?format=xml');
+ break;
+ case 'atom_1_0':
+ parent::collectExpandableDatas('http://segfault.linuxmint.com/feed/atom/');
+ break;
+ default: returnClientError('Unknown version ' . $this->getInput('version') . '!');
+ }
+ }
+
+ protected function parseItem($newsItem) {
+ switch($this->getInput('version')) {
+ case 'rss_0_9_1':
+ return $this->parseRSS_0_9_1_Item($newsItem);
+ break;
+ case 'rss_1_0':
+ return $this->parseRSS_1_0_Item($newsItem);
+ break;
+ case 'rss_2_0':
+ return $this->parseRSS_2_0_Item($newsItem);
+ break;
+ case 'atom_1_0':
+ return $this->parseATOMItem($newsItem);
+ break;
+ default: returnClientError('Unknown version ' . $this->getInput('version') . '!');
+ }
+ }
+}
diff --git a/bridges/FierPandaBridge.php b/bridges/FierPandaBridge.php
new file mode 100644
index 0000000..cd9d11b
--- /dev/null
+++ b/bridges/FierPandaBridge.php
@@ -0,0 +1,24 @@
+<?php
+class FierPandaBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'snroki';
+ const NAME = 'Fier Panda Bridge';
+ const URI = 'http://www.fier-panda.fr/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns latest articles from Fier Panda.';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request Fier Panda.');
+
+ foreach($html->find('div.container-content article') as $element) {
+ $item = array();
+ $item['uri'] = $this->getURI() . $element->find('a', 0)->href;
+ $item['title'] = trim($element->find('h1 a', 0)->innertext);
+ // Remove the link at the end of the article
+ $element->find('p a', 0)->outertext = '';
+ $item['content'] = $element->find('p', 0)->innertext;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/FilterBridge.php b/bridges/FilterBridge.php
new file mode 100644
index 0000000..e8b451c
--- /dev/null
+++ b/bridges/FilterBridge.php
@@ -0,0 +1,77 @@
+<?php
+
+class FilterBridge extends FeedExpander {
+
+ const MAINTAINER = 'Frenzie';
+ const NAME = 'Filter';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Filters a feed of your choice';
+
+ const PARAMETERS = array(array(
+ 'url' => array(
+ 'name' => 'Feed URL',
+ 'required' => true,
+ ),
+ 'filter' => array(
+ 'name' => 'Filter item title (regular expression)',
+ 'required' => false,
+ ),
+ 'filter_type' => array(
+ 'name' => 'Filter type',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => array(
+ 'Permit' => 'permit',
+ 'Block' => 'block',
+ ),
+ 'defaultValue' => 'permit',
+ ),
+ ));
+
+ protected function parseItem($newItem){
+ $item = parent::parseItem($newItem);
+
+ switch(true) {
+ case $this->getFilterType() === 'permit':
+ if (preg_match($this->getFilter(), $item['title'])) {
+ return $item;
+ }
+ break;
+ case $this->getFilterType() === 'block':
+ if (!preg_match($this->getFilter(), $item['title'])) {
+ return $item;
+ }
+ break;
+ }
+ return null;
+ }
+
+ protected function getFilter(){
+ return '/' . $this->getInput('filter') . '/';
+ }
+
+ protected function getFilterType(){
+ return $this->getInput('filter_type');
+ }
+
+ public function getURI(){
+ $url = $this->getInput('url');
+
+ if(empty($url)) {
+ $url = parent::getURI();
+ }
+ return $url;
+ }
+
+ public function collectData(){
+ if($this->getInput('url') && substr($this->getInput('url'), 0, strlen('http')) !== 'http') {
+ // just in case someone find a way to access local files by playing with the url
+ returnClientError('The url parameter must either refer to http or https protocol.');
+ }
+ try{
+ $this->collectExpandableDatas($this->getURI());
+ } catch (HttpException $e) {
+ $this->collectExpandableDatas($this->getURI());
+ }
+ }
+}
diff --git a/bridges/FlickrBridge.php b/bridges/FlickrBridge.php
new file mode 100644
index 0000000..f5ebe9c
--- /dev/null
+++ b/bridges/FlickrBridge.php
@@ -0,0 +1,120 @@
+<?php
+
+/* This is a mashup of FlickrExploreBridge by sebsauvage and FlickrTagBridge
+ * by erwang, providing the functionality of both in one.
+ */
+class FlickrBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Flickr Bridge';
+ const URI = 'https://www.flickr.com/';
+ const CACHE_TIMEOUT = 21600; // 6 hours
+ const DESCRIPTION = 'Returns images from Flickr';
+
+ const PARAMETERS = array(
+ 'Explore' => array(),
+ 'By keyword' => array(
+ 'q' => array(
+ 'name' => 'Keyword',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert keyword',
+ 'exampleValue' => 'bird'
+ )
+ ),
+ 'By username' => array(
+ 'u' => array(
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert username (as shown in the address bar)',
+ 'exampleValue' => 'flickr'
+ )
+ ),
+ );
+
+ public function collectData(){
+ switch($this->queriedContext) {
+ case 'Explore':
+ $key = 'photos';
+ $html = getSimpleHTMLDOM(self::URI . 'explore')
+ or returnServerError('Could not request Flickr.');
+ break;
+ case 'By keyword':
+ $key = 'photos';
+ $html = getSimpleHTMLDOM(self::URI . 'search/?q=' . urlencode($this->getInput('q')) . '&s=rec')
+ or returnServerError('No results for this query.');
+ break;
+ case 'By username':
+ $key = 'photoPageList';
+ $html = getSimpleHTMLDOM(self::URI . 'photos/' . urlencode($this->getInput('u')))
+ or returnServerError('Requested username can\'t be found.');
+ break;
+ default:
+ returnClientError('Invalid context: ' . $this->queriedContext);
+ }
+
+ // Find SCRIPT containing JSON data
+ $model = $html->find('.modelExport', 0);
+ $model_text = $model->innertext;
+
+ // Find start and end of JSON data
+ $start = strpos($model_text, 'modelExport:') + strlen('modelExport:');
+ $end = strpos($model_text, 'auth:') - strlen('auth:');
+
+ // Dissect JSON data and remove trailing comma
+ $model_text = trim(substr($model_text, $start, $end - $start));
+ $model_text = substr($model_text, 0, strlen($model_text) - 1);
+
+ $model_json = json_decode($model_text, true);
+
+ foreach($html->find('.photo-list-photo-view') as $element) {
+ // Get the styles
+ $style = explode(';', $element->style);
+
+ // Get the background-image style
+ $backgroundImage = explode(':', end($style));
+
+ // URI type : url(//cX.staticflickr.com/X/XXXXX/XXXXXXXXX.jpg)
+ $imageURI = trim(str_replace(['url(', ')'], '', end($backgroundImage)));
+
+ // Get the image ID
+ $imageURIs = explode('_', basename($imageURI));
+ $imageID = reset($imageURIs);
+
+ // Use JSON data to build items
+ foreach(reset($model_json)[0][$key]['_data'] as $element) {
+ if($element['id'] === $imageID) {
+ $item = array();
+
+ /* Author name depends on scope. On a keyword search the
+ * author is part of the picture data. On a username search
+ * the author is part of the owner data.
+ */
+ if(array_key_exists('username', $element)) {
+ $item['author'] = $element['username'];
+ } elseif (array_key_exists('owner', reset($model_json)[0])) {
+ $item['author'] = reset($model_json)[0]['owner']['username'];
+ }
+
+ $item['title'] = (array_key_exists('title', $element) ? $element['title'] : 'Untitled');
+ $item['uri'] = self::URI . 'photo.gne?id=' . $imageID;
+
+ $description = (array_key_exists('description', $element) ? $element['description'] : '');
+
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $imageURI
+ . '" /></a><br><p>'
+ . $description
+ . '</p>';
+
+ $this->items[] = $item;
+
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/bridges/FootitoBridge.php b/bridges/FootitoBridge.php
new file mode 100644
index 0000000..22aead4
--- /dev/null
+++ b/bridges/FootitoBridge.php
@@ -0,0 +1,75 @@
+<?php
+class FootitoBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'superbaillot.net';
+ const NAME = 'Footito';
+ const URI = 'http://www.footito.fr/';
+ const DESCRIPTION = 'Footito';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request Footito.');
+
+ foreach($html->find('div.post') as $element) {
+ $item = array();
+
+ $content = trim($element->innertext);
+ $content = str_replace(
+ '<img',
+ "<img style='float : left;'",
+ $content );
+
+ $content = str_replace(
+ 'class="logo"',
+ "style='float : left;'",
+ $content );
+
+ $content = str_replace(
+ 'class="contenu"',
+ "style='margin-left : 60px;'",
+ $content );
+
+ $content = str_replace(
+ 'class="responsive-comment"',
+ "style='border-top : 1px #DDD solid; background-color : white; padding : 10px;'",
+ $content );
+
+ $content = str_replace(
+ 'class="jaime"',
+ "style='display : none;'",
+ $content );
+
+ $content = str_replace(
+ 'class="auteur-event responsive"',
+ "style='display : none;'",
+ $content );
+
+ $content = str_replace(
+ 'class="report-abuse-button"',
+ "style='display : none;'",
+ $content );
+
+ $content = str_replace(
+ 'class="reaction clearfix"',
+ "style='margin : 10px 0px; padding : 5px; border-bottom : 1px #DDD solid;'",
+ $content );
+
+ $content = str_replace(
+ 'class="infos"',
+ "style='font-size : 0.7em;'",
+ $content );
+
+ $item['content'] = $content;
+
+ $title = $element->find('.contenu .texte ', 0)->plaintext;
+ $item['title'] = $title;
+
+ $info = $element->find('div.infos', 0);
+
+ $item['timestamp'] = strtotime($info->find('time', 0)->datetime);
+ $item['author'] = $info->find('a.auteur', 0)->plaintext;
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/FourchanBridge.php b/bridges/FourchanBridge.php
new file mode 100644
index 0000000..ecd9d90
--- /dev/null
+++ b/bridges/FourchanBridge.php
@@ -0,0 +1,78 @@
+<?php
+class FourchanBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = '4chan';
+ const URI = 'https://boards.4chan.org/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns posts from the specified thread';
+
+ const PARAMETERS = array( array(
+ 'c' => array(
+ 'name' => 'Thread category',
+ 'required' => true
+ ),
+ 't' => array(
+ 'name' => 'Thread number',
+ 'type' => 'number',
+ 'required' => true
+ )
+ ));
+
+ public function getURI(){
+ if(!is_null($this->getInput('c')) && !is_null($this->getInput('t'))) {
+ return static::URI . $this->getInput('c') . '/thread/' . $this->getInput('t');
+ }
+
+ return parent::getURI();
+ }
+
+ public function collectData(){
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request 4chan, thread not found');
+
+ foreach($html->find('div.postContainer') as $element) {
+ $item = array();
+ $item['id'] = $element->find('.post', 0)->getAttribute('id');
+ $item['uri'] = $this->getURI() . '#' . $item['id'];
+ $item['timestamp'] = $element->find('span.dateTime', 0)->getAttribute('data-utc');
+ $item['author'] = $element->find('span.name', 0)->plaintext;
+
+ $file = $element->find('.file', 0);
+
+ if(!empty($file)) {
+ $item['image'] = $element->find('.file a', 0)->href;
+ $item['imageThumb'] = $element->find('.file img', 0)->src;
+ if(!isset($item['imageThumb']) and strpos($item['image'], '.swf') !== false)
+ $item['imageThumb'] = 'http://i.imgur.com/eO0cxf9.jpg';
+ }
+
+ if(!empty($element->find('span.subject', 0)->innertext)) {
+ $item['subject'] = $element->find('span.subject', 0)->innertext;
+ }
+
+ $item['title'] = 'reply ' . $item['id'] . ' | ' . $item['author'];
+ if(isset($item['subject'])) {
+ $item['title'] = $item['subject'] . ' - ' . $item['title'];
+ }
+
+ $content = $element->find('.postMessage', 0)->innertext;
+ $content = str_replace('href="#p', 'href="' . $this->getURI() . '#p', $content);
+ $item['content'] = '<span id="' . $item['id'] . '">' . $content . '</span>';
+
+ if(isset($item['image'])) {
+ $item['content'] = '<a href="'
+ . $item['image']
+ . '"><img alt="'
+ . $item['id']
+ . '" src="'
+ . $item['imageThumb']
+ . '" /></a><br>'
+ .$item['content'];
+ }
+ $this->items[] = $item;
+ }
+ $this->items = array_reverse($this->items);
+ }
+}
diff --git a/bridges/FuturaSciencesBridge.php b/bridges/FuturaSciencesBridge.php
new file mode 100644
index 0000000..b9479c3
--- /dev/null
+++ b/bridges/FuturaSciencesBridge.php
@@ -0,0 +1,173 @@
+<?php
+class FuturaSciencesBridge extends FeedExpander {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = 'Futura-Sciences Bridge';
+ const URI = 'http://www.futura-sciences.com/';
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ const PARAMETERS = array( array(
+ 'feed' => array(
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'values' => array(
+ 'Les flux multi-magazines' => array(
+ 'Les dernières actualités de Futura-Sciences' => 'actualites',
+ 'Les dernières définitions de Futura-Sciences' => 'definitions',
+ 'Les dernières photos de Futura-Sciences' => 'photos',
+ 'Les dernières questions - réponses de Futura-Sciences' => 'questions-reponses',
+ 'Les derniers dossiers de Futura-Sciences' => 'dossiers'
+ ),
+ 'Les flux Services' => array(
+ 'Les cartes virtuelles de Futura-Sciences' => 'services/cartes-virtuelles',
+ 'Les fonds d\'écran de Futura-Sciences' => 'services/fonds-ecran'
+ ),
+ 'Les flux Santé' => array(
+ 'Les dernières actualités de Futura-Santé' => 'sante/actualites',
+ 'Les dernières définitions de Futura-Santé' => 'sante/definitions',
+ 'Les dernières questions-réponses de Futura-Santé' => 'sante/question-reponses',
+ 'Les derniers dossiers de Futura-Santé' => 'sante/dossiers'
+ ),
+ 'Les flux High-Tech' => array(
+ 'Les dernières actualités de Futura-High-Tech' => 'high-tech/actualites',
+ 'Les dernières astuces de Futura-High-Tech' => 'high-tech/question-reponses',
+ 'Les dernières définitions de Futura-High-Tech' => 'high-tech/definitions',
+ 'Les derniers dossiers de Futura-High-Tech' => 'high-tech/dossiers'
+ ),
+ 'Les flux Espace' => array(
+ 'Les dernières actualités de Futura-Espace' => 'espace/actualites',
+ 'Les dernières définitions de Futura-Espace' => 'espace/definitions',
+ 'Les dernières questions-réponses de Futura-Espace' => 'espace/question-reponses',
+ 'Les derniers dossiers de Futura-Espace' => 'espace/dossiers'
+ ),
+ 'Les flux Environnement' => array(
+ 'Les dernières actualités de Futura-Environnement' => 'environnement/actualites',
+ 'Les dernières définitions de Futura-Environnement' => 'environnement/definitions',
+ 'Les dernières questions-réponses de Futura-Environnement' => 'environnement/question-reponses',
+ 'Les derniers dossiers de Futura-Environnement' => 'environnement/dossiers'
+ ),
+ 'Les flux Maison' => array(
+ 'Les dernières actualités de Futura-Maison' => 'maison/actualites',
+ 'Les dernières astuces de Futura-Maison' => 'maison/question-reponses',
+ 'Les dernières définitions de Futura-Maison' => 'maison/definitions',
+ 'Les derniers dossiers de Futura-Maison' => 'maison/dossiers'
+ ),
+ 'Les flux Nature' => array(
+ 'Les dernières actualités de Futura-Nature' => 'nature/actualites',
+ 'Les dernières définitions de Futura-Nature' => 'nature/definitions',
+ 'Les dernières questions-réponses de Futura-Nature' => 'nature/question-reponses',
+ 'Les derniers dossiers de Futura-Nature' => 'nature/dossiers'
+ ),
+ 'Les flux Terre' => array(
+ 'Les dernières actualités de Futura-Terre' => 'terre/actualites',
+ 'Les dernières définitions de Futura-Terre' => 'terre/definitions',
+ 'Les dernières questions-réponses de Futura-Terre' => 'terre/question-reponses',
+ 'Les derniers dossiers de Futura-Terre' => 'terre/dossiers'
+ ),
+ 'Les flux Matière' => array(
+ 'Les dernières actualités de Futura-Matière' => 'matiere/actualites',
+ 'Les dernières définitions de Futura-Matière' => 'matiere/definitions',
+ 'Les dernières questions-réponses de Futura-Matière' => 'matiere/question-reponses',
+ 'Les derniers dossiers de Futura-Matière' => 'matiere/dossiers'
+ ),
+ 'Les flux Mathématiques' => array(
+ 'Les dernières actualités de Futura-Mathématiques' => 'mathematiques/actualites',
+ 'Les derniers dossiers de Futura-Mathématiques' => 'mathematiques/dossiers'
+ )
+ )
+ )
+ ));
+
+ public function collectData(){
+ $url = self::URI . 'rss/' . $this->getInput('feed') . '.xml';
+ $this->collectExpandableDatas($url, 10);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $item['uri'] = str_replace('#xtor=RSS-8', '', $item['uri']);
+ $article = getSimpleHTMLDOMCached($item['uri'])
+ or returnServerError('Could not request Futura-Sciences: ' . $item['uri']);
+ $item['content'] = $this->extractArticleContent($article);
+ $author = $this->extractAuthor($article);
+ $item['author'] = empty($author) ? $item['author'] : $author;
+ return $item;
+ }
+
+ private function stripWithDelimiters($string, $start, $end){
+ while(strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ } return $string;
+ }
+
+ private function stripRecursiveHTMLSection($string, $tag_name, $tag_start){
+ $open_tag = '<' . $tag_name;
+ $close_tag = '</' . $tag_name . '>';
+ $close_tag_length = strlen($close_tag);
+ if(strpos($tag_start, $open_tag) === 0) {
+ while(strpos($string, $tag_start) !== false) {
+ $max_recursion = 100;
+ $section_to_remove = null;
+ $section_start = strpos($string, $tag_start);
+ $search_offset = $section_start;
+ do {
+ $max_recursion--;
+ $section_end = strpos($string, $close_tag, $search_offset);
+ $search_offset = $section_end + $close_tag_length;
+ $section_to_remove = substr($string, $section_start, $section_end - $section_start + $close_tag_length);
+ $open_tag_count = substr_count($section_to_remove, $open_tag);
+ $close_tag_count = substr_count($section_to_remove, $close_tag);
+ } while ($open_tag_count > $close_tag_count && $max_recursion > 0);
+ $string = str_replace($section_to_remove, '', $string);
+ }
+ }
+ return $string;
+ }
+
+ private function extractArticleContent($article){
+ $contents = $article->find('section.article-text-classic', 0)->innertext;
+ $headline = trim($article->find('p.description', 0)->plaintext);
+ if(!empty($headline))
+ $headline = '<p><b>' . $headline . '</b></p>';
+
+ foreach (array(
+ '<div class="clear',
+ '<div class="sharebar2',
+ '<div class="diaporamafullscreen"',
+ '<div class="module social-button',
+ '<div style="margin-bottom:10px;" class="noprint"',
+ '<div class="ficheprevnext',
+ '<div class="bar noprint',
+ '<div class="toolbar noprint',
+ '<div class="addthis_toolbox',
+ '<div class="noprint',
+ '<div class="bg bglight border border-full noprint',
+ '<div class="httplogbar-wrapper noprint',
+ '<div id="forumcomments',
+ '<div ng-if="active"'
+ ) as $div_start) {
+ $contents = $this->stripRecursiveHTMLSection($contents, 'div', $div_start);
+ }
+
+ $contents = $this->stripWithDelimiters($contents, '<hr ', '/>');
+ $contents = $this->stripWithDelimiters($contents, '<p class="content-date', '</p>');
+ $contents = $this->stripWithDelimiters($contents, '<h1 class="content-title', '</h1>');
+ $contents = $this->stripWithDelimiters($contents, 'fs:definition="', '"');
+ $contents = $this->stripWithDelimiters($contents, 'fs:xt:clicktype="', '"');
+ $contents = $this->stripWithDelimiters($contents, 'fs:xt:clickname="', '"');
+ $contents = $this->stripWithDelimiters($contents, '<script ', '</script>');
+
+ return $headline . trim($contents);
+ }
+
+ // Extracts the author from an article or element
+ private function extractAuthor($article){
+ $article_author = $article->find('h3.epsilon', 0);
+ if($article_author) {
+ return trim(str_replace(', Futura-Sciences', '', $article_author->plaintext));
+ }
+ return '';
+ }
+}
diff --git a/bridges/GBAtempBridge.php b/bridges/GBAtempBridge.php
new file mode 100644
index 0000000..f80a25c
--- /dev/null
+++ b/bridges/GBAtempBridge.php
@@ -0,0 +1,157 @@
+<?php
+class GBAtempBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = 'GBAtemp';
+ const URI = 'https://gbatemp.net/';
+ const DESCRIPTION = 'GBAtemp is a user friendly underground video game community.';
+
+ const PARAMETERS = array( array(
+ 'type' => array(
+ 'name' => 'Type',
+ 'type' => 'list',
+ 'required' => true,
+ 'values' => array(
+ 'News' => 'N',
+ 'Reviews' => 'R',
+ 'Tutorials' => 'T',
+ 'Forum' => 'F'
+ )
+ )
+ ));
+
+ private function extractFromDelimiters($string, $start, $end){
+ if(strpos($string, $start) !== false) {
+ $section_retrieved = substr($string, strpos($string, $start) + strlen($start));
+ $section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
+ return $section_retrieved;
+ }
+
+ return false;
+ }
+
+ private function stripWithDelimiters($string, $start, $end){
+ while(strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ }
+
+ return $string;
+ }
+
+ private function buildItem($uri, $title, $author, $timestamp, $content){
+ $item = array();
+ $item['uri'] = $uri;
+ $item['title'] = $title;
+ $item['author'] = $author;
+ $item['timestamp'] = $timestamp;
+ $item['content'] = $content;
+ return $item;
+ }
+
+ private function cleanupPostContent($content, $site_url){
+ $content = str_replace(':arrow:', '&#x27a4;', $content);
+ $content = str_replace('href="attachments/', 'href="'.$site_url.'attachments/', $content);
+ $content = $this->stripWithDelimiters($content, '<script', '</script>');
+ return $content;
+ }
+
+ private function fetchPostContent($uri, $site_url){
+ $html = getSimpleHTMLDOM($uri);
+ if(!$html) {
+ return 'Could not request GBAtemp ' . $uri;
+ }
+
+ $content = $html->find('div.messageContent', 0)->innertext;
+ return $this->cleanupPostContent($content, $site_url);
+ }
+
+ public function collectData(){
+
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request GBAtemp.');
+
+ switch($this->getInput('type')) {
+ case 'N':
+ foreach($html->find('li[class=news_item full]') as $newsItem) {
+ $url = self::URI . $newsItem->find('a', 0)->href;
+ $time = intval(
+ $this->extractFromDelimiters(
+ $newsItem->find('abbr.DateTime', 0)->outertext,
+ 'data-time="',
+ '"'
+ )
+ );
+ $author = $newsItem->find('a.username', 0)->plaintext;
+ $title = $newsItem->find('a', 1)->plaintext;
+ $content = $this->fetchPostContent($url, self::URI);
+ $this->items[] = $this->buildItem($url, $title, $author, $time, $content);
+ }
+ case 'R':
+ foreach($html->find('li.portal_review') as $reviewItem) {
+ $url = self::URI . $reviewItem->find('a', 0)->href;
+ $title = $reviewItem->find('span.review_title', 0)->plaintext;
+ $content = getSimpleHTMLDOM($url)
+ or returnServerError('Could not request GBAtemp: ' . $uri);
+ $author = $content->find('a.username', 0)->plaintext;
+ $time = intval(
+ $this->extractFromDelimiters(
+ $content->find('abbr.DateTime', 0)->outertext,
+ 'data-time="',
+ '"'
+ )
+ );
+ $intro = '<p><b>' . ($content->find('div#review_intro', 0)->plaintext) . '</b></p>';
+ $review = $content->find('div#review_main', 0)->innertext;
+ $subheader = '<p><b>' . $content->find('div.review_subheader', 0)->plaintext . '</b></p>';
+ $procons = $content->find('table.review_procons', 0)->outertext;
+ $scores = $content->find('table.reviewscores', 0)->outertext;
+ $content = $this->cleanupPostContent($intro . $review . $subheader . $procons . $scores, self::URI);
+ $this->items[] = $this->buildItem($url, $title, $author, $time, $content);
+ }
+ case 'T':
+ foreach($html->find('li.portal-tutorial') as $tutorialItem) {
+ $url = self::URI . $tutorialItem->find('a', 0)->href;
+ $title = $tutorialItem->find('a', 0)->plaintext;
+ $time = intval(
+ $this->extractFromDelimiters(
+ $tutorialItem->find('abbr.DateTime', 0)->outertext,
+ 'data-time="',
+ '"'
+ )
+ );
+ $author = $tutorialItem->find('a.username', 0)->plaintext;
+ $content = $this->fetchPostContent($url, self::URI);
+ $this->items[] = $this->buildItem($url, $title, $author, $time, $content);
+ }
+ case 'F':
+ foreach($html->find('li.rc_item') as $postItem) {
+ $url = self::URI . $postItem->find('a', 1)->href;
+ $title = $postItem->find('a', 1)->plaintext;
+ $time = intval(
+ $this->extractFromDelimiters(
+ $postItem->find('abbr.DateTime', 0)->outertext,
+ 'data-time="',
+ '"'
+ )
+ );
+ $author = $postItem->find('a.username', 0)->plaintext;
+ $content = $this->fetchPostContent($url, self::URI);
+ $this->items[] = $this->buildItem($url, $title, $author, $time, $content);
+ }
+ }
+ }
+
+ public function getName() {
+ if(!is_null($this->getInput('type'))) {
+ $type = array_search(
+ $this->getInput('type'),
+ self::PARAMETERS[$this->queriedContext]['type']['values']
+ );
+ return 'GBAtemp ' . $type . ' Bridge';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/GelbooruBridge.php b/bridges/GelbooruBridge.php
new file mode 100644
index 0000000..4fe30e2
--- /dev/null
+++ b/bridges/GelbooruBridge.php
@@ -0,0 +1,35 @@
+<?php
+require_once('DanbooruBridge.php');
+
+class GelbooruBridge extends DanbooruBridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Gelbooru';
+ const URI = 'http://gelbooru.com/';
+ const DESCRIPTION = 'Returns images from given page';
+
+ const PATHTODATA = '.thumb';
+ const IDATTRIBUTE = 'id';
+ const TAGATTRIBUTE = 'title';
+
+ const PIDBYPAGE = 63;
+
+ protected function getFullURI(){
+ return $this->getURI()
+ . 'index.php?page=post&s=list&pid='
+ . ($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/GiphyBridge.php b/bridges/GiphyBridge.php
new file mode 100644
index 0000000..26d1eba
--- /dev/null
+++ b/bridges/GiphyBridge.php
@@ -0,0 +1,76 @@
+<?php
+define('GIPHY_LIMIT', 10);
+
+class GiphyBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'kraoc';
+ const NAME = 'Giphy Bridge';
+ const URI = 'http://giphy.com/';
+ const CACHE_TIMEOUT = 300; //5min
+ const DESCRIPTION = 'Bridge for giphy.com';
+
+ const PARAMETERS = array( array(
+ 's' => array(
+ 'name' => 'search tag',
+ 'required' => true
+ ),
+ 'n' => array(
+ 'name' => 'max number of returned items',
+ 'type' => 'number'
+ )
+ ));
+
+ public function collectData(){
+ $html = '';
+ $base_url = 'http://giphy.com';
+ $html = getSimpleHTMLDOM(self::URI . '/search/' . urlencode($this->getInput('s') . '/'))
+ or returnServerError('No results for this query.');
+
+ $max = GIPHY_LIMIT;
+ if($this->getInput('n')) {
+ $max = $this->getInput('n');
+ }
+
+ $limit = 0;
+ $kw = urlencode($this->getInput('s'));
+ foreach($html->find('div.hoverable-gif') as $entry) {
+ if($limit < $max) {
+ $node = $entry->first_child();
+ $href = $node->getAttribute('href');
+
+ $html2 = getSimpleHTMLDOM(self::URI . $href)
+ or returnServerError('No results for this query.');
+ $figure = $html2->getElementByTagName('figure');
+ $img = $figure->firstChild();
+ $caption = $figure->lastChild();
+
+ $item = array();
+ $item['id'] = $img->getAttribute('data-gif_id');
+ $item['uri'] = $img->getAttribute('data-bitly_gif_url');
+ $item['username'] = 'Giphy - ' . ucfirst($kw);
+ $title = $caption->innertext();
+ $title = preg_replace('/\s+/', ' ', $title);
+ $title = str_replace('animated GIF', '', $title);
+ $title = str_replace($kw, '', $title);
+ $title = preg_replace('/\s+/', ' ', $title);
+ $title = trim($title);
+ if(strlen($title) <= 0) {
+ $title = $item['id'];
+ }
+ $item['title'] = trim($title);
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $img->getAttribute('src')
+ . '" width="'
+ . $img->getAttribute('data-original-width')
+ . '" height="'
+ . $img->getAttribute('data-original-height')
+ . '" /></a>';
+
+ $this->items[] = $item;
+ $limit++;
+ }
+ }
+ }
+}
diff --git a/bridges/GithubIssueBridge.php b/bridges/GithubIssueBridge.php
new file mode 100644
index 0000000..0ed775d
--- /dev/null
+++ b/bridges/GithubIssueBridge.php
@@ -0,0 +1,192 @@
+<?php
+class GithubIssueBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Pierre Mazière';
+ const NAME = 'Github Issue';
+ const URI = 'https://github.com/';
+ const CACHE_TIMEOUT = 600; // 10min
+ const DESCRIPTION = 'Returns the issues or comments of an issue of a github project';
+
+ const PARAMETERS = array(
+ 'global' => array(
+ 'u' => array(
+ 'name' => 'User name',
+ 'required' => true
+ ),
+ 'p' => array(
+ 'name' => 'Project name',
+ 'required' => true
+ )
+ ),
+ 'Project Issues' => array(
+ 'c' => array(
+ 'name' => 'Show Issues Comments',
+ 'type' => 'checkbox'
+ )
+ ),
+ 'Issue comments' => array(
+ 'i' => array(
+ 'name' => 'Issue number',
+ 'type' => 'number',
+ 'required' => 'true'
+ )
+ )
+ );
+
+ public function getName(){
+ $name = $this->getInput('u') . '/' . $this->getInput('p');
+ switch($this->queriedContext) {
+ case 'Project Issues':
+ if($this->getInput('c')) {
+ $prefix = static::NAME . 's comments for ';
+ } else {
+ $prefix = static::NAME . 's for ';
+ }
+ $name = $prefix . $name;
+ break;
+ case 'Issue comments':
+ $name = static::NAME . ' ' . $name . ' #' . $this->getInput('i');
+ break;
+ default: return parent::getName();
+ }
+ return $name;
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('u')) && !is_null($this->getInput('p'))) {
+ $uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p') . '/issues';
+ if($this->queriedContext === 'Issue comments') {
+ $uri .= '/' . $this->getInput('i');
+ } elseif($this->getInput('c')) {
+ $uri .= '?q=is%3Aissue+sort%3Aupdated-desc';
+ }
+ return $uri;
+ }
+
+ return parent::getURI();
+ }
+
+ protected function extractIssueComment($issueNbr, $title, $comment){
+ $class = $comment->getAttribute('class');
+ $classes = explode(' ', $class);
+ $event = false;
+ if(in_array('discussion-item', $classes)) {
+ $event = true;
+ }
+
+ $author = 'unknown';
+ if($comment->find('.author', 0)) {
+ $author = $comment->find('.author', 0)->plaintext;
+ }
+
+ $uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p') . '/issues/' . $issueNbr;
+
+ $comment = $comment->firstChild();
+ if(!$event) {
+ $comment = $comment->nextSibling();
+ }
+
+ if($event) {
+ $title .= ' / ' . substr($class, strpos($class, 'discussion-item-') + strlen('discussion-item-'));
+ if(!$comment->hasAttribute('id')) {
+ $items = array();
+ $timestamp = strtotime($comment->find('relative-time', 0)->getAttribute('datetime'));
+ $content = $comment->innertext;
+ while($comment = $comment->nextSibling()) {
+ $item = array();
+ $item['author'] = $author;
+ $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
+ $item['timestamp'] = $timestamp;
+ $item['content'] = $content . '<p>' . $comment->children(1)->innertext . '</p>';
+ $item['uri'] = $uri . '#' . $comment->children(1)->getAttribute('id');
+ $items[] = $item;
+ }
+ return $items;
+ }
+ $content = $comment->parent()->innertext;
+ } else {
+ $title .= ' / ' . trim($comment->firstChild()->plaintext);
+ $content = '<pre>' . $comment->find('.comment-body', 0)->innertext . '</pre>';
+ }
+
+ $item = array();
+ $item['author'] = $author;
+ $item['uri'] = $uri . '#' . $comment->getAttribute('id');
+ $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
+ $item['timestamp'] = strtotime($comment->find('relative-time', 0)->getAttribute('datetime'));
+ $item['content'] = $content;
+ return $item;
+ }
+
+ protected function extractIssueComments($issue){
+ $items = array();
+ $title = $issue->find('.gh-header-title', 0)->plaintext;
+ $issueNbr = trim(substr($issue->find('.gh-header-number', 0)->plaintext, 1));
+ $comments = $issue->find('.js-discussion', 0);
+ foreach($comments->children() as $comment) {
+ $classes = explode(' ', $comment->getAttribute('class'));
+ if(in_array('discussion-item', $classes)
+ || in_array('timeline-comment-wrapper', $classes)) {
+ $item = $this->extractIssueComment($issueNbr, $title, $comment);
+ if(array_keys($item) !== range(0, count($item) - 1)) {
+ $item = array($item);
+ }
+ $items = array_merge($items, $item);
+ }
+ }
+ return $items;
+ }
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('No results for Github Issue ' . $this->getURI());
+
+ switch($this->queriedContext) {
+ case 'Issue comments':
+ $this->items = $this->extractIssueComments($html);
+ break;
+ case 'Project Issues':
+ foreach($html->find('.js-active-navigation-container .js-navigation-item') as $issue) {
+ $info = $issue->find('.opened-by', 0);
+ $issueNbr = substr(trim($info->plaintext), 1, strpos(trim($info->plaintext), ' '));
+
+ $item = array();
+ $item['content'] = '';
+
+ if($this->getInput('c')) {
+ $uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p') . '/issues/' . $issueNbr;
+ $issue = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT);
+ if($issue) {
+ $this->items = array_merge($this->items, $this->extractIssueComments($issue));
+ continue;
+ }
+ $item['content'] = 'Can not extract comments from ' . $uri;
+ }
+
+ $item['author'] = $info->find('a', 0)->plaintext;
+ $item['timestamp'] = strtotime($info->find('relative-time', 0)->getAttribute('datetime'));
+ $item['title'] = html_entity_decode(
+ $issue->find('.js-navigation-open', 0)->plaintext,
+ ENT_QUOTES,
+ 'UTF-8'
+ );
+ $comments = $issue->find('.col-5', 0)->plaintext;
+ $item['content'] .= "\n" . 'Comments: ' . ($comments ? $comments : '0');
+ $item['uri'] = self::URI . $issue->find('.js-navigation-open', 0)->getAttribute('href');
+ $this->items[] = $item;
+ }
+ break;
+ }
+
+ array_walk($this->items, function(&$item){
+ $item['content'] = preg_replace('/\s+/', ' ', $item['content']);
+ $item['content'] = str_replace('href="/', 'href="' . static::URI, $item['content']);
+ $item['content'] = str_replace(
+ 'href="#',
+ 'href="' . substr($item['uri'], 0, strpos($item['uri'], '#') + 1),
+ $item['content']
+ );
+ $item['title'] = preg_replace('/\s+/', ' ', $item['title']);
+ });
+ }
+}
diff --git a/bridges/GithubSearchBridge.php b/bridges/GithubSearchBridge.php
new file mode 100644
index 0000000..d3a615b
--- /dev/null
+++ b/bridges/GithubSearchBridge.php
@@ -0,0 +1,50 @@
+<?php
+class GithubSearchBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'corenting';
+ const NAME = 'Github Repositories Search';
+ const URI = 'https://github.com/';
+ const CACHE_TIMEOUT = 600; // 10min
+ const DESCRIPTION = 'Returns a specified repositories search (sorted by recently updated)';
+ const PARAMETERS = array( array(
+ 's' => array(
+ 'type' => 'text',
+ 'name' => 'Search query'
+ )
+ ));
+
+ public function collectData(){
+ $params = array('utf8' => '✓',
+ 'q' => urlencode($this->getInput('s')),
+ 's' => 'updated',
+ 'o' => 'desc',
+ 'type' => 'Repositories');
+ $url = self::URI . 'search?' . http_build_query($params);
+
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Error while downloading the website content');
+
+ foreach($html->find('div.repo-list-item') as $element) {
+ $item = array();
+
+ $uri = $element->find('h3 a', 0)->href;
+ $uri = substr(self::URI, 0, -1) . $uri;
+ $item['uri'] = $uri;
+
+ $title = $element->find('h3', 0)->plaintext;
+ $item['title'] = $title;
+
+ if (count($element->find('p')) == 2) {
+ $content = $element->find('p', 0)->innertext;
+ } else{
+ $content = '';
+ }
+ $item['content'] = $content;
+
+ $date = $element->find('relative-time', 0)->datetime;
+ $item['timestamp'] = strtotime($date);
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/GizmodoBridge.php b/bridges/GizmodoBridge.php
new file mode 100644
index 0000000..35f162b
--- /dev/null
+++ b/bridges/GizmodoBridge.php
@@ -0,0 +1,36 @@
+<?php
+class GizmodoBridge extends FeedExpander {
+
+ const MAINTAINER = 'polopollo';
+ const NAME = 'Gizmodo';
+ const URI = 'http://gizmodo.com/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns the newest posts from Gizmodo (full text).';
+
+ protected function parseItem($item){
+ $item = parent::parseItem($item);
+
+ $articleHTMLContent = getSimpleHTMLDOMCached($item['uri']);
+ if(!$articleHTMLContent) {
+ $text = 'Could not load ' . $item['uri'];
+ } else {
+ $text = $articleHTMLContent->find('div.entry-content', 0)->innertext;
+ foreach($articleHTMLContent->find('pagespeed_iframe') as $element) {
+ $text .= '<p>link to a iframe (could be a video): <a href="'
+ . $element->src
+ . '">'
+ . $element->src
+ . '</a></p><br>';
+ }
+
+ $text = strip_tags($text, '<p><b><a><blockquote><img><em>');
+ }
+
+ $item['content'] = $text;
+ return $item;
+ }
+
+ public function collectData(){
+ $this->collectExpandableDatas('http://feeds.gawker.com/gizmodo/full');
+ }
+}
diff --git a/bridges/GoComicsBridge.php b/bridges/GoComicsBridge.php
new file mode 100644
index 0000000..3223d19
--- /dev/null
+++ b/bridges/GoComicsBridge.php
@@ -0,0 +1,61 @@
+<?php
+class GoComicsBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'sky';
+ const NAME = 'GoComics Unofficial RSS';
+ const URI = 'https://www.gocomics.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'The Unofficial GoComics RSS';
+ const PARAMETERS = array( array(
+ 'comicname' => array(
+ 'name' => 'comicname',
+ 'type' => 'text',
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request GoComics: ' . $this->getURI());
+
+ //Get info from first page
+ $author = preg_replace('/By /', '', $html->find('.media-subheading', 0)->plaintext);
+
+ $link = self::URI . $html->find('.gc-deck--cta-0', 0)->find('a', 0)->href;
+ for($i = 0; $i < 5; $i++) {
+
+ $item = array();
+
+ $page = getSimpleHTMLDOM($link)
+ or returnServerError('Could not request GoComics: ' . $link);
+ $imagelink = $page->find('.img-fluid', 1)->src;
+ $date = explode('/', $link);
+
+ $item['id'] = $imagelink;
+ $item['uri'] = $link;
+ $item['author'] = $author;
+ $item['title'] = 'GoComics ' . $this->getInput('comicname');
+ $item['timestamp'] = DateTime::createFromFormat('Ymd', $date[5] . $date[6] . $date[7])->getTimestamp();
+ $item['content'] = '<img src="' . $imagelink . '" />';
+
+ $link = self::URI . $page->find('.js-previous-comic', 0)->href;
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('comicname'))) {
+ return self::URI . urlencode($this->getInput('comicname'));
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('comicname'))) {
+ return $this->getInput('comicname') . ' - GoComics';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/GooglePlusPostBridge.php b/bridges/GooglePlusPostBridge.php
new file mode 100644
index 0000000..0fccc96
--- /dev/null
+++ b/bridges/GooglePlusPostBridge.php
@@ -0,0 +1,111 @@
+<?php
+class GooglePlusPostBridge extends BridgeAbstract{
+
+ protected $_title;
+ protected $_url;
+
+ const MAINTAINER = 'Grummfy';
+ const NAME = 'Google Plus Post Bridge';
+ const URI = 'https://plus.google.com/';
+ const CACHE_TIMEOUT = 600; //10min
+ const DESCRIPTION = 'Returns user public post (without API).';
+
+ const PARAMETERS = array( array(
+ 'username' => array(
+ 'name' => 'username or Id',
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $username = $this->getInput('username');
+
+ // Usernames start with a + if it's not an ID
+ if(!is_numeric($username) && substr($username, 0, 1) !== '+') {
+ $username = '+' . $username;
+ }
+
+ // get content parsed
+ $html = getSimpleHTMLDOMCached(self::URI . urlencode($username) . '/posts')
+ or returnServerError('No results for this query.');
+
+ // get title, url, ... there is a lot of intresting stuff in meta
+ $this->_title = $html->find('meta[property=og:title]', 0)->getAttribute('content');
+ $this->_url = $html->find('meta[property=og:url]', 0)->getAttribute('content');
+
+ // I don't even know where to start with this discusting html...
+ foreach($html->find('div[jsname=WsjYwc]') as $post) {
+ $item = array();
+
+ $item['author'] = $item['fullname'] = $post->find('div div div div a', 0)->innertext;
+ $item['id'] = $post->find('div div div', 0)->getAttribute('id');
+ $item['avatar'] = $post->find('div img', 0)->src;
+ $item['uri'] = self::URI . $post->find('div div div a', 1)->href;
+
+ $timestamp = $post->find('a.qXj2He span', 0);
+
+ if($timestamp) {
+ $item['timestamp'] = strtotime('+' . preg_replace(
+ '/[^0-9A-Za-z]/',
+ '',
+ $timestamp->getAttribute('aria-label')));
+ }
+
+ // hashtag to treat : https://plus.google.com/explore/tag
+ // $hashtags = array();
+ // foreach($post->find('a.d-s') as $hashtag){
+ // $hashtags[trim($hashtag->plaintext)] = self::URI . $hashtag->href;
+ // }
+
+ $item['content'] = '';
+
+ // avatar display
+ $item['content'] .= '<div style="float:left; margin: 0 0.5em 0.5em 0;"><a href="'
+ . self::URI
+ . urlencode($this->getInput('username'));
+
+ $item['content'] .= '"><img align="top" alt="'
+ . $item['author']
+ . '" src="'
+ . $item['avatar']
+ . '" /></a></div>';
+
+ $content = $post->find('div[jsname=EjRJtf]', 0);
+ // extract plaintext
+ $item['content_simple'] = $content->plaintext;
+ $item['title'] = substr($item['content_simple'], 0, 72) . '...';
+
+ // XXX ugly but I don't have any idea how to do a better stuff,
+ // str_replace on link doesn't work as expected and ask too many checks
+ foreach($content->find('a') as $link) {
+ $hasHttp = strpos($link->href, 'http');
+ $hasDoubleSlash = strpos($link->href, '//');
+
+ if((!$hasHttp && !$hasDoubleSlash)
+ || (false !== $hasHttp && strpos($link->href, 'http') != 0)
+ || (false === $hasHttp && false !== $hasDoubleSlash && $hasDoubleSlash != 0)) {
+ // skipp bad link, for some hashtag or other stuff
+ if(strpos($link->href, '/') == 0) {
+ $link->href = substr($link->href, 1);
+ }
+
+ $link->href = self::URI . $link->href;
+ }
+ }
+ $content = $content->innertext;
+
+ $item['content'] .= '<div style="margin-top: -1.5em">' . $content . '</div>';
+ $item['content'] = trim(strip_tags($item['content'], '<a><p><div><img>'));
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName(){
+ return $this->_title ?: 'Google Plus Post Bridge';
+ }
+
+ public function getURI(){
+ return $this->_url ?: parent::getURI();
+ }
+}
diff --git a/bridges/GoogleSearchBridge.php b/bridges/GoogleSearchBridge.php
new file mode 100644
index 0000000..2eb5841
--- /dev/null
+++ b/bridges/GoogleSearchBridge.php
@@ -0,0 +1,64 @@
+<?php
+/**
+* Returns the 100 most recent links in results in past year, sorting by date (most recent first).
+* Example:
+* http://www.google.com/search?q=sebsauvage&num=100&complete=0&tbs=qdr:y,sbd:1
+* complete=0&num=100 : get 100 results
+* qdr:y : in past year
+* sbd:1 : sort by date (will only work if qdr: is specified)
+*/
+class GoogleSearchBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'sebsauvage';
+ const NAME = 'Google search';
+ const URI = 'https://www.google.com/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns most recent results from Google search.';
+
+ const PARAMETERS = array(array(
+ 'q' => array(
+ 'name' => 'keyword',
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $html = '';
+
+ $html = getSimpleHTMLDOM(self::URI
+ . 'search?q='
+ . urlencode($this->getInput('q'))
+ .'&num=100&complete=0&tbs=qdr:y,sbd:1')
+ or returnServerError('No results for this query.');
+
+ $emIsRes = $html->find('div[id=ires]', 0);
+
+ if(!is_null($emIsRes)) {
+ foreach($emIsRes->find('div[class=g]') as $element) {
+
+ $item = array();
+
+ // Extract direct URL from google href (eg. /url?q=...)
+ $t = $element->find('a[href]', 0)->href;
+ $item['uri'] = '' . $t;
+ parse_str(parse_url($t, PHP_URL_QUERY), $parameters);
+ if(isset($parameters['q'])) {
+ $item['uri'] = $parameters['q'];
+ }
+
+ $item['title'] = $element->find('h3', 0)->plaintext;
+ $item['content'] = $element->find('span[class=st]', 0)->plaintext;
+
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('q'))) {
+ return $this->getInput('q') . ' - Google search';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/GrandComicsDatabaseBridge.php b/bridges/GrandComicsDatabaseBridge.php
new file mode 100644
index 0000000..2b2fdfe
--- /dev/null
+++ b/bridges/GrandComicsDatabaseBridge.php
@@ -0,0 +1,61 @@
+<?php
+class GrandComicsDatabaseBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'corenting';
+ const NAME = 'Grand Comics Database Bridge';
+ const URI = 'https://www.comics.org/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Returns the latest comics added to a series timeline';
+ const PARAMETERS = array( array(
+ 'series' => array(
+ 'name' => 'Series id (from the timeline URL)',
+ 'required' => true,
+ 'exampleValue' => '63051',
+ ),
+ ));
+
+ public function collectData(){
+
+ $url = self::URI . 'series/' . $this->getInput('series') . '/details/timeline/';
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Error while downloading the website content');
+
+ $table = $html->find('table', 0);
+ $list = array_reverse($table->find('[class^=row_even]'));
+ $seriesName = $html->find('span[id=series_name]', 0)->innertext;
+
+ // Get row headers
+ $rowHeaders = $table->find('th');
+ foreach($list as $article) {
+
+ // Skip empty rows
+ $emptyRow = $article->find('td.empty_month');
+ if (count($emptyRow) != 0) {
+ continue;
+ }
+
+ $rows = $article->find('td');
+ $key_date = $rows[0]->innertext;
+
+ // Get URL too
+ $uri = 'https://www.comics.org' . $article->find('a')[0]->href;
+
+ // Build content
+ $content = '';
+ for($i = 0; $i < count($rowHeaders); $i++) {
+ $headerItem = $rowHeaders[$i]->innertext;
+ $rowItem = $rows[$i]->innertext;
+ $content = $content . $headerItem . ': ' . $rowItem . '<br/>';
+ }
+
+ // Build final item
+ $item = array();
+ $item['title'] = $seriesName . ' - ' . $key_date;
+ $item['timestamp'] = strtotime($key_date);
+ $item['content'] = str_get_html($content);
+ $item['uri'] = $uri;
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/HDWallpapersBridge.php b/bridges/HDWallpapersBridge.php
new file mode 100644
index 0000000..cea6e34
--- /dev/null
+++ b/bridges/HDWallpapersBridge.php
@@ -0,0 +1,83 @@
+<?php
+class HDWallpapersBridge extends BridgeAbstract {
+ const MAINTAINER = 'nel50n';
+ const NAME = 'HD Wallpapers Bridge';
+ const URI = 'http://www.hdwallpapers.in/';
+ const CACHE_TIMEOUT = 43200; //12h
+ const DESCRIPTION = 'Returns the latests wallpapers from HDWallpapers';
+
+ const PARAMETERS = array( array(
+ 'c' => array(
+ 'name' => 'category',
+ 'defaultValue' => 'latest_wallpapers'
+ ),
+ 'm' => array(
+ 'name' => 'max number of wallpapers'
+ ),
+ 'r' => array(
+ 'name' => 'resolution',
+ 'defaultValue' => '1920x1200',
+ 'exampleValue' => '1920x1200, 1680x1050,…'
+ )
+ ));
+
+ public function collectData(){
+ $category = $this->category;
+ if(strrpos($category, 'wallpapers') !== strlen($category) - strlen('wallpapers')) {
+ $category .= '-desktop-wallpapers';
+ }
+
+ $num = 0;
+ $max = $this->getInput('m') ?: 14;
+ $lastpage = 1;
+
+ for($page = 1; $page <= $lastpage; $page++) {
+ $link = self::URI . '/' . $category . '/page/' . $page;
+ $html = getSimpleHTMLDOM($link)
+ or returnServerError('No results for this query.');
+
+ if($page === 1) {
+ preg_match('/page\/(\d+)$/', $html->find('.pagination a', -2)->href, $matches);
+ $lastpage = min($matches[1], ceil($max / 14));
+ }
+
+ foreach($html->find('.wallpapers .wall a') as $element) {
+ $thumbnail = $element->find('img', 0);
+
+ $item = array();
+ // http://www.hdwallpapers.in/download/yosemite_reflections-1680x1050.jpg
+ $item['uri'] = self::URI
+ . '/download'
+ . str_replace('wallpapers.html', $this->getInput('r') . '.jpg', $element->href);
+
+ $item['timestamp'] = time();
+ $item['title'] = $element->find('p', 0)->text();
+ $item['content'] = $item['title']
+ . '<br><a href="'
+ . $item['uri']
+ . '"><img src="'
+ . self::URI
+ . $thumbnail->src
+ . '" /></a>';
+
+ $this->items[] = $item;
+
+ $num++;
+ if ($num >= $max)
+ break 2;
+ }
+ }
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('c')) && !is_null($this->getInput('r'))) {
+ return 'HDWallpapers - '
+ . str_replace(['__', '_'], [' & ', ' '], $this->getInput('c'))
+ . ' ['
+ . $this->getInput('r')
+ . ']';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/HentaiHavenBridge.php b/bridges/HentaiHavenBridge.php
new file mode 100644
index 0000000..21a0ff5
--- /dev/null
+++ b/bridges/HentaiHavenBridge.php
@@ -0,0 +1,37 @@
+<?php
+class HentaiHavenBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'albirew';
+ const NAME = 'Hentai Haven';
+ const URI = 'http://hentaihaven.org/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns releases from Hentai Haven';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request Hentai Haven.');
+
+ foreach($html->find('div.zoe-grid') as $element) {
+ $item = array();
+ $item['uri'] = $element->find('div.brick-content h3 a', 0)->href;
+ $thumbnailUri = $element->find('a.thumbnail-image img', 0)->getAttribute('data-src');
+ $item['title'] = mb_convert_encoding(
+ trim($element->find('div.brick-content h3 a', 0)->innertext),
+ 'UTF-8',
+ 'HTML-ENTITIES'
+ );
+
+ $item['tags'] = $element->find('div.oFlyout_bg div.oFlyout div.flyoutContent span.tags', 0)->plaintext;
+ $item['content'] = 'Tags: '
+ . $item['tags']
+ . '<br><br><a href="'
+ . $item['uri']
+ . '"><img width="300" height="169" src="'
+ . $thumbnailUri
+ . '" /></a><br>'
+ . $element->find('div.oFlyout_bg div.oFlyout div.flyoutContent p.description', 0)->innertext;
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/HotUKDealsBridge.php b/bridges/HotUKDealsBridge.php
new file mode 100644
index 0000000..a7eebf6
--- /dev/null
+++ b/bridges/HotUKDealsBridge.php
@@ -0,0 +1,1397 @@
+<?php
+
+require_once(__DIR__ . '/DealabsBridge.php');
+class HotUKDealsBridge extends PepperBridgeAbstract {
+
+ const NAME = 'HotUKDeals bridge';
+ const URI = 'https://www.hotukdeals.com/';
+ const DESCRIPTION = 'Return the HotUKDeals search result using keywords';
+ const MAINTAINER = 'sysadminstory';
+ const PARAMETERS = array(
+ 'Search by keyword(s))' => array (
+ 'q' => array(
+ 'name' => 'Keyword(s)',
+ 'type' => 'text',
+ 'required' => true
+ ),
+ 'hide_expired' => array(
+ 'name' => 'Hide expired deals',
+ 'type' => 'checkbox',
+ 'required' => 'true'
+ ),
+ 'hide_local' => array(
+ 'name' => 'Hide local deals',
+ 'type' => 'checkbox',
+ 'title' => 'Hide deals in physical store',
+ 'required' => 'true'
+ ),
+ 'priceFrom' => array(
+ 'name' => 'Minimal Price',
+ 'type' => 'text',
+ 'title' => 'Minmal Price in Pounds',
+ 'required' => 'false',
+ 'defaultValue' => ''
+ ),
+ 'priceTo' => array(
+ 'name' => 'Maximum Price',
+ 'type' => 'text',
+ 'title' => 'Maximum Price in Pounds',
+ 'required' => 'false',
+ 'defaultValue' => ''
+ ),
+ ),
+
+ 'Deals per group' => array(
+ 'group' => array(
+ 'name' => 'Group',
+ 'type' => 'list',
+ 'required' => 'true',
+ 'title' => 'Group whose deals must be displayed',
+ 'values' => array(
+ '2DS' => '2ds',
+ '3D Bluray' => '3d-bluray',
+ '3D Glasses' => '3d-glasses',
+ '3D Printer' => '3d-printer',
+ '3DS' => '3ds',
+ '3DS Games' => '3ds-games',
+ '3D TV' => '3d-tv',
+ '4G' => '4g',
+ '4k Bluray' => '4k-bluray',
+ '4k Monitor' => '4k-monitor',
+ '4K TV' => '4k-tv',
+ '7up' => '7up',
+ '144Hz Monitor' => '144hz',
+ 'AA Batteries' => 'aa',
+ 'Acer' => 'acer',
+ 'Actifry' => 'actifry',
+ 'Action Camera' => 'action-camera',
+ 'Add On Item' => 'add-on-item',
+ 'Adidas' => 'adidas',
+ 'Adobe' => 'adobe',
+ 'Aftershave' => 'aftershave',
+ 'Air Bed' => 'air-bed',
+ 'Air Conditioner' => 'air-con',
+ 'Air Fryer' => 'air-fryer',
+ 'Airport Parking' => 'airport-parking',
+ 'AKG' => 'akg',
+ 'Alarm' => 'alarm',
+ 'Alcatel' => 'alcatel',
+ 'Alcohol' => 'alcohol',
+ 'Alienware' => 'alienware',
+ 'Alton Towers' => 'alton-towers',
+ 'Amazon Echo' => 'amazon-echo',
+ 'Amazon Fire Stick' => 'amazon-fire-stick',
+ 'Amazon Fire Tv' => 'fire-tv',
+ 'Amazon Pantry' => 'amazon-pantry',
+ 'Amazon Prime' => 'amazon-prime',
+ 'Amazon Warehouse' => 'amazon-warehouse',
+ 'AMD' => 'amd',
+ 'Amex' => 'amex',
+ 'Amiibo' => 'amiibo',
+ 'Amsterdam' => 'amsterdam',
+ 'Android' => 'android',
+ 'Android Tablet' => 'android-tablet',
+ 'Android TV' => 'android-tv',
+ 'Anker' => 'anker',
+ 'Apple' => 'apple',
+ 'Apple TV' => 'apple-tv',
+ 'Apple Watch' => 'apple-watch',
+ 'Armani' => 'armani',
+ 'Asics' => 'asics',
+ 'ASUS' => 'asus',
+ 'Audi' => 'audi',
+ 'Baby' => 'baby',
+ 'Baby & Kids' => 'kids',
+ 'Baby Monitor' => 'baby-monitor',
+ 'Baby Swing' => 'baby-swing',
+ 'Backpack' => 'backpack',
+ 'Bag' => 'bag',
+ 'Bank' => 'bank',
+ 'Barbour' => 'barbour',
+ 'Barcelona' => 'barcelona',
+ 'Bathroom' => 'bathroom',
+ 'Batman' => 'batman',
+ 'Battery' => 'battery',
+ 'Battlefield' => 'battlefield',
+ 'Battlefield 1' => 'battlefield-1',
+ 'BBQ' => 'bbq',
+ 'Bean To Cup' => 'bean-to-cup',
+ 'Beard Trimmer' => 'beard-trimmer',
+ 'Bed' => 'bed',
+ 'Bedding' => 'bedding',
+ 'Bed Frame' => 'bed-frame',
+ 'Beer' => 'beer',
+ 'Beko' => 'beko',
+ 'Belfast' => 'belfast',
+ 'Bench' => 'bench',
+ 'Berghaus' => 'berghaus',
+ 'Bike' => 'bike',
+ 'Bin' => 'bin',
+ 'Bioshock' => 'bioshock',
+ 'Black Ops 3' => 'black-ops-3',
+ 'Blackpool' => 'blackpool',
+ 'Blender' => 'blender',
+ 'Blinds' => 'blinds',
+ 'Bloodborne' => 'bloodborne',
+ 'Blu-Ray' => 'blu-ray',
+ 'Bluetooth Headphones' => 'bluetooth-headphones',
+ 'Bluetooth Speaker' => 'bluetooth-speaker',
+ 'BMW' => 'bmw',
+ 'Board Game' => 'board-game',
+ 'Boiler' => 'boiler',
+ 'Bosch' => 'bosch',
+ 'Bose' => 'bose',
+ 'Bourbon' => 'bourbon',
+ 'Boxers' => 'boxers',
+ 'Bra' => 'bra',
+ 'Braun' => 'braun',
+ 'Breakdown' => 'breakdown',
+ 'Brewdog' => 'brewdog',
+ 'Brita' => 'brita',
+ 'Broadband' => 'broadband',
+ 'BT' => 'bt',
+ 'Bt Sport' => 'bt-sport',
+ 'Buggy' => 'buggy',
+ 'Call Of Duty' => 'call-of-duty',
+ 'Camera' => 'camera',
+ 'Camera Lens' => 'lens',
+ 'Camping' => 'camping',
+ 'Candle' => 'candle',
+ 'Canon' => 'canon',
+ 'Canvas' => 'canvas',
+ 'Car' => 'car',
+ 'Caravan' => 'caravan',
+ 'Car Battery' => 'car-battery',
+ 'Car Hire' => 'car-hire',
+ 'Car Insurance' => 'car-insurance',
+ 'Car Lease' => 'car-lease',
+ 'Car Mats' => 'car-mats',
+ 'Carpet' => 'carpet',
+ 'Carpet Cleaner' => 'carpet-cleaner',
+ 'Car Seat' => 'car-seat',
+ 'Car Stereo' => 'car-stereo',
+ 'Casio' => 'casio',
+ 'Caterpillar Boots' => 'caterpillar',
+ 'Cat Food' => 'cat-food',
+ 'CCTV' => 'cctv',
+ 'Chainsaw' => 'chainsaw',
+ 'Chair' => 'chair',
+ 'Champagne' => 'champagne',
+ 'Charger' => 'charger',
+ 'Chessington' => 'chessington',
+ 'Chest Freezer' => 'chest-freezer',
+ 'Chocolate' => 'chocolate',
+ 'Chromebook' => 'chromebook',
+ 'Chromecast' => 'chromecast',
+ 'Cider' => 'cider',
+ 'Cinema' => 'cinema',
+ 'Cineworld' => 'cineworld',
+ 'Circular Saw' => 'circular-saw',
+ 'Circulon' => 'circulon',
+ 'Citizen' => 'citizen',
+ 'Clarins' => 'clarins',
+ 'Clarks' => 'clarks',
+ 'Clinique' => 'clinique',
+ 'Clothes' => 'clothes',
+ 'Coat' => 'coat',
+ 'Coffee' => 'coffee',
+ 'Coffee Machine' => 'coffee-machine',
+ 'Coke' => 'coke',
+ 'Compost' => 'compost',
+ 'Computers' => 'computers',
+ 'Converse' => 'converse',
+ 'Cooker' => 'cooker',
+ 'Cordless Phone' => 'cordless-phone',
+ 'Corsair' => 'corsair',
+ 'Cot' => 'cot',
+ 'CPU' => 'cpu',
+ 'Crash Bandicoot' => 'crash-bandicoot',
+ 'Credit Card' => 'credit-card',
+ 'Cricket' => 'cricket',
+ 'Crisps' => 'crisps',
+ 'Crocs' => 'crocs',
+ 'Cruise' => 'cruise',
+ 'Cuprinol' => 'cuprinol',
+ 'Cutlery' => 'cutlery',
+ 'Dab Radio' => 'dab-radio',
+ 'Dark Souls' => 'dark-souls',
+ 'Dark Souls 3' => 'dark-souls-3',
+ 'Dash Cam' => 'dash-cam',
+ 'Days Out' => 'days-out',
+ 'DDR3' => 'ddr3',
+ 'DDR4' => 'ddr4',
+ 'Deezer' => 'deezer',
+ 'Dehumidifier' => 'dehumidifier',
+ 'Dell' => 'dell',
+ 'Delonghi' => 'delonghi',
+ 'Denon' => 'denon',
+ 'Desk' => 'desk',
+ 'Desktop' => 'desktop',
+ 'Destiny' => 'destiny',
+ 'Destiny 2' => 'destiny-2',
+ 'Deus Ex' => 'deus-ex',
+ 'Dewalt' => 'dewalt',
+ 'Digital Camera' => 'digital-camera',
+ 'Dining Table' => 'dining-table',
+ 'Dinner Set' => 'dinner-set',
+ 'Dirt 4' => 'dirt-4',
+ 'Dishonored 2' => 'dishonored-2',
+ 'Dishwasher' => 'dishwasher',
+ 'Disney' => 'disney',
+ 'Disney Infinity' => 'disney-infinity',
+ 'Disneyland' => 'disneyland',
+ 'DIY' => 'diy',
+ 'Doctor Who' => 'doctor-who',
+ 'Dog' => 'dog',
+ 'Dog Bed' => 'dog-bed',
+ 'Dolce Gusto' => 'dolce-gusto',
+ 'DOOM' => 'doom',
+ 'Dremel' => 'dremel',
+ 'Dress' => 'dress',
+ 'Drill' => 'drill',
+ 'Drone' => 'drone',
+ 'Dryer' => 'dryer',
+ 'DSLR Camera' => 'dslr',
+ 'Dubai' => 'dubai',
+ 'Dulux' => 'dulux',
+ 'Durex' => 'durex',
+ 'Duvet' => 'duvet',
+ 'DVD' => 'dvd',
+ 'DVD Player' => 'dvd-player',
+ 'Dying Light' => 'dying-light',
+ 'Dyson' => 'dyson',
+ 'Dyson V6' => 'dyson-v6',
+ 'Dyson V8' => 'dyson-v8',
+ 'E-Cig' => 'e-cig',
+ 'EA' => 'ea',
+ 'EA Access' => 'ea-access',
+ 'Earphones' => 'earphones',
+ 'Earrings' => 'earrings',
+ 'Eastpak' => 'eastpak',
+ 'eBook' => 'ebook',
+ 'Eco-Drive' => 'eco-drive',
+ 'Ecobubble' => 'ecobubble',
+ 'Edifice' => 'edifice',
+ 'Edinburgh' => 'edinburgh',
+ 'EE' => 'ee',
+ 'Egg' => 'egg',
+ 'Egypt' => 'egypt',
+ 'Elder Scrolls' => 'elder-scrolls',
+ 'Electric Bike' => 'electric-bike',
+ 'Electric Cooker' => 'electric-cooker',
+ 'Electric Fires' => 'electric-fire',
+ 'Electric Shower' => 'electric-shower',
+ 'Electric Toothbrush' => 'electric-toothbrush',
+ 'Electronics' => 'electronics',
+ 'Elemis' => 'elemis',
+ 'Elephone' => 'elephone',
+ 'Elgato' => 'elgato',
+ 'Elite Dangerous' => 'elite-dangerous',
+ 'Emirates' => 'emirates',
+ 'Eneloop' => 'eneloop',
+ 'Energy' => 'energy',
+ 'Engine Oil' => 'engine-oil',
+ 'Entertainment' => 'entertainment',
+ 'Epilator' => 'epilator',
+ 'Epson' => 'epson',
+ 'eReader' => 'ereader',
+ 'Espresso' => 'espresso',
+ 'Estee Lauder' => 'estee-lauder',
+ 'Ethernet' => 'ethernet',
+ 'Eurostar' => 'eurostar',
+ 'Eurotunnel' => 'eurotunnel',
+ 'EVGA' => 'evga',
+ 'Extension Lead' => 'extension-lead',
+ 'External Hard Drive' => 'external-hard-drive',
+ 'Fairy' => 'fairy',
+ 'Fallout' => 'fallout',
+ 'Fallout 4' => 'fallout-4',
+ 'Fan' => 'fan',
+ 'Fancy Dress' => 'fancy-dress',
+ 'Far Cry' => 'far-cry',
+ 'Far Cry 4' => 'far-cry-4',
+ 'Far Cry Primal' => 'far-cry-primal',
+ 'Fashion' => 'fashion',
+ 'Fathers Day' => 'fathers-day',
+ 'Felix' => 'felix',
+ 'Fence' => 'fence',
+ 'Fender Guitars' => 'fender',
+ 'Ferrero Rocher' => 'ferrero-rocher',
+ 'Ferry' => 'ferry',
+ 'Festival' => 'festival',
+ 'Fiat' => 'fiat',
+ 'FIFA' => 'fifa',
+ 'FIFA 17' => 'fifa-17',
+ 'FIFA 18' => 'fifa-18',
+ 'Figures' => 'figures',
+ 'Final Fantasy' => 'final-fantasy',
+ 'Finance & Utilities' => 'personal-finance',
+ 'Finish' => 'finish',
+ 'Finlux' => 'finlux',
+ 'Fire Emblem' => 'fire-emblem',
+ 'Fire Pit' => 'fire-pit',
+ 'Fireplace' => 'fireplace',
+ 'Fish' => 'fish',
+ 'Fisher Price' => 'fisher-price',
+ 'Fishing' => 'fishing',
+ 'Fiskars' => 'fiskars',
+ 'Fitbit' => 'fitbit',
+ 'Fitbit Alta' => 'fitbit-alta',
+ 'Fitbit Blaze' => 'fitbit-blaze',
+ 'Fitbit Charge 2' => 'fitbit-charge-2',
+ 'Fitness Tracker' => 'fitness-tracker',
+ 'Flamingo Land' => 'flamingo-land',
+ 'Flask' => 'flask',
+ 'Fleece' => 'fleece',
+ 'Flight' => 'flight',
+ 'Flip Flops' => 'flip-flops',
+ 'Floodlight' => 'floodlight',
+ 'Flooring' => 'flooring',
+ 'Florida' => 'florida',
+ 'Flowers' => 'flowers',
+ 'Flybe' => 'flybe',
+ 'Flymo' => 'flymo',
+ 'Food' => 'food',
+ 'Food Mixer' => 'food-mixer',
+ 'Food Processor' => 'food-processor',
+ 'Football' => 'football',
+ 'Football Boots' => 'football-boots',
+ 'Football Manager' => 'football-manager',
+ 'Football Shirt' => 'football-shirt',
+ 'Ford' => 'ford',
+ 'For Honor' => 'for-honor',
+ 'Formula 1' => 'f1',
+ 'Forza' => 'forza',
+ 'Forza Horizon' => 'forza-horizon',
+ 'Forza Horizon 3' => 'forza-horizon-3',
+ 'Fossil' => 'fossil',
+ 'Fosters' => 'fosters',
+ 'Foundation' => 'foundation',
+ 'France' => 'france',
+ 'Fred Perry' => 'fred-perry',
+ 'Freebies' => 'freebies',
+ 'Freesat' => 'freesat',
+ 'Freeview' => 'freeview',
+ 'Freezer' => 'freezer',
+ 'Fridge' => 'fridge',
+ 'Fridge Freezer' => 'fridge-freezer',
+ 'Frozen' => 'frozen',
+ 'Fruit' => 'fruit',
+ 'Fryer' => 'fryer',
+ 'Frying Pan' => 'frying-pan',
+ 'Fujifilm' => 'fuji',
+ 'Funko Pop' => 'funko-pop',
+ 'Furby' => 'furby',
+ 'Furniture' => 'furniture',
+ 'Fusion' => 'fusion',
+ 'G-Shock' => 'g-shock',
+ 'G-Sync Monitor' => 'g-sync',
+ 'Game Of Thrones' => 'game-of-thrones',
+ 'Gaming' => 'gaming',
+ 'Gaming Chair' => 'gaming-chair',
+ 'Gaming Controller' => 'controller',
+ 'Gaming Headset' => 'gaming-headset',
+ 'Gaming Keyboard' => 'gaming-keyboard',
+ 'Gaming Laptop' => 'gaming-laptop',
+ 'Gaming Monitor' => 'gaming-monitor',
+ 'Gaming PC' => 'gaming-pc',
+ 'Garage' => 'garage',
+ 'Garden' => 'garden',
+ 'Garden Furniture' => 'garden-furniture',
+ 'Garmin' => 'garmin',
+ 'Gas' => 'gas',
+ 'Gas Cooker' => 'gas-cooker',
+ 'Gatwick' => 'gatwick',
+ 'Gazebo' => 'gazebo',
+ 'Gazelle' => 'gazelle',
+ 'GBK' => 'gbk',
+ 'Gears Of War' => 'gears-of-war',
+ 'Gears Of War 4' => 'gears-of-war-4',
+ 'GeForce' => 'geforce',
+ 'George Foreman' => 'george-foreman',
+ 'Geox' => 'geox',
+ 'GHD' => 'ghd',
+ 'Ghostbusters' => 'ghostbusters',
+ 'Ghost Recon' => 'ghost-recon',
+ 'Gibson Guitars' => 'gibson',
+ 'Giffgaff' => 'giffgaff',
+ 'Gift Card' => 'gift-card',
+ 'Gifts' => 'gifts',
+ 'Gift Set' => 'gift-set',
+ 'Gilet' => 'gilet',
+ 'Gillette' => 'gillette',
+ 'Gimbal' => 'gimbal',
+ 'Gin' => 'gin',
+ 'Glasgow' => 'glasgow',
+ 'Glasses' => 'glasses',
+ 'Gloves' => 'gloves',
+ 'Glue Gun' => 'glue-gun',
+ 'Gluten Free' => 'gluten-free',
+ 'Goggles' => 'goggles',
+ 'Go Kart' => 'go-kart',
+ 'Golf' => 'golf',
+ 'Golf Balls' => 'golf-balls',
+ 'Golf Clubs' => 'golf-clubs',
+ 'Goodfellas' => 'goodfellas',
+ 'Google' => 'google',
+ 'Google Home' => 'google-home',
+ 'Google Pixel' => 'google-pixel',
+ 'Go Outdoors' => 'go-outdoors',
+ 'GoPro' => 'gopro',
+ 'Graco' => 'graco',
+ 'Grand National' => 'grand-national',
+ 'Graphics Card' => 'graphics-card',
+ 'Gravity Rush' => 'gravity-rush',
+ 'Graze' => 'graze',
+ 'Greece' => 'greece',
+ 'Greenhouse' => 'greenhouse',
+ 'Greggs' => 'greggs',
+ 'Grey Goose' => 'grey-goose',
+ 'Grill' => 'grill',
+ 'Grinder' => 'grinder',
+ 'Grobag' => 'grobag',
+ 'Groceries' => 'groceries',
+ 'GTA' => 'gta',
+ 'GTA V' => 'gta-v',
+ 'Gtx 970' => 'gtx-970',
+ 'GTX 1060' => 'gtx-1060',
+ 'GTX 1070' => 'gtx-1070',
+ 'GTX 1080' => 'gtx-1080',
+ 'Guardians Of The Galaxy' => 'guardians-of-the-galaxy',
+ 'Gucci' => 'gucci',
+ 'Guinness' => 'guinness',
+ 'Guitar' => 'guitar',
+ 'Guitar Hero' => 'guitar-hero',
+ 'Gullivers' => 'gullivers',
+ 'Gym' => 'gym',
+ 'Gym Membership' => 'gym-membership',
+ 'H1z1' => 'h1z1',
+ 'Habitat' => 'habitat',
+ 'Hair' => 'hair',
+ 'Hair Clipper' => 'hair-clipper',
+ 'Hair Dryer' => 'hair-dryer',
+ 'Hair Dye' => 'hair-dye',
+ 'Halifax' => 'halifax',
+ 'Halo' => 'halo',
+ 'Halo 5' => 'halo-5',
+ 'Hammer' => 'hammer',
+ 'Hammock' => 'hammock',
+ 'Hamper' => 'hamper',
+ 'Handbag' => 'handbag',
+ 'Hand Mixer' => 'hand-mixer',
+ 'Happyland' => 'happyland',
+ 'Hard Drive' => 'hard-drive',
+ 'Haribo' => 'haribo',
+ 'Harman Kardon' => 'harman-kardon',
+ 'Harmony' => 'harmony',
+ 'Harry Potter' => 'harry-potter',
+ 'Hat' => 'hat',
+ 'Hatchimals' => 'hatchimals',
+ 'Hayfever' => 'hayfever',
+ 'Hdr Tv' => 'hdr-tv',
+ 'HD TV' => 'hd-tv',
+ 'Headboard' => 'headboard',
+ 'Headphones' => 'headphones',
+ 'Headset' => 'headset',
+ 'Health & Beauty' => 'beauty',
+ 'Heater' => 'heater',
+ 'Hedge Trimmer' => 'hedge-trimmer',
+ 'Heineken' => 'heineken',
+ 'Heinz' => 'heinz',
+ 'Helmet' => 'helmet',
+ 'Hermes' => 'hermes',
+ 'Highchair' => 'highchair',
+ 'Hiking' => 'hiking',
+ 'Hilton' => 'hilton',
+ 'Hisense' => 'hisense',
+ 'Hitachi' => 'hitachi',
+ 'Hitman' => 'hitman',
+ 'Hive' => 'hive',
+ 'Hob' => 'hob',
+ 'Holiday Inn' => 'holiday-inn',
+ 'Holidays & Leisure' => 'holiday',
+ 'Home & Garden' => 'home',
+ 'Home Cinema' => 'home-cinema',
+ 'Homedics' => 'homedics',
+ 'Homefront' => 'homefront',
+ 'Homeplug' => 'homeplug',
+ 'Home Security' => 'home-security',
+ 'Honey' => 'honey',
+ 'Honeywell' => 'honeywell',
+ 'Hong Kong' => 'hong-kong',
+ 'Honor' => 'honor',
+ 'Honor 6x' => 'honor-6x',
+ 'Hoodie' => 'hoodie',
+ 'Hoover' => 'hoover',
+ 'Horizon Zero Dawn' => 'horizon-zero-dawn',
+ 'Hornby' => 'hornby',
+ 'Hose' => 'hose',
+ 'Hotel' => 'hotel',
+ 'Hotpoint' => 'hotpoint',
+ 'Hot Tub' => 'hot-tub',
+ 'Hot Wheels' => 'hot-wheels',
+ 'Hozelock' => 'hozelock',
+ 'HP' => 'hp',
+ 'HP Envy' => 'hp-envy',
+ 'HP Laptop' => 'hp-laptop',
+ 'H Samuel' => 'h-samuel',
+ 'HTC' => 'htc',
+ 'HTC 10' => 'htc-10',
+ 'HTC Vive' => 'htc-vive',
+ 'Huawei' => 'huawei',
+ 'Huawei P9' => 'huawei-p9',
+ 'Huggies' => 'huggies',
+ 'Hugo Boss' => 'hugo-boss',
+ 'Humax' => 'humax',
+ 'Humidifier' => 'humidifier',
+ 'Hunter' => 'hunter',
+ 'Hydro 5' => 'hydro-5',
+ 'Hyperx' => 'hyperx',
+ 'Hyundai' => 'hyundai',
+ 'Iams Pet Food' => 'iams',
+ 'Ibiza' => 'ibiza',
+ 'Icandy' => 'icandy',
+ 'Ice Cream' => 'ice-cream',
+ 'Ice Cream Maker' => 'ice-cream-maker',
+ 'Imaginext' => 'imaginext',
+ 'Impact Driver' => 'impact-driver',
+ 'Indesit' => 'indesit',
+ 'India' => 'india',
+ 'Inflatable' => 'inflatable',
+ 'Injustice' => 'injustice',
+ 'Ink' => 'ink',
+ 'Inner Tube' => 'inner-tube',
+ 'Instant Ink' => 'instant-ink',
+ 'Insulation' => 'insulation',
+ 'Insurance' => 'insurance',
+ 'Intel' => 'intel',
+ 'Intel Atom' => 'atom',
+ 'Intel i3' => 'i3',
+ 'Intel i5' => 'i5',
+ 'Intel i7' => 'i7',
+ 'Internal Hard Drive' => 'internal-hard-drive',
+ 'Internet' => 'internet',
+ 'In The Night Garden' => 'in-the-night-garden',
+ 'iOS' => 'ios',
+ 'iPad' => 'ipad',
+ 'iPad Air' => 'ipad-air',
+ 'iPad Case' => 'ipad-case',
+ 'iPad Mini' => 'ipad-mini',
+ 'iPad Pro' => 'ipad-pro',
+ 'Ip Camera' => 'ip-camera',
+ 'iPhone' => 'iphone',
+ 'iPhone 5S' => 'iphone-5s',
+ 'iPhone 6' => 'iphone-6',
+ 'iPhone 6 Plus' => 'iphone-6-plus',
+ 'iPhone 6s' => 'iphone-6s',
+ 'iPhone 6s Plus' => 'iphone-6s-plus',
+ 'iPhone 7' => 'iphone-7',
+ 'iPhone 7 Plus' => 'iphone-7-plus',
+ 'iPhone Case' => 'iphone-case',
+ 'Iphone SE' => 'iphone-se',
+ 'iPod' => 'ipod',
+ 'iPod Nano' => 'ipod-nano',
+ 'iPod Touch' => 'ipod-touch',
+ 'Ireland' => 'ireland',
+ 'Irn Bru' => 'irn-bru',
+ 'Iron' => 'iron',
+ 'Ironing Board' => 'ironing-board',
+ 'Isle Of Wight' => 'isle-of-wight',
+ 'Isofix' => 'isofix',
+ 'Issey Miyake' => 'issey-miyake',
+ 'Italy' => 'italy',
+ 'iTunes' => 'itunes',
+ 'ITV' => 'itv',
+ 'Jabra' => 'jabra',
+ 'Jack Daniels' => 'jack-daniels',
+ 'Jacket' => 'jacket',
+ 'Jack Wills' => 'jack-wills',
+ 'Jack Wolfskin' => 'jack-wolfskin',
+ 'Jaguar' => 'jaguar',
+ 'Jamaica' => 'jamaica',
+ 'Jameson' => 'jameson',
+ 'Japan' => 'japan',
+ 'Jawbone' => 'jawbone',
+ 'Jaybird' => 'jaybird',
+ 'JBL' => 'jbl',
+ 'Jeans' => 'jeans',
+ 'Jewellery' => 'jewellery',
+ 'Jigsaw' => 'jigsaw',
+ 'Jim Beam' => 'jim-beam',
+ 'Jimmy Choo' => 'jimmy-choo',
+ 'Joop' => 'joop',
+ 'Jordan' => 'jordan',
+ 'Joseph Joseph' => 'joseph-joseph',
+ 'Joules' => 'joules',
+ 'Juice' => 'juice',
+ 'Juicer' => 'juicer',
+ 'Jumper' => 'jumper',
+ 'Jumperoo' => 'jumperoo',
+ 'Jura' => 'jura',
+ 'Just Cause 3' => 'just-cause-3',
+ 'Just Dance' => 'just-dance',
+ 'JVC' => 'jvc',
+ 'Karcher' => 'karcher',
+ 'Kaspersky' => 'kaspersky',
+ 'Kayak' => 'kayak',
+ 'Keg' => 'keg',
+ 'Kenwood' => 'kenwood',
+ 'Kenwood kMix' => 'kmix',
+ 'Keter' => 'keter',
+ 'Kettle' => 'kettle',
+ 'Kettlebell' => 'kettlebell',
+ 'Keyboard' => 'keyboard',
+ 'Kia' => 'kia',
+ 'Kickers' => 'kickers',
+ 'Kids Bike' => 'kids-bike',
+ 'Kinder' => 'kinder',
+ 'Kindle' => 'kindle',
+ 'Kindle Fire' => 'kindle-fire',
+ 'Kindle Paperwhite' => 'kindle-paperwhite',
+ 'Kinect' => 'kinect',
+ 'Kingdom Hearts' => 'kingdom-hearts',
+ 'King Size' => 'king-size',
+ 'Kirby' => 'kirby',
+ 'Kitchen' => 'kitchen',
+ 'KitchenAid' => 'kitchenaid',
+ 'Kitchen Roll' => 'kitchen-roll',
+ 'Kitsound' => 'kitsound',
+ 'Knickers' => 'knickers',
+ 'Knife' => 'knife',
+ 'Kobo' => 'kobo',
+ 'Kodi' => 'kodi',
+ 'Kopparberg' => 'kopparberg',
+ 'Kraken' => 'kraken',
+ 'Krakow' => 'krakow',
+ 'Krispy Kreme' => 'krispy-kreme',
+ 'Kurt Geiger' => 'kurt-geiger',
+ 'Lacoste' => 'lacoste',
+ 'Ladder' => 'ladder',
+ 'Lamb' => 'lamb',
+ 'Laminate' => 'laminate',
+ 'Lamp' => 'lamp',
+ 'Lancome' => 'lancome',
+ 'Laptop' => 'laptop',
+ 'Laser Printer' => 'laser-printer',
+ 'Laura Ashley' => 'laura-ashley',
+ 'Lavazza' => 'lavazza',
+ 'Lavender' => 'lavender',
+ 'Lawnmower' => 'lawnmower',
+ 'Lay-Z-Spa' => 'lay-z-spa',
+ 'Le Creuset' => 'le-creuset',
+ 'LED Bulbs' => 'led-bulbs',
+ 'LED TV' => 'led-tv',
+ 'Leeds' => 'leeds',
+ 'Lego' => 'lego',
+ 'Lego City' => 'lego-city',
+ 'Lego Dimensions' => 'lego-dimensions',
+ 'Lego Friends' => 'lego-friends',
+ 'Legoland' => 'legoland',
+ 'Lego Star Wars' => 'lego-star-wars',
+ 'Lego Technic' => 'lego-technic',
+ 'Leicester' => 'leicester',
+ 'Lenovo' => 'lenovo',
+ 'Lenovo Tablet' => 'lenovo-tablet',
+ 'Lenovo Yoga' => 'lenovo-yoga',
+ 'Levi' => 'levi',
+ 'LG' => 'lg',
+ 'LG G5' => 'lg-g5',
+ 'LG G6' => 'lg-g6',
+ 'LG TV' => 'lg-tv',
+ 'Light' => 'light',
+ 'Lighting' => 'lighting',
+ 'Lightning Cable' => 'lightning-cable',
+ 'Lindor' => 'lindor',
+ 'Lindt' => 'lindt',
+ 'Lingerie' => 'lingerie',
+ 'Linx' => 'linx',
+ 'Little Tikes' => 'little-tikes',
+ 'Liverpool' => 'liverpool',
+ 'Logitech' => 'logitech',
+ 'London' => 'london',
+ 'London Eye' => 'london-eye',
+ 'London Zoo' => 'london-zoo',
+ 'Lottery' => 'lottery',
+ 'Lounger' => 'lounger',
+ 'Lurpak' => 'lurpak',
+ 'Lynx' => 'lynx',
+ 'MacBook' => 'macbook',
+ 'MacBook Air' => 'macbook-air',
+ 'MacBook Pro' => 'macbook-pro',
+ 'Mac Mini' => 'mac-mini',
+ 'Mad Max' => 'mad-max',
+ 'Mafia 3' => 'mafia-3',
+ 'Magazine' => 'magazine',
+ 'Magimix' => 'magimix',
+ 'Majorca' => 'majorca',
+ 'Make Up' => 'make-up',
+ 'Makita' => 'makita',
+ 'Maldives' => 'maldives',
+ 'Manchester' => 'manchester',
+ 'Manfrotto' => 'manfrotto',
+ 'Marc Jacobs' => 'marc-jacobs',
+ 'Mario' => 'mario',
+ 'Mario Kart' => 'mario-kart',
+ 'Mario Kart 8' => 'mario-kart-8',
+ 'Mario Kart 8 Deluxe' => 'mario-kart-8-deluxe',
+ 'Marvel' => 'marvel',
+ 'Mascara' => 'mascara',
+ 'Massage' => 'massage',
+ 'Mass Effect' => 'mass-effect',
+ 'Mass Effect Andromeda' => 'mass-effect-andromeda',
+ 'Maternity' => 'maternity',
+ 'Mattress' => 'mattress',
+ 'Mattress Topper' => 'mattress-topper',
+ 'Mavic' => 'mavic',
+ 'Maxi Cosi' => 'maxi-cosi',
+ 'Meat' => 'meat',
+ 'Mechanical Keyboard' => 'mechanical-keyboard',
+ 'Medion' => 'medion',
+ 'Memory Foam' => 'memory-foam',
+ 'Mens Boots' => 'mens-boots',
+ 'Mens Fashion' => 'mens-clothing',
+ 'Mens Shoes' => 'mens-shoes',
+ 'Mercedes' => 'mercedes',
+ 'Merlin' => 'merlin',
+ 'Merrell' => 'merrell',
+ 'Mexico' => 'mexico',
+ 'Michael Kors' => 'michael-kors',
+ 'Microphone' => 'microphone',
+ 'Micro SD' => 'micro-sd',
+ 'Microserver' => 'microserver',
+ 'Microsoft' => 'microsoft',
+ 'Microsoft Office' => 'microsoft-office',
+ 'Microwave' => 'microwave',
+ 'Miele' => 'miele',
+ 'Milwaukee' => 'milwaukee',
+ 'Minecraft' => 'minecraft',
+ 'Mini' => 'mini',
+ 'Mini Fridge' => 'mini-fridge',
+ 'Mini PC' => 'mini-pc',
+ 'Mirror' => 'mirror',
+ 'Mitre Saw' => 'mitre-saw',
+ 'Mobile Broadband' => 'mobile-broadband',
+ 'Mobile Contract' => 'mobile-contract',
+ 'Mobile Phone' => 'mobile-phone',
+ 'Mobiles' => 'mobiles',
+ 'Molton Brown' => 'molton-brown',
+ 'Monitor' => 'monitor',
+ 'Monopoly' => 'monopoly',
+ 'Monsoon' => 'monsoon',
+ 'Mop' => 'mop',
+ 'Morocco' => 'morocco',
+ 'Mortgage' => 'mortgage',
+ 'Moses Basket' => 'moses-basket',
+ 'Mot' => 'mot',
+ 'Motherboard' => 'motherboard',
+ 'Moto 360' => 'moto-360',
+ 'Moto G' => 'moto-g',
+ 'Moto G4' => 'moto-g4',
+ 'Moto G5' => 'moto-g5',
+ 'Motorcycle' => 'motorcycle',
+ 'Motorola' => 'motorola',
+ 'Moto Z' => 'moto-z',
+ 'Mountain Bike' => 'mountain-bike',
+ 'Mouse' => 'mouse',
+ 'Mouse Mat' => 'mouse-mat',
+ 'Movie' => 'movie',
+ 'MP3 Player' => 'mp3-player',
+ 'MSI' => 'msi',
+ 'Mug' => 'mug',
+ 'Multitool' => 'multitool',
+ 'Music' => 'music',
+ 'My Little Pony' => 'my-little-pony',
+ 'Nandos' => 'nandos',
+ 'NAS' => 'nas',
+ 'National Express' => 'national-express',
+ 'National Trust' => 'national-trust',
+ 'Necklace' => 'necklace',
+ 'Nectar' => 'nectar',
+ 'Neff' => 'neff',
+ 'Nerf' => 'nerf',
+ 'Nescafe' => 'nescafe',
+ 'Nespresso' => 'nespresso',
+ 'Nest' => 'nest',
+ 'Netflix' => 'netflix',
+ 'Netgear' => 'netgear',
+ 'Netgear Arlo' => 'arlo',
+ 'New Balance' => 'new-balance',
+ 'Newcastle' => 'newcastle',
+ 'New Look' => 'new-look',
+ 'New York' => 'new-york',
+ 'Nextbase' => 'nextbase',
+ 'Nexus' => 'nexus',
+ 'NFL' => 'nfl',
+ 'Nier' => 'nier',
+ 'Nike' => 'nike',
+ 'Nike Air Max' => 'nike-air-max',
+ 'Nikon' => 'nikon',
+ 'Nilfisk' => 'nilfisk',
+ 'Ninja' => 'ninja',
+ 'Nintendo' => 'nintendo',
+ 'Nintendo DS' => 'nintendo-ds',
+ 'Nintendo eShop' => 'eshop',
+ 'Nintendo Switch' => 'nintendo-switch',
+ 'Nioh' => 'nioh',
+ 'Nissan' => 'nissan',
+ 'Nivea' => 'nivea',
+ 'Nokia' => 'nokia',
+ 'North Face' => 'north-face',
+ 'Norton' => 'norton',
+ 'Note 4' => 'note-4',
+ 'Now TV' => 'now-tv',
+ 'Nursery' => 'nursery',
+ 'Nus' => 'nus',
+ 'Nutella' => 'nutella',
+ 'Nutribullet' => 'nutribullet',
+ 'Nutri Ninja' => 'nutri-ninja',
+ 'NVIDIA' => 'nvidia',
+ 'O2' => 'o2',
+ 'O2 Refresh' => 'o2-refresh',
+ 'Oak' => 'oak',
+ 'Oakley' => 'oakley',
+ 'Oculus' => 'oculus',
+ 'Odeon' => 'odeon',
+ 'Office' => 'office',
+ 'Office Chair' => 'office-chair',
+ 'OLED TV' => 'oled',
+ 'Olympus' => 'olympus',
+ 'Oneplus' => 'oneplus',
+ 'Onkyo' => 'onkyo',
+ 'Oral-B' => 'oral-b',
+ 'Origin' => 'origin',
+ 'Orlando' => 'orlando',
+ 'Osprey' => 'osprey',
+ 'Ottoman' => 'ottoman',
+ 'Outdoor' => 'outdoor',
+ 'Oven' => 'oven',
+ 'Overwatch' => 'overwatch',
+ 'Paddling Pool' => 'paddling-pool',
+ 'Paint' => 'paint',
+ 'Pampers' => 'pampers',
+ 'Pan' => 'pan',
+ 'Panasonic' => 'panasonic',
+ 'Panasonic Lumix' => 'lumix',
+ 'Pandora' => 'pandora',
+ 'Papa Johns' => 'papa-johns',
+ 'Parasol' => 'parasol',
+ 'Parcel' => 'parcel',
+ 'Paris' => 'paris',
+ 'Parking' => 'parking',
+ 'Paw Patrol' => 'paw-patrol',
+ 'PAYG' => 'payg',
+ 'Paypal' => 'paypal',
+ 'PC' => 'pc',
+ 'PC Case' => 'pc-case',
+ 'PC Game' => 'pc-game',
+ 'Pebble' => 'pebble',
+ 'Peppa Pig' => 'peppa-pig',
+ 'Pepsi' => 'pepsi',
+ 'Perfume' => 'perfume',
+ 'Persona' => 'persona',
+ 'Persona 5' => 'persona-5',
+ 'Petrol' => 'petrol',
+ 'Philips' => 'philips',
+ 'Philips Hue' => 'philips-hue',
+ 'Phones' => 'phone',
+ 'Photo' => 'photo',
+ 'Piano' => 'piano',
+ 'Pillow' => 'pillow',
+ 'Pizza' => 'pizza',
+ 'Plant' => 'plant',
+ 'Playhouse' => 'playhouse',
+ 'Playmobil' => 'playmobil',
+ 'Playstation' => 'playstation',
+ 'Playstation Plus' => 'playstation-plus',
+ 'Playstation VR' => 'playstation-vr',
+ 'Pokemon' => 'pokemon',
+ 'Pool' => 'pool',
+ 'Power Bank' => 'power-bank',
+ 'Powerline' => 'powerline',
+ 'Power Rangers' => 'power-rangers',
+ 'Pram' => 'pram',
+ 'Pressure Cooker' => 'pressure-cooker',
+ 'Pressure Washer' => 'pressure-washer',
+ 'Printer' => 'printer',
+ 'Projector' => 'projector',
+ 'Protein' => 'protein',
+ 'PS3' => 'ps3',
+ 'PS4' => 'ps4',
+ 'PS4 Controller' => 'ps4-controller',
+ 'PS4 Games' => 'ps4-games',
+ 'PS4 Headset' => 'ps4-headset',
+ 'PS4 Pro' => 'ps4-pro',
+ 'PS4 Slim' => 'ps4-slim',
+ 'PSN' => 'psn',
+ 'PSU' => 'psu',
+ 'PS Vita' => 'ps-vita',
+ 'Puma' => 'puma',
+ 'Pushchair' => 'pushchair',
+ 'Qnap' => 'qnap',
+ 'Quorn' => 'quorn',
+ 'Rab' => 'rab',
+ 'Radiator' => 'radiator',
+ 'Radio' => 'radio',
+ 'Radley' => 'radley',
+ 'Railcard' => 'railcard',
+ 'Ralph Lauren' => 'ralph-lauren',
+ 'RAM' => 'ram',
+ 'Raspberry Pi' => 'raspberry-pi',
+ 'Rattan Garden Furniture' => 'rattan',
+ 'Ray Ban' => 'ray-ban',
+ 'Razer' => 'razer',
+ 'Razor' => 'razor',
+ 'Reebok' => 'reebok',
+ 'Resident Evil' => 'resident-evil',
+ 'Resident Evil 7' => 'resident-evil-7',
+ 'Rice' => 'rice',
+ 'Ring' => 'ring',
+ 'Road Bike' => 'road-bike',
+ 'Rocket League' => 'rocket-league',
+ 'Rogue One' => 'rogue-one',
+ 'Roku' => 'roku',
+ 'Rolex' => 'rolex',
+ 'Roof Box' => 'roof-box',
+ 'Roses' => 'roses',
+ 'Rotary' => 'rotary',
+ 'Router' => 'router',
+ 'Rug' => 'rug',
+ 'Rum' => 'rum',
+ 'Running' => 'running',
+ 'RX 480' => 'rx-480',
+ 'Ryanair' => 'ryanair',
+ 'Ryobi' => 'ryobi',
+ 'Sale' => 'sale',
+ 'Salmon' => 'salmon',
+ 'Salomon' => 'salomon',
+ 'Samsonite' => 'samsonite',
+ 'Samsung' => 'samsung',
+ 'Samsung Galaxy' => 'samsung-galaxy',
+ 'Samsung Galaxy S7' => 'samsung-galaxy-s7',
+ 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge',
+ 'Samsung Galaxy S8' => 'samsung-galaxy-s8',
+ 'Samsung Galaxy S8 Plus' => 'samsung-s8-plus',
+ 'Samsung Gear' => 'samsung-gear',
+ 'Samsung TV' => 'samsung-tv',
+ 'Sandals' => 'sandals',
+ 'Sander' => 'sander',
+ 'SanDisk' => 'sandisk',
+ 'Sat Nav' => 'sat-nav',
+ 'Saw' => 'saw',
+ 'Scalextric' => 'scalextric',
+ 'Scooter' => 'scooter',
+ 'Screenwash' => 'screenwash',
+ 'Screwdriver' => 'screwdriver',
+ 'SD Card' => 'sd-card',
+ 'SDXC' => 'sdxc',
+ 'Seagate' => 'seagate',
+ 'Seat' => 'seat',
+ 'Security Camera' => 'security-camera',
+ 'Seeds' => 'seeds',
+ 'Seiko' => 'seiko',
+ 'Sennheiser' => 'sennheiser',
+ 'Server' => 'server',
+ 'Sewing Machine' => 'sewing-machine',
+ 'Shadow Of Mordor' => 'shadow-of-mordor',
+ 'Shark' => 'shark',
+ 'Sharpie' => 'sharpie',
+ 'Shaver' => 'shaver',
+ 'Shed' => 'shed',
+ 'Shelves' => 'shelves',
+ 'Shirt' => 'shirt',
+ 'Shoes' => 'shoe',
+ 'Shopkins' => 'shopkins',
+ 'Shorts' => 'shorts',
+ 'Shower' => 'shower',
+ 'Shredder' => 'shredder',
+ 'Sideboard' => 'sideboard',
+ 'Sim' => 'sim',
+ 'Sim Free' => 'sim-free',
+ 'Sim Only' => 'sim-only',
+ 'Sink' => 'sink',
+ 'Skechers' => 'skechers',
+ 'Ski' => 'ski',
+ 'Skoda' => 'skoda',
+ 'Sky' => 'sky',
+ 'Skylanders' => 'skylanders',
+ 'Skyrim' => 'skyrim',
+ 'Sleeping Bag' => 'sleeping-bag',
+ 'Slide' => 'slide',
+ 'Slimming World' => 'slimming-world',
+ 'Slippers' => 'slippers',
+ 'Slow Cooker' => 'slow-cooker',
+ 'SLR Camera' => 'slr',
+ 'Smart' => 'smart',
+ 'Smartphone' => 'smartphone',
+ 'Smartthings' => 'smartthings',
+ 'Smart TV' => 'smart-tv',
+ 'Smartwatch' => 'smartwatch',
+ 'Snapfish' => 'snapfish',
+ 'Socket Set' => 'socket-set',
+ 'Socks' => 'socks',
+ 'Sofa' => 'sofa',
+ 'Software & Apps' => 'software-apps',
+ 'Sonicare' => 'sonicare',
+ 'Sonos' => 'sonos',
+ 'Sony' => 'sony',
+ 'Sony TV' => 'sony-tv',
+ 'Soundbar' => 'soundbar',
+ 'Soup Maker' => 'soup-maker',
+ 'Spa' => 'spa',
+ 'Spain' => 'spain',
+ 'Speakers' => 'speakers',
+ 'Spinner' => 'spinner',
+ 'Spirits' => 'spirits',
+ 'Splatoon' => 'splatoon',
+ 'Sports & Fitness' => 'sports-fitness',
+ 'SSD' => 'ssd',
+ 'Starbucks' => 'starbucks',
+ 'Star Trek' => 'star-trek',
+ 'Star Wars' => 'star-wars',
+ 'Steak' => 'steak',
+ 'Steam' => 'steam',
+ 'Steamer' => 'steamer',
+ 'Steam Iron' => 'steam-iron',
+ 'Steam Link' => 'steam-link',
+ 'Steam Mop' => 'steam-mop',
+ 'Storage' => 'storage',
+ 'Storage Box' => 'storage-box',
+ 'Strimmer' => 'strimmer',
+ 'Student' => 'student',
+ 'Suit' => 'suit',
+ 'Suitcase' => 'suitcase',
+ 'Sun Cream' => 'sun-cream',
+ 'Sunglasses' => 'sunglasses',
+ 'Superdry' => 'superdry',
+ 'Surface' => 'surface',
+ 'Surface Book' => 'surface-book',
+ 'Sweets' => 'sweets',
+ 'Swing' => 'swing',
+ 'Synology' => 'synology',
+ 'T-Shirt' => 't-shirt',
+ 'Table' => 'table',
+ 'Tablet' => 'tablet',
+ 'Table Tennis' => 'table-tennis',
+ 'Tado' => 'tado',
+ 'Tag Heuer' => 'tag-heuer',
+ 'Takeaway' => 'takeaway',
+ 'Talkmobile' => 'talkmobile',
+ 'Tap' => 'tap',
+ 'Tassimo' => 'tassimo',
+ 'Tastecard' => 'tastecard',
+ 'Tea' => 'tea',
+ 'Ted Baker' => 'ted-baker',
+ 'Tefal' => 'tefal',
+ 'Tekken' => 'tekken',
+ 'Tekken 7' => 'tekken-7',
+ 'Telegraph' => 'telegraph',
+ 'Telescope' => 'telescope',
+ 'Tenerife' => 'tenerife',
+ 'Tennis' => 'tennis',
+ 'Tent' => 'tent',
+ 'Tesco Clothing' => 'tesco-clothing',
+ 'Tesla' => 'tesla',
+ 'Thailand' => 'thailand',
+ 'Theatre' => 'theatre',
+ 'The Body Shop' => 'body-shop',
+ 'The Last Guardian' => 'the-last-guardian',
+ 'The Last Of Us' => 'the-last-of-us',
+ 'Theme Park' => 'theme-park',
+ 'Thermometer' => 'thermometer',
+ 'Thermos' => 'thermos',
+ 'Thermostat' => 'thermostat',
+ 'The Sun' => 'the-sun',
+ 'The Witcher 3' => 'the-witcher-3',
+ 'Thinkpad' => 'thinkpad',
+ 'Thomas Sabo' => 'thomas-sabo',
+ 'Thorntons' => 'thorntons',
+ 'Thorpe Park' => 'thorpe-park',
+ 'Throw' => 'throw',
+ 'Thrustmaster' => 'thrustmaster',
+ 'Thule' => 'thule',
+ 'Tights' => 'tights',
+ 'Tile' => 'tile',
+ 'Timberland' => 'timberland',
+ 'Tissot' => 'tissot',
+ 'Titanfall' => 'titanfall',
+ 'Titanfall 2' => 'titanfall-2',
+ 'Toaster' => 'toaster',
+ 'Toddler Bed' => 'toddler-bed',
+ 'Toilet' => 'toilet',
+ 'Toilet Roll' => 'toilet-roll',
+ 'Toilet Seat' => 'toilet-seat',
+ 'Tomb Raider' => 'tomb-raider',
+ 'Tom Clancy' => 'tom-clancy',
+ 'Tom Ford' => 'tom-ford',
+ 'Tommee Tippee' => 'tommee-tippee',
+ 'Toms' => 'toms',
+ 'TomTom' => 'tomtom',
+ 'Tool' => 'tool',
+ 'Toothbrush' => 'toothbrush',
+ 'Toothpaste' => 'toothpaste',
+ 'Toot Toot' => 'toot-toot',
+ 'Torch' => 'torch',
+ 'Torque Wrench' => 'torque-wrench',
+ 'Toshiba' => 'toshiba',
+ 'Towel' => 'towel',
+ 'Toyota' => 'toyota',
+ 'Toys' => 'toy',
+ 'Toy Story' => 'toy-story',
+ 'Tp Link' => 'tp-link',
+ 'Tracksuit' => 'tracksuit',
+ 'Train' => 'train',
+ 'Trainers' => 'trainers',
+ 'Trampoline' => 'trampoline',
+ 'Transcend' => 'transcend',
+ 'Transformers' => 'transformers',
+ 'Travel' => 'travel',
+ 'Travel Insurance' => 'travel-insurance',
+ 'Travelodge' => 'travelodge',
+ 'Travel System' => 'travel-system',
+ 'Treadmill' => 'treadmill',
+ 'Trespass' => 'trespass',
+ 'Trike' => 'trike',
+ 'Tripod' => 'tripod',
+ 'Tripp' => 'tripp',
+ 'Trolley' => 'trolley',
+ 'Trousers' => 'trousers',
+ 'Trunki' => 'trunki',
+ 'Tumble Dryer' => 'tumble-dryer',
+ 'Tuna' => 'tuna',
+ 'Turbo Trainer' => 'turbo-trainer',
+ 'Turkey' => 'turkey',
+ 'Turntable' => 'turntable',
+ 'Turtle Beach' => 'turtle-beach',
+ 'TV' => 'tv',
+ 'TV Stand' => 'tv-stand',
+ 'Tyres' => 'tyres',
+ 'Ubisoft' => 'ubisoft',
+ 'Ue Boom' => 'ue-boom',
+ 'UFC' => 'ufc',
+ 'UGG' => 'ugg',
+ 'Ulefone' => 'ulefone',
+ 'Ultimate Ears UE Boom 2' => 'ue-boom-2',
+ 'Ultimate Outdoors' => 'ultimate-outdoors',
+ 'Ultrabook' => 'ultrabook',
+ 'Ultrawide Monitor' => 'ultrawide',
+ 'Umbrella' => 'umbrella',
+ 'Umi' => 'umi',
+ 'Uncharted' => 'uncharted',
+ 'Uncharted 4' => 'uncharted-4',
+ 'Under Armour' => 'under-armour',
+ 'Underwear' => 'underwear',
+ 'Unicorn' => 'unicorn',
+ 'Unidays' => 'unidays',
+ 'Urban Decay' => 'urban-decay',
+ 'Usa' => 'usa',
+ 'USB Hub' => 'usb-hub',
+ 'USB Memory Stick' => 'flash-drive',
+ 'Usn' => 'usn',
+ 'Vacuum Cleaners' => 'vacuum-cleaners',
+ 'Vango' => 'vango',
+ 'Vanish' => 'vanish',
+ 'Vans' => 'vans',
+ 'Vape' => 'vape',
+ 'Vauxhall' => 'vauxhall',
+ 'Vax' => 'vax',
+ 'Velvet' => 'velvet',
+ 'Venice' => 'venice',
+ 'Versace' => 'versace',
+ 'Vibrator' => 'vibrator',
+ 'Victorinox' => 'victorinox',
+ 'Vileda' => 'vileda',
+ 'Vinyl' => 'vinyl',
+ 'Virgin' => 'virgin',
+ 'Vitamix' => 'vitamix',
+ 'Vodafone' => 'vodafone',
+ 'Vodka' => 'vodka',
+ 'Volvo' => 'volvo',
+ 'VPN' => 'vpn',
+ 'VR' => 'vr',
+ 'VTech' => 'vtech',
+ 'Vue' => 'vue',
+ 'VW' => 'vw',
+ 'Wacom' => 'wacom',
+ 'Waffle Maker' => 'waffle-maker',
+ 'Wahl' => 'wahl',
+ 'Walkers' => 'walkers',
+ 'Walking Boots' => 'walking-boots',
+ 'Walking Dead' => 'walking-dead',
+ 'Wallet' => 'wallet',
+ 'Wallpaper' => 'wallpaper',
+ 'Walsall' => 'walsall',
+ 'Wardrobe' => 'wardrobe',
+ 'Warhammer' => 'warhammer',
+ 'Washer Dryer' => 'washer-dryer',
+ 'Washing Machine' => 'washing-machine',
+ 'Watch' => 'watch',
+ 'Watch Dogs' => 'watch-dogs',
+ 'Watch Dogs 2' => 'watch-dogs-2',
+ 'Water Bottle' => 'water-bottle',
+ 'Water Butt' => 'water-butt',
+ 'Water Filter' => 'water-filter',
+ 'Wayfair' => 'wayfair',
+ 'Webcam' => 'webcam',
+ 'Weber' => 'weber',
+ 'Wedding' => 'wedding',
+ 'Weed' => 'weed',
+ 'Weekend Break' => 'weekend-break',
+ 'Weetabix' => 'weetabix',
+ 'Weight Watchers' => 'weight-watchers',
+ 'Wellies' => 'wellies',
+ 'Wenger' => 'wenger',
+ 'Western Digital' => 'western-digital',
+ 'Wetsuit' => 'wetsuit',
+ 'Wheelbarrow' => 'wheelbarrow',
+ 'Whey' => 'whey',
+ 'Whiskas' => 'whiskas',
+ 'Whisky' => 'whisky',
+ 'Wifi Camera' => 'wifi-camera',
+ 'Wifi Extender' => 'wifi-extender',
+ 'Wii' => 'wii',
+ 'Wii U' => 'wii-u',
+ 'Wii U Pro Controller' => 'wii-u-pro-controller',
+ 'Wileyfox' => 'wileyfox',
+ 'Wilkinson Sword' => 'wilkinson-sword',
+ 'Wimbledon' => 'wimbledon',
+ 'Windows' => 'windows',
+ 'Windows 10' => 'windows-10',
+ 'Wine' => 'wine',
+ 'Wipes' => 'wipes',
+ 'Wireless Headphones' => 'wireless-headphones',
+ 'Wireless Keyboard' => 'wireless-keyboard',
+ 'Witcher' => 'witcher',
+ 'Wok' => 'wok',
+ 'Wolfenstein' => 'wolfenstein',
+ 'Women Fashion' => 'womens-clothes',
+ 'Workbench' => 'workbench',
+ 'World Of Warcraft' => 'world-of-warcraft',
+ 'Worx' => 'worx',
+ 'Wuaki' => 'wuaki',
+ 'WWE' => 'wwe',
+ 'Xbox' => 'xbox',
+ 'Xbox 360' => 'xbox-360',
+ 'Xbox 360 Game' => 'xbox-360-game',
+ 'Xbox Controller' => 'xbox-controller',
+ 'Xbox Gift Card' => 'xbox-gift-card',
+ 'Xbox Headset' => 'xbox-headset',
+ 'Xbox Live' => 'xbox-live',
+ 'Xbox One' => 'xbox-one',
+ 'Xbox One Controller' => 'xbox-one-controller',
+ 'Xbox One Elite Controller' => 'xbox-one-elite-controller',
+ 'Xbox One Games' => 'xbox-one-games',
+ 'Xbox One S' => 'xbox-one-s',
+ 'Xbox One X' => 'xbox-one-x',
+ 'Xbox Wireless Adapter' => 'xbox-wireless-adapter',
+ 'Xcom' => 'xcom',
+ 'XCOM 2' => 'xcom-2',
+ 'XFX' => 'xfx',
+ 'Xiaomi' => 'xiaomi',
+ 'Xiaomi Redmi' => 'redmi',
+ 'Xperia' => 'xperia',
+ 'Xperia Z3' => 'xperia-z3',
+ 'Xperia Z5' => 'xperia-z5',
+ 'XPS' => 'xps',
+ 'Yakuza' => 'yakuza',
+ 'Yale' => 'yale',
+ 'Yamaha' => 'yamaha',
+ 'Yankee Candle' => 'yankee-candle',
+ 'Yoga' => 'yoga',
+ 'York' => 'york',
+ 'Yorkshire' => 'yorkshire',
+ 'Yoshi' => 'yoshi',
+ 'Youview' => 'youview',
+ 'Yves Saint Laurent' => 'yves-saint-laurent',
+ 'Zante' => 'zante',
+ 'Zanussi' => 'zanussi',
+ 'Zelda' => 'zelda',
+ 'Zelda Breath Of The Wild' => 'zelda-breath-of-the-wild',
+ 'Zenbook' => 'zenbook',
+ 'Zippo' => 'zippo',
+ 'Zizzi' => 'zizzi',
+ 'Zoo' => 'zoo',
+ 'Zoostorm' => 'zoostorm',
+ 'ZOTAC' => 'zotac',
+ 'ZTE' => 'zte',
+ 'ZyXEL' => 'zyxel',
+ )
+ ),
+ 'order' => array(
+ 'name' => 'Order by',
+ 'type' => 'list',
+ 'required' => 'true',
+ 'title' => 'Sort order of deals',
+ 'values' => array(
+ 'From the most to the least hot deal' => '-hot',
+ 'From the most recent deal to the oldest' => '',
+ 'From the most commented deal to the least commented deal' => '-discussed'
+ )
+ )
+ )
+ );
+
+ public $lang = array(
+ 'bridge-uri' => SELF::URI,
+ 'bridge-name' => SELF::NAME,
+ 'context-keyword' => 'Search by keyword(s))',
+ 'context-group' => 'Deals per group',
+ 'uri-group' => '/tag/',
+ 'request-error' => 'Could not request HotUKDeals',
+ 'no-results' => 'Ooops, looks like we could',
+ 'relative-date-indicator' => array(
+ 'ago',
+ ),
+ 'price' => 'Price',
+ 'shipping' => 'Shipping',
+ 'origin' => 'Origin',
+ 'discount' => 'Discount',
+ 'title-keyword' => 'Search',
+ 'title-group' => 'Group',
+ 'local-months' => array(
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Occ',
+ 'Nov',
+ 'Dec',
+ 'st',
+ 'nd',
+ 'rd',
+ 'th'
+ ),
+ 'local-time-relative' => array(
+ 'Found ',
+ 'm',
+ 'h,',
+ 'day',
+ 'days',
+ 'month',
+ 'year',
+ 'and '
+ ),
+ 'date-prefixes' => array(
+ 'Found ',
+ 'Refreshed ',
+ 'Made hot '
+ ),
+ 'relative-date-alt-prefixes' => array(
+ 'Made hot ',
+ 'Refreshed ',
+ 'Last updated '
+ ),
+ 'relative-date-ignore-suffix' => array(
+ '/by.*$/'
+ ),
+ 'localdeal' => array(
+ 'Local',
+ 'Expires'
+ )
+ );
+
+}
diff --git a/bridges/IPBBridge.php b/bridges/IPBBridge.php
new file mode 100644
index 0000000..5b9d0e0
--- /dev/null
+++ b/bridges/IPBBridge.php
@@ -0,0 +1,310 @@
+<?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' => 'Specifies the number of items to return on each request (-1: all)',
+ 'defaultValue' => 10
+ )
+ )
+ );
+ 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
+
+ while(true) {
+ $next = $this->$callback($html, is_null($next));
+
+ if(is_null($next) || ($limit > 0 && count($this->items) >= $limit)) {
+ break;
+ }
+
+ $html = getSimpleHTMLDOMCached($next);
+ }
+
+ // We might have more items than specified, remove excess
+ $this->items = array_slice($this->items, 0, $limit);
+ }
+
+ 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/IdenticaBridge.php b/bridges/IdenticaBridge.php
new file mode 100644
index 0000000..ef52998
--- /dev/null
+++ b/bridges/IdenticaBridge.php
@@ -0,0 +1,52 @@
+<?php
+class IdenticaBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Identica Bridge';
+ const URI = 'https://identi.ca/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns user timelines';
+
+ const PARAMETERS = array( array(
+ 'u' => array(
+ 'name' => 'username',
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Requested username can\'t be found.');
+
+ foreach($html->find('li.major') as $dent) {
+ $item = array();
+
+ // get dent link
+ $item['uri'] = html_entity_decode($dent->find('a', 0)->href);
+
+ // extract dent timestamp
+ $item['timestamp'] = strtotime($dent->find('abbr.easydate', 0)->plaintext);
+
+ // extract dent text
+ $item['content'] = trim($dent->find('div.activity-content', 0)->innertext);
+ $item['title'] = $this->getInput('u') . ' | ' . $item['content'];
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('u'))) {
+ return $this->getInput('u') . ' - Identica Bridge';
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('u'))) {
+ return self::URI . urlencode($this->getInput('u'));
+ }
+
+ return parent::getURI();
+ }
+}
diff --git a/bridges/InstagramBridge.php b/bridges/InstagramBridge.php
new file mode 100644
index 0000000..2539da2
--- /dev/null
+++ b/bridges/InstagramBridge.php
@@ -0,0 +1,149 @@
+<?php
+class InstagramBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'pauder';
+ const NAME = 'Instagram Bridge';
+ const URI = 'https://instagram.com/';
+ const DESCRIPTION = 'Returns the newest images';
+
+ const PARAMETERS = array(
+ array(
+ 'u' => array(
+ 'name' => 'username',
+ 'required' => true
+ )
+ ),
+ array(
+ 'h' => array(
+ 'name' => 'hashtag',
+ 'required' => true
+ )
+ ),
+ 'global' => array(
+ 'media_type' => array(
+ 'name' => 'Media type',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => array(
+ 'All' => 'all',
+ 'Story' => 'story',
+ 'Video' => 'video',
+ 'Picture' => 'picture',
+ ),
+ 'defaultValue' => 'all'
+ )
+ )
+
+ );
+
+ public function collectData(){
+
+ if(!is_null($this->getInput('h')) && $this->getInput('media_type') == 'story') {
+ returnClientError('Stories are not supported for hashtags!');
+ }
+
+ $data = $this->getInstagramJSON($this->getURI());
+
+ if(!is_null($this->getInput('u'))) {
+ $userMedia = $data->entry_data->ProfilePage[0]->graphql->user->edge_owner_to_timeline_media->edges;
+ } else {
+ $userMedia = $data->entry_data->TagPage[0]->graphql->hashtag->edge_hashtag_to_media->edges;
+ }
+
+ foreach($userMedia as $media) {
+ $media = $media->node;
+
+ if(!is_null($this->getInput('u'))) {
+ switch($this->getInput('media_type')) {
+ case 'all': break;
+ case 'video':
+ if($media->__typename != 'GraphVideo') continue 2;
+ break;
+ case 'picture':
+ if($media->__typename != 'GraphImage') continue 2;
+ break;
+ case 'story':
+ if($media->__typename != 'GraphSidecar') continue 2;
+ break;
+ default: break;
+ }
+ } else {
+ if($this->getInput('media_type') == 'video' && !$media->is_video) continue;
+ }
+
+ $item = array();
+ $item['uri'] = self::URI . 'p/' . $media->shortcode . '/';
+
+ if (isset($media->edge_media_to_caption->edges[0]->node->text)) {
+ $item['title'] = $media->edge_media_to_caption->edges[0]->node->text;
+ } else {
+ $item['title'] = basename($media->display_url);
+ }
+
+ if(!is_null($this->getInput('u')) && $media->__typename == 'GraphSidecar') {
+ $data = $this->getInstagramStory($item['uri']);
+ $item['content'] = $data[0];
+ $item['enclosures'] = $data[1];
+ } else {
+ $item['content'] = '<img src="' . htmlentities($media->display_url) . '" alt="'. $item['title'] . '" />';
+ $item['enclosures'] = array($media->display_url);
+ }
+
+ $item['timestamp'] = $media->taken_at_timestamp;
+
+ $this->items[] = $item;
+ }
+ }
+
+ protected function getInstagramStory($uri) {
+
+ $data = $this->getInstagramJSON($uri);
+ $mediaInfo = $data->entry_data->PostPage[0]->graphql->shortcode_media;
+
+ //Process the first element, that isn't in the node graph
+ $caption = $mediaInfo->edge_media_to_caption->edges[0]->node->text;
+
+ $enclosures = [$mediaInfo->display_url];
+ $content = '<img src="' . htmlentities($mediaInfo->display_url) . '" alt="'. $caption . '" />';
+
+ foreach($mediaInfo->edge_sidecar_to_children->edges as $media) {
+
+ $content .= '<img src="' . htmlentities($media->node->display_url) . '" alt="'. $caption . '" />';
+ $enclosures[] = $media->node->display_url;
+
+ }
+
+ return [$content, $enclosures];
+
+ }
+
+ protected function getInstagramJSON($uri) {
+
+ $html = getContents($uri)
+ or returnServerError('Could not request Instagram.');
+ $scriptRegex = '/window\._sharedData = (.*);<\/script>/';
+
+ preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0);
+
+ return json_decode($matches[1][0]);
+
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('u'))) {
+ return $this->getInput('u') . ' - Instagram Bridge';
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('u'))) {
+ return self::URI . urlencode($this->getInput('u'));
+ } elseif(!is_null($this->getInput('h'))) {
+ return self::URI . 'explore/tags/' . urlencode($this->getInput('h'));
+ }
+
+ return parent::getURI();
+ }
+}
diff --git a/bridges/JapanExpoBridge.php b/bridges/JapanExpoBridge.php
new file mode 100644
index 0000000..c80bb24
--- /dev/null
+++ b/bridges/JapanExpoBridge.php
@@ -0,0 +1,100 @@
+<?php
+class JapanExpoBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Ginko';
+ const NAME = 'Japan Expo Actualités';
+ const URI = 'http://www.japan-expo-paris.com/fr/actualites';
+ const CACHE_TIMEOUT = 14400; // 4h
+ const DESCRIPTION = 'Returns most recent entries from Japan Expo actualités.';
+ const PARAMETERS = array( array(
+ 'mode' => array(
+ 'name' => 'Show full contents',
+ 'type' => 'checkbox',
+ )
+ ));
+
+ public function collectData(){
+
+ function frenchPubDateToTimestamp($date_to_parse) {
+ return strtotime(
+ strtr(
+ strtolower(str_replace('Publié le ', '', $date_to_parse)),
+ array(
+ 'janvier' => 'jan',
+ 'février' => 'feb',
+ 'mars' => 'march',
+ 'avril' => 'apr',
+ 'mai' => 'may',
+ 'juin' => 'jun',
+ 'juillet' => 'jul',
+ 'août' => 'aug',
+ 'septembre' => 'sep',
+ 'octobre' => 'oct',
+ 'novembre' => 'nov',
+ 'décembre' => 'dec'
+ )
+ )
+ );
+ }
+
+ $convert_article_images = function($matches){
+ if(is_array($matches) && count($matches) > 1) {
+ return '<img src="' . $matches[1] . '" />';
+ }
+ };
+
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request JapanExpo: ' . self::URI);
+ $fullcontent = $this->getInput('mode');
+ $count = 0;
+
+ foreach($html->find('a._tile2') as $element) {
+
+ $url = $element->href;
+ $thumbnail = 'http://s.japan-expo.com/katana/images/JES049/paris.png';
+ preg_match('/url\(([^)]+)\)/', $element->find('img.rspvimgset', 0)->style, $img_search_result);
+
+ if(count($img_search_result) >= 2)
+ $thumbnail = trim($img_search_result[1], "'");
+
+ if($fullcontent) {
+ if($count >= 5) {
+ break;
+ }
+
+ $article_html = getSimpleHTMLDOMCached('Could not request JapanExpo: ' . $url);
+ $header = $article_html->find('header.pageHeadBox', 0);
+ $timestamp = strtotime($header->find('time', 0)->datetime);
+ $title_html = $header->find('div.section', 0)->next_sibling();
+ $title = $title_html->plaintext;
+ $headings = $title_html->next_sibling()->outertext;
+ $article = $article_html->find('div.content', 0)->innertext;
+ $article = preg_replace_callback(
+ '/<img [^>]+ style="[^\(]+\(\'([^\']+)\'[^>]+>/i',
+ $convert_article_images,
+ $article);
+
+ $content = $headings . $article;
+ } else {
+ $date_text = $element->find('span.date', 0)->plaintext;
+ $timestamp = frenchPubDateToTimestamp($date_text);
+ $title = trim($element->find('span._title', 0)->plaintext);
+ $content = '<img src="'
+ . $thumbnail
+ . '"></img><br />'
+ . $date_text
+ . '<br /><a href="'
+ . $url
+ . '">Lire l\'article</a>';
+ }
+
+ $item = array();
+ $item['uri'] = $url;
+ $item['title'] = $title;
+ $item['timestamp'] = $timestamp;
+ $item['content'] = $content;
+ $this->items[] = $item;
+ $count++;
+ }
+ }
+}
diff --git a/bridges/JustETFBridge.php b/bridges/JustETFBridge.php
new file mode 100644
index 0000000..f0223e9
--- /dev/null
+++ b/bridges/JustETFBridge.php
@@ -0,0 +1,353 @@
+<?php
+class JustETFBridge extends BridgeAbstract {
+ const NAME = 'justETF Bridge';
+ const URI = 'https://www.justetf.com';
+ const DESCRIPTION = 'Currently only supports the news feed';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = array(
+ 'News' => array(
+ 'full' => array(
+ 'name' => 'Full Article',
+ 'type' => 'checkbox',
+ 'title' => 'Enable to load full articles'
+ )
+ ),
+ 'Profile' => array(
+ 'isin' => array(
+ 'name' => 'ISIN',
+ 'type' => 'text',
+ 'required' => true,
+ 'pattern' => '[a-zA-Z]{2}[a-zA-Z0-9]{10}',
+ 'title' => 'ISIN, consisting of 2-letter country code, 9-character identifier, check character'
+ ),
+ 'strategy' => array(
+ 'name' => 'Include Strategy',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'description' => array(
+ 'name' => 'Include Description',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ )
+ ),
+ 'global' => array(
+ 'lang' => array(
+ 'name' => 'Language',
+ 'required' => true,
+ 'type' => 'list',
+ 'values' => array(
+ 'Englisch' => 'en',
+ 'Deutsch' => 'de',
+ 'Italiano' => 'it'
+ ),
+ 'defaultValue' => 'Englisch'
+ )
+ )
+ );
+
+ public function collectData() {
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Failed loading contents from ' . $this->getURI());
+
+ defaultLinkTo($html, static::URI);
+
+ switch($this->queriedContext) {
+ case 'News':
+ $this->collectNews($html);
+ break;
+ case 'Profile':
+ $this->collectProfile($html);
+ break;
+ }
+ }
+
+ public function getURI() {
+ $uri = static::URI;
+
+ if($this->getInput('lang')) {
+ $uri .= '/' . $this->getInput('lang');
+ }
+
+ switch($this->queriedContext) {
+ case 'News':
+ $uri .= '/news';
+ break;
+ case 'Profile':
+ $uri .= '/etf-profile.html?' . http_build_query(array(
+ 'isin' => strtoupper($this->getInput('isin'))
+ ));
+ break;
+ }
+
+ return $uri;
+ }
+
+ public function getName() {
+ $name = static::NAME;
+
+ $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : '';
+
+ switch($this->queriedContext) {
+ case 'News': break;
+ case 'Profile':
+ if($this->getInput('isin')) {
+ $name .= ' ISIN ' . strtoupper($this->getInput('isin'));
+ }
+ }
+
+ if($this->getInput('lang')) {
+ $name .= ' (' . strtoupper($this->getInput('lang')) . ')';
+ }
+
+ return $name;
+ }
+
+ #region Common
+
+ /**
+ * Fixes dates depending on the choosen language:
+ *
+ * de : dd.mm.yy
+ * en : dd.mm.yy
+ * it : dd/mm/yy
+ *
+ * Basically strtotime doesn't convert dates correctly due to formats
+ * being hard to interpret. So we use the DateTime object, manually
+ * fixing dates and times (set to 00:00:00.000).
+ *
+ * We don't know the timezone, so just assume +00:00 (or whatever
+ * DateTime chooses)
+ */
+ private function fixDate($date) {
+ switch($this->getInput('lang')) {
+ case 'en':
+ case 'de':
+ $df = date_create_from_format('d.m.y', $date);
+ break;
+ case 'it':
+ $df = date_create_from_format('d/m/y', $date);
+ break;
+ }
+
+ date_time_set($df, 0, 0);
+
+ // debugMessage(date_format($df, 'U'));
+
+ return date_format($df, 'U');
+ }
+
+ private function extractImages($article) {
+ // Notice: We can have zero or more images (though it should mostly be 1)
+ $elements = $article->find('img');
+
+ $images = array();
+
+ foreach($elements as $img) {
+ // Skip the logo (mostly provided part of a hidden div)
+ if(substr($img->src, strrpos($img->src, '/') + 1) === 'logo.png')
+ continue;
+
+ $images[] = $img->src;
+ }
+
+ return $images;
+ }
+
+ #endregion
+
+ #region News
+
+ private function collectNews($html) {
+ $articles = $html->find('div.newsTopArticle')
+ or returnServerError('No articles found! Layout might have changed!');
+
+ foreach($articles as $article) {
+
+ $item = array();
+
+ // Common data
+
+ $item['uri'] = $this->extractNewsUri($article);
+ $item['timestamp'] = $this->extractNewsDate($article);
+ $item['title'] = $this->extractNewsTitle($article);
+
+ if($this->getInput('full')) {
+
+ $uri = $this->extractNewsUri($article);
+
+ $html = getSimpleHTMLDOMCached($uri)
+ or returnServerError('Failed loading full article from ' . $uri);
+
+ $fullArticle = $html->find('div.article', 0)
+ or returnServerError('No content found! Layout might have changed!');
+
+ defaultLinkTo($fullArticle, static::URI);
+
+ $item['author'] = $this->extractFullArticleAuthor($fullArticle);
+ $item['content'] = $this->extractFullArticleContent($fullArticle);
+ $item['enclosures'] = $this->extractImages($fullArticle);
+
+ } else {
+
+ $item['content'] = $this->extractNewsDescription($article);
+ $item['enclosures'] = $this->extractImages($article);
+
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function extractNewsUri($article) {
+ $element = $article->find('a', 0)
+ or returnServerError('Anchor not found!');
+
+ return $element->href;
+ }
+
+ private function extractNewsDate($article) {
+ $element = $article->find('div.subheadline', 0)
+ or returnServerError('Date not found!');
+
+ // debugMessage($element->plaintext);
+
+ $date = trim(explode('|', $element->plaintext)[0]);
+
+ return $this->fixDate($date);
+ }
+
+ private function extractNewsDescription($article) {
+ $element = $article->find('span.newsText', 0)
+ or returnServerError('Description not found!');
+
+ $element->find('a', 0)->onclick = '';
+
+ // debugMessage($element->innertext);
+
+ return $element->innertext;
+ }
+
+ private function extractNewsTitle($article) {
+ $element = $article->find('h3', 0)
+ or returnServerError('Title not found!');
+
+ return $element->plaintext;
+ }
+
+ private function extractFullArticleContent($article) {
+ $element = $article->find('div.article_body', 0)
+ or returnServerError('Article body not found!');
+
+ // Remove teaser image
+ $element->find('img.teaser-img', 0)->outertext = '';
+
+ // Remove self advertisements
+ foreach($element->find('.call-action') as $adv) {
+ $adv->outertext = '';
+ }
+
+ // Remove tips
+ foreach($element->find('.panel-edu') as $tip) {
+ $tip->outertext = '';
+ }
+
+ // Remove inline scripts (used for i.e. interactive graphs) as they are
+ // rendered as a long series of strings
+ foreach($element->find('script') as $script) {
+ $script->outertext = '[Content removed! Visit site to see full contents!]';
+ }
+
+ return $element->innertext;
+ }
+
+ private function extractFullArticleAuthor($article) {
+ $element = $article->find('span[itemprop=name]', 0)
+ or returnServerError('Author not found!');
+
+ return $element->plaintext;
+ }
+
+ #endregion
+
+ #region Profile
+
+ private function collectProfile($html) {
+ $item = array();
+
+ $item['uri'] = $this->getURI();
+ $item['timestamp'] = $this->extractProfileDate($html);
+ $item['title'] = $this->extractProfiletitle($html);
+ $item['author'] = $this->extractProfileAuthor($html);
+ $item['content'] = $this->extractProfileContent($html);
+
+ $this->items[] = $item;
+ }
+
+ private function extractProfileDate($html) {
+ $element = $html->find('div.infobox div.vallabel', 0)
+ or returnServerError('Date not found!');
+
+ // debugMessage($element->plaintext);
+
+ $date = trim(explode("\r\n", $element->plaintext)[1]);
+
+ return $this->fixDate($date);
+ }
+
+ private function extractProfileTitle($html) {
+ $element = $html->find('span.h1', 0)
+ or returnServerError('Title not found!');
+
+ return $element->plaintext;
+ }
+
+ private function extractProfileContent($html) {
+ // There are a few thins we are interested:
+ // - Investment Strategy
+ // - Description
+ // - Quote
+
+ $strategy = $html->find('div.tab-container div.col-sm-6 p', 0)
+ or returnServerError('Investment Strategy not found!');
+
+ // Description requires a bit of cleanup due to lack of propper identification
+
+ $description = $html->find('div.headline', 5)
+ or returnServerError('Description container not found!');
+
+ $description = $description->parent();
+
+ foreach($description->find('div') as $div) {
+ $div->outertext = '';
+ }
+
+ $quote = $html->find('div.infobox div.val', 0)
+ or returnServerError('Quote not found!');
+
+ $quote_html = '<strong>Quote</strong><br><p>' . $quote . '</p>';
+ $strategy_html = '';
+ $description_html = '';
+
+ if($this->getInput('strategy') === true) {
+ $strategy_html = '<strong>Strategy</strong><br><p>' . $strategy . '</p><br>';
+ }
+
+ if($this->getInput('description') === true) {
+ $description_html = '<strong>Description</strong><br><p>' . $description . '</p><br>';
+ }
+
+ return $strategy_html . $description_html . $quote_html;
+ }
+
+ private function extractProfileAuthor($html) {
+ // Use ISIN + WKN as author
+ // Notice: "identfier" is not a typo [sic]!
+ $element = $html->find('span.identfier', 0)
+ or returnServerError('Author not found!');
+
+ return $element->plaintext;
+ }
+
+ #endregion
+}
diff --git a/bridges/KATBridge.php b/bridges/KATBridge.php
new file mode 100644
index 0000000..c4325a6
--- /dev/null
+++ b/bridges/KATBridge.php
@@ -0,0 +1,123 @@
+<?php
+class KATBridge extends BridgeAbstract {
+ const MAINTAINER = 'niawag';
+ const NAME = 'KickassTorrents';
+ const URI = 'https://katcr.co/new/';
+ const DESCRIPTION = 'Returns results for the keywords. You can put several
+ list of keywords by separating them with a semicolon (e.g. "one show;another
+ show"). Category based search needs the category number as input. User based
+ search takes the Uploader ID: see KAT URL for user feed. Search can be done in a specified category';
+
+ const PARAMETERS = array( array(
+ 'q' => array(
+ 'name' => 'keywords, separated by semicolons',
+ 'exampleValue' => 'first list;second list;…',
+ 'required' => true
+ ),
+ 'crit' => array(
+ 'type' => 'list',
+ 'name' => 'Search type',
+ 'values' => array(
+ 'search' => 'search',
+ 'category' => 'cat',
+ 'user' => 'usr'
+ )
+ ),
+ 'cat_check' => array(
+ 'type' => 'checkbox',
+ 'name' => 'Specify category for normal search ?',
+ ),
+ 'cat' => array(
+ 'name' => 'Category number',
+ 'exampleValue' => '100, 200… See KAT for category number'
+ ),
+ 'trusted' => array(
+ 'type' => 'checkbox',
+ 'name' => 'Only get results from Elite or Verified uploaders ?',
+ ),
+ ));
+ public function collectData(){
+ function parseDateTimestamp($element){
+ $guessedDate = strptime($element, '%d-%m-%Y %H:%M:%S');
+ $timestamp = mktime(
+ $guessedDate['tm_hour'],
+ $guessedDate['tm_min'],
+ $guessedDate['tm_sec'],
+ $guessedDate['tm_mon'] + 1,
+ $guessedDate['tm_mday'],
+ $guessedDate['tm_year'] + 1900);
+ return $timestamp;
+ }
+ $catBool = $this->getInput('cat_check');
+ if($catBool) {
+ $catNum = $this->getInput('cat');
+ }
+ $critList = $this->getInput('crit');
+ $trustedBool = $this->getInput('trusted');
+ $keywordsList = explode(';', $this->getInput('q'));
+ foreach($keywordsList as $keywords) {
+ switch($critList) {
+ case 'search':
+ if($catBool == false) {
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'torrents-search.php?search=' .
+ rawurlencode($keywords)
+ ) or returnServerError('Could not request KAT.');
+ } else {
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'torrents-search.php?search=' .
+ rawurlencode($keywords) .
+ '&cat=' .
+ rawurlencode($catNum)
+ ) or returnServerError('Could not request KAT.');
+ }
+ break;
+ case 'cat':
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'torrents.php?cat=' .
+ rawurlencode($keywords)
+ ) or returnServerError('Could not request KAT.');
+ break;
+ case 'usr':
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'account-details.php?id=' .
+ rawurlencode($keywords)
+ ) or returnServerError('Could not request KAT.');
+ break;
+ }
+ if ($html->find('table.ttable_headinner', 0) == false)
+ returnServerError('No result for query ' . $keywords);
+ foreach($html->find('tr.t-row') as $element) {
+ if(!$trustedBool
+ || !is_null($element->find('i[title="Elite Uploader"]', 0))
+ || !is_null($element->find('i[title="Verified Uploader"]', 0))) {
+ $item = array();
+ $item['uri'] = self::URI . $element->find('a', 2)->href;
+ $item['id'] = self::URI . $element->find('a.cellMainLink', 0)->href;
+ $item['timestamp'] = parseDateTimestamp($element->find('td', 2)->plaintext);
+ $item['author'] = $element->find('a.plain', 0)->plaintext;
+ $item['title'] = $element->find('a.cellMainLink', 0)->plaintext;
+ $item['seeders'] = (int)$element->find('td', 3)->plaintext;
+ $item['leechers'] = (int)$element->find('td', 4)->plaintext;
+ $item['size'] = $element->find('td', 1)->plaintext;
+ $item['content'] = $item['title']
+ . '<br>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/KernelBugTrackerBridge.php b/bridges/KernelBugTrackerBridge.php
new file mode 100644
index 0000000..f3135af
--- /dev/null
+++ b/bridges/KernelBugTrackerBridge.php
@@ -0,0 +1,150 @@
+<?php
+class KernelBugTrackerBridge extends BridgeAbstract {
+
+ const NAME = 'Kernel Bug Tracker';
+ const URI = 'https://bugzilla.kernel.org';
+ const DESCRIPTION = 'Returns feeds for bug comments';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = array(
+ 'Bug comments' => array(
+ 'id' => array(
+ 'name' => 'Bug tracking ID',
+ 'type' => 'number',
+ 'required' => true,
+ 'title' => 'Insert bug tracking ID',
+ 'exampleValue' => 121241
+ ),
+ 'limit' => array(
+ 'name' => 'Number of comments to return',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Specify number of comments to return',
+ 'defaultValue' => -1
+ ),
+ 'sorting' => array(
+ 'name' => 'Sorting',
+ 'type' => 'list',
+ 'required' => false,
+ 'title' => 'Defines the sorting order of the comments returned',
+ 'defaultValue' => 'of',
+ 'values' => array(
+ 'Oldest first' => 'of',
+ 'Latest first' => 'lf'
+ )
+ )
+ )
+ );
+
+ private $bugid = '';
+ private $bugdesc = '';
+
+ public function collectData(){
+ $limit = $this->getInput('limit');
+ $sorting = $this->getInput('sorting');
+
+ // We use the print preview page for simplicity
+ $html = getSimpleHTMLDOMCached($this->getURI() . '&format=multiple',
+ 86400,
+ null,
+ null,
+ true,
+ true,
+ DEFAULT_TARGET_CHARSET,
+ false, // Do NOT remove line breaks
+ DEFAULT_BR_TEXT,
+ DEFAULT_SPAN_TEXT);
+
+ if($html === false)
+ returnServerError('Failed to load page!');
+
+ // Store header information into private members
+ $this->bugid = $html->find('#bugzilla-body', 0)->find('a', 0)->innertext;
+ $this->bugdesc = $html->find('table.bugfields', 0)->find('tr', 0)->find('td', 0)->innertext;
+
+ // Get and limit comments
+ $comments = $html->find('div.bz_comment');
+
+ if($limit > 0 && count($comments) > $limit) {
+ $comments = array_slice($comments, count($comments) - $limit, $limit);
+ }
+
+ // Order comments
+ switch($sorting) {
+ case 'lf': $comments = array_reverse($comments, true);
+ case 'of':
+ default: // Nothing to do, keep original order
+ }
+
+ foreach($comments as $comment) {
+ $comment = $this->inlineStyles($comment);
+
+ $item = array();
+ $item['uri'] = $this->getURI() . '#' . $comment->id;
+ $item['author'] = $comment->find('span.bz_comment_user', 0)->innertext;
+ $item['title'] = $comment->find('span.bz_comment_number', 0)->find('a', 0)->innertext;
+ $item['timestamp'] = strtotime($comment->find('span.bz_comment_time', 0)->innertext);
+ $item['content'] = $comment->find('pre.bz_comment_text', 0)->innertext;
+
+ // Fix line breaks (they use LF)
+ $item['content'] = str_replace("\n", '<br>', $item['content']);
+
+ // Fix relative URIs
+ $item['content'] = $this->replaceRelativeURI($item['content']);
+
+ $this->items[] = $item;
+ }
+
+ }
+
+ public function getURI(){
+ switch($this->queriedContext) {
+ case 'Bug comments':
+ return parent::getURI()
+ . '/show_bug.cgi?id='
+ . $this->getInput('id');
+ break;
+ default: return parent::getURI();
+ }
+ }
+
+ public function getName(){
+ switch($this->queriedContext) {
+ case 'Bug comments':
+ return 'Bug '
+ . $this->bugid
+ . ' tracker for '
+ . $this->bugdesc
+ . ' - '
+ . parent::getName();
+ break;
+ default: return parent::getName();
+ }
+ }
+
+ /**
+ * Replaces all relative URIs with absolute ones
+ *
+ * @param string $content The source string
+ * @return string Returns the source string with all relative URIs replaced
+ * by absolute ones.
+ */
+ private function replaceRelativeURI($content){
+ return preg_replace('/href="(?!http)/', 'href="' . self::URI . '/', $content);
+ }
+
+ /**
+ * Adds styles as attributes to tags with known classes
+ *
+ * @param object $html A simplehtmldom object
+ * @return object Returns the original object with styles added as
+ * attributes.
+ */
+ private function inlineStyles($html){
+ foreach($html->find('.bz_obsolete') as $element) {
+ $element->style = 'text-decoration:line-through;';
+ }
+
+ return $html;
+ }
+
+}
diff --git a/bridges/KonachanBridge.php b/bridges/KonachanBridge.php
new file mode 100644
index 0000000..4250e8b
--- /dev/null
+++ b/bridges/KonachanBridge.php
@@ -0,0 +1,11 @@
+<?php
+require_once('MoebooruBridge.php');
+
+class KonachanBridge extends MoebooruBridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Konachan';
+ const URI = 'http://konachan.com/';
+ const DESCRIPTION = 'Returns images from given page';
+
+}
diff --git a/bridges/KoreusBridge.php b/bridges/KoreusBridge.php
new file mode 100644
index 0000000..a5e09cb
--- /dev/null
+++ b/bridges/KoreusBridge.php
@@ -0,0 +1,22 @@
+<?php
+class KoreusBridge extends FeedExpander {
+
+ const MAINTAINER = 'pit-fgfjiudghdf';
+ const NAME = 'Koreus';
+ const URI = 'http://www.koreus.com/';
+ const DESCRIPTION = 'Returns the newest posts from Koreus (full text)';
+
+ protected function parseItem($item){
+ $item = parent::parseItem($item);
+
+ $html = getSimpleHTMLDOMCached($item['uri']);
+ $text = $html->find('p.itemText', 0)->innertext;
+ $item['content'] = utf8_encode($text);
+
+ return $item;
+ }
+
+ public function collectData(){
+ $this->collectExpandableDatas('http://feeds.feedburner.com/Koreus-articles');
+ }
+}
diff --git a/bridges/KununuBridge.php b/bridges/KununuBridge.php
new file mode 100644
index 0000000..85f7c7b
--- /dev/null
+++ b/bridges/KununuBridge.php
@@ -0,0 +1,249 @@
+<?php
+class KununuBridge extends BridgeAbstract {
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Kununu Bridge';
+ const URI = 'https://www.kununu.com/';
+ const CACHE_TIMEOUT = 86400; // 24h
+ const DESCRIPTION = 'Returns the latest reviews for a company and site of your choice.';
+
+ const PARAMETERS = array(
+ 'global' => array(
+ 'site' => array(
+ 'name' => 'Site',
+ 'type' => 'list',
+ 'required' => true,
+ 'title' => 'Select your site',
+ 'values' => array(
+ 'Austria' => 'at',
+ 'Germany' => 'de',
+ 'Switzerland' => 'ch',
+ 'United States' => 'us'
+ )
+ ),
+ 'full' => array(
+ 'name' => 'Load full article',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'exampleValue' => 'checked',
+ 'title' => 'Activate to load full article'
+ )
+ ),
+ array(
+ 'company' => array(
+ 'name' => 'Company',
+ 'required' => true,
+ 'exampleValue' => 'kununu-us',
+ 'title' => 'Insert company name (i.e. Kununu US) or URI path (i.e. kununu-us)'
+ )
+ )
+ );
+
+ private $companyName = '';
+
+ public function getURI(){
+ if(!is_null($this->getInput('company')) && !is_null($this->getInput('site'))) {
+
+ $company = $this->fixCompanyName($this->getInput('company'));
+ $site = $this->getInput('site');
+ $section = '';
+
+ switch($site) {
+ case 'at':
+ case 'de':
+ case 'ch':
+ $section = 'kommentare';
+ break;
+ case 'us':
+ $section = 'reviews';
+ break;
+ }
+
+ return self::URI . $site . '/' . $company . '/' . $section . '?sort=update_time_desc';
+ }
+
+ return parent::getURI();
+ }
+
+ function getName(){
+ if(!is_null($this->getInput('company'))) {
+ $company = $this->fixCompanyName($this->getInput('company'));
+ return ($this->companyName ?: $company) . ' - ' . self::NAME;
+ }
+
+ return parent::getName();
+ }
+
+ public function collectData(){
+ $full = $this->getInput('full');
+
+ // Load page
+ $html = getSimpleHTMLDOMCached($this->getURI());
+ if(!$html)
+ returnServerError('Unable to receive data from ' . $this->getURI() . '!');
+ // Update name for this request
+ $this->companyName = $this->extractCompanyName($html);
+
+ // Find the section with all the panels (reviews)
+ $section = $html->find('section.kununu-scroll-element', 0);
+ if($section === false)
+ returnServerError('Unable to find panel section!');
+
+ // Find all articles (within the panels)
+ $articles = $section->find('article');
+ if($articles === false || empty($articles))
+ returnServerError('Unable to find articles!');
+
+ // Go through all articles
+ foreach($articles as $article) {
+ $item = array();
+
+ $item['author'] = $this->extractArticleAuthorPosition($article);
+ $item['timestamp'] = $this->extractArticleDate($article);
+ $item['title'] = $this->extractArticleRating($article)
+ . ' : '
+ . $this->extractArticleSummary($article);
+
+ $item['uri'] = $this->extractArticleUri($article);
+
+ if($full)
+ $item['content'] = $this->extractFullDescription($item['uri']);
+ else
+ $item['content'] = $this->extractArticleDescription($article);
+
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Fixes relative URLs in the given text
+ */
+ private function fixUrl($text){
+ return preg_replace('/href=(\'|\")\//i', 'href="'.self::URI, $text);
+ }
+
+ /*
+ * Returns a fixed version of the provided company name
+ */
+ private function fixCompanyName($company){
+ $company = trim($company);
+ $company = str_replace(' ', '-', $company);
+ $company = strtolower($company);
+ return $this->encodeUmlauts($company);
+ }
+
+ /**
+ * Encodes unmlauts in the given text
+ */
+ private function encodeUmlauts($text){
+ $umlauts = Array('/ä/','/ö/','/ü/','/Ä/','/Ö/','/Ü/','/ß/');
+ $replace = Array('ae','oe','ue','Ae','Oe','Ue','ss');
+
+ return preg_replace($umlauts, $replace, $text);
+ }
+
+ /**
+ * Returns the company name from the review html
+ */
+ private function extractCompanyName($html){
+ $company_name = $html->find('h1[itemprop=name]', 0);
+ if(is_null($company_name))
+ returnServerError('Cannot find company name!');
+
+ return $company_name->plaintext;
+ }
+
+ /**
+ * Returns the date from a given article
+ */
+ private function extractArticleDate($article){
+ // They conviniently provide a time attribute for us :)
+ $date = $article->find('meta[itemprop=dateCreated]', 0);
+ if(is_null($date))
+ returnServerError('Cannot find article date!');
+
+ return strtotime($date->content);
+ }
+
+ /**
+ * Returns the rating from a given article
+ */
+ private function extractArticleRating($article){
+ $rating = $article->find('span.rating', 0);
+ if(is_null($rating))
+ returnServerError('Cannot find article rating!');
+
+ return $rating->getAttribute('aria-label');
+ }
+
+ /**
+ * Returns the summary from a given article
+ */
+ private function extractArticleSummary($article){
+ $summary = $article->find('[itemprop=name]', 0);
+ if(is_null($summary))
+ returnServerError('Cannot find article summary!');
+
+ return strip_tags($summary->innertext);
+ }
+
+ /**
+ * Returns the URI from a given article
+ */
+ private function extractArticleUri($article){
+ $anchor = $article->find('h1.review-title a', 0);
+ if(is_null($anchor))
+ returnServerError('Cannot find article URI!');
+
+ return self::URI . $anchor->href;
+ }
+
+ /**
+ * Returns the position of the author from a given article
+ */
+ private function extractArticleAuthorPosition($article){
+ // We need to parse the user-content manually
+ $user_content = $article->find('div.user-content', 0);
+ if(is_null($user_content))
+ returnServerError('Cannot find user content!');
+
+ // Go through all h2 elements to find index of required span (I know... it's stupid)
+ $author_position = 'Unknown';
+ foreach($user_content->find('div') as $content) {
+ if(stristr(strtolower($content->plaintext), 'position')) { /* This works for at, ch, de, us */
+ $author_position = $content->next_sibling()->plaintext;
+ break;
+ }
+ }
+
+ return $author_position;
+ }
+
+ /**
+ * Returns the description from a given article
+ */
+ private function extractArticleDescription($article){
+ $description = $article->find('[itemprop=reviewBody]', 0);
+ if(is_null($description))
+ returnServerError('Cannot find article description!');
+
+ return $this->fixUrl($description->innertext);
+ }
+
+ /**
+ * Returns the full description from a given uri
+ */
+ private function extractFullDescription($uri){
+ // Load full article
+ $html = getSimpleHTMLDOMCached($uri);
+ if($html === false)
+ returnServerError('Could not load full description!');
+
+ // Find the article
+ $article = $html->find('article', 0);
+ if(is_null($article))
+ returnServerError('Cannot find article!');
+
+ // Luckily they use the same layout for the review overview and full article pages :)
+ return $this->extractArticleDescription($article);
+ }
+}
diff --git a/bridges/LWNprevBridge.php b/bridges/LWNprevBridge.php
new file mode 100644
index 0000000..0b79aeb
--- /dev/null
+++ b/bridges/LWNprevBridge.php
@@ -0,0 +1,265 @@
+<?php
+class LWNprevBridge extends BridgeAbstract{
+ const MAINTAINER = 'Pierre Mazière';
+ const NAME = 'LWN Free Weekly Edition';
+ const URI = 'https://lwn.net/';
+ const CACHE_TIMEOUT = 604800; // 1 week
+ const DESCRIPTION = 'LWN Free Weekly Edition available one week late';
+
+ private $editionTimeStamp;
+
+ function getURI(){
+ return self::URI.'free/bigpage';
+ }
+
+ private function jumpToNextTag(&$node){
+ while($node && $node->nodeType === XML_TEXT_NODE) {
+ $nextNode = $node->nextSibling;
+ if(!$nextNode) {
+ break;
+ }
+ $node = $nextNode;
+ }
+ }
+
+ private function jumpToPreviousTag(&$node){
+ while($node && $node->nodeType === XML_TEXT_NODE) {
+ $previousNode = $node->previousSibling;
+ if(!$previousNode) {
+ break;
+ }
+ $node = $previousNode;
+ }
+ }
+
+ public function collectData(){
+ // Because the LWN page is written in loose HTML and not XHTML,
+ // Simple HTML Dom is not accurate enough for the job
+ $content = getContents($this->getURI())
+ or returnServerError('No results for LWNprev');
+
+ $contents = explode('<b>Page editor</b>', $content);
+
+ foreach($contents as $content) {
+ if(strpos($content, '<html>') === false) {
+ $content = <<<EOD
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html><head><title>LWN</title></head><body>{$content}</body></html>
+EOD;
+ } else {
+ $content = $content.'</body></html>';
+ }
+
+ libxml_use_internal_errors(true);
+ $html = new DOMDocument();
+ $html->loadHTML($content);
+ libxml_clear_errors();
+
+ $edition = $html->getElementsByTagName('h1');
+ if($edition->length !== 0) {
+ $text = $edition->item(0)->textContent;
+ $this->editionTimeStamp = strtotime(
+ substr($text, strpos($text, 'for ') + strlen('for '))
+ );
+ }
+
+ if(strpos($content, 'Cat1HL') === false) {
+ $items = $this->getFeatureContents($html);
+ } elseif(strpos($content, 'Cat3HL') === false) {
+ $items = $this->getBriefItems($html);
+ } else {
+ $items = $this->getAnnouncements($html);
+ }
+
+ $this->items = array_merge($this->items, $items);
+ }
+ }
+
+ private function getArticleContent(&$title){
+ $link = $title->firstChild;
+ $this->jumpToNextTag($link);
+ $item['uri'] = self::URI;
+ if($link->nodeName === 'a') {
+ $item['uri'] .= $link->getAttribute('href');
+ }
+
+ $item['timestamp'] = $this->editionTimeStamp;
+
+ $node = $title;
+ $content = '';
+ $contentEnd = false;
+ while(!$contentEnd) {
+ $node = $node->nextSibling;
+ if(!$node || (
+ $node->nodeType !== XML_TEXT_NODE &&
+ $node->nodeName === 'h2' || (
+ !is_null($node->attributes) &&
+ !is_null($class = $node->attributes->getNamedItem('class')) &&
+ in_array($class->nodeValue, array('Cat1HL','Cat2HL'))
+ )
+ )
+ ) {
+ $contentEnd = true;
+ } else {
+ $content .= $node->C14N();
+ }
+ }
+ $item['content'] = $content;
+ return $item;
+ }
+
+ private function getFeatureContents(&$html){
+ $items = array();
+ foreach($html->getElementsByTagName('h2') as $title) {
+ if($title->getAttribute('class') !== 'SummaryHL') {
+ continue;
+ }
+
+ $item = array();
+
+ $author = $title->nextSibling;
+ $this->jumpToNextTag($author);
+ if($author->getAttribute('class') === 'FeatureByline') {
+ $item['author'] = $author->getElementsByTagName('b')->item(0)->textContent;
+ } else {
+ continue;
+ }
+
+ $item['title'] = $title->textContent;
+
+ $items[] = array_merge($item, $this->getArticleContent($title));
+ }
+ return $items;
+ }
+
+ private function getItemPrefix(&$cat, &$cats){
+ $cat1 = '';
+ $cat2 = '';
+ $cat3 = '';
+ switch($cat->getAttribute('class')) {
+ case 'Cat3HL':
+ $cat3 = $cat->textContent;
+ $cat = $cat->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $cats[2] = $cat3;
+ if($cat->getAttribute('class') !== 'Cat2HL') {
+ break;
+ }
+ case 'Cat2HL':
+ $cat2 = $cat->textContent;
+ $cat = $cat->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $cats[1] = $cat2;
+ if(empty($cat3)) {
+ $cats[2] = '';
+ }
+ if($cat->getAttribute('class') !== 'Cat1HL') {
+ break;
+ }
+ case 'Cat1HL':
+ $cat1 = $cat->textContent;
+ $cats[0] = $cat1;
+ if(empty($cat3)) {
+ $cats[2] = '';
+ }
+ if(empty($cat2)) {
+ $cats[1] = '';
+ }
+ break;
+ default:
+ break;
+ }
+
+ $prefix = '';
+ if(!empty($cats[0])) {
+ $prefix .= '['.$cats[0].($cats[1] ? '/'.$cats[1] : '').'] ';
+ }
+ return $prefix;
+ }
+
+ private function getAnnouncements(&$html){
+ $items = array();
+ $cats = array('','','');
+
+ foreach($html->getElementsByTagName('p') as $newsletters) {
+ if($newsletters->getAttribute('class') !== 'Cat3HL') {
+ continue;
+ }
+
+ $item = array();
+
+ $item['uri'] = self::URI.'#'.count($items);
+
+ $item['timestamp'] = $this->editionTimeStamp;
+
+ $item['author'] = 'LWN';
+
+ $cat = $newsletters->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $prefix = $this->getItemPrefix($cat, $cats);
+ $item['title'] = $prefix.' '.$newsletters->textContent;
+
+ $node = $newsletters;
+ $content = '';
+ $contentEnd = false;
+ while(!$contentEnd) {
+ $node = $node->nextSibling;
+ if(!$node || (
+ $node->nodeType !== XML_TEXT_NODE && (
+ !is_null($node->attributes) &&
+ !is_null($class = $node->attributes->getNamedItem('class')) &&
+ in_array($class->nodeValue, array('Cat1HL','Cat2HL','Cat3HL'))
+ )
+ )
+ ) {
+ $contentEnd = true;
+ } else {
+ $content .= $node->C14N();
+ }
+ }
+ $item['content'] = $content;
+ $items[] = $item;
+ }
+
+ foreach($html->getElementsByTagName('h2') as $title) {
+ if($title->getAttribute('class') !== 'SummaryHL') {
+ continue;
+ }
+
+ $item = array();
+
+ $cat = $title->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $cat = $cat->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $prefix = $this->getItemPrefix($cat, $cats);
+ $item['title'] = $prefix.' '.$title->textContent;
+ $items[] = array_merge($item, $this->getArticleContent($title));
+ }
+
+ return $items;
+ }
+
+ private function getBriefItems(&$html){
+ $items = array();
+ $cats = array('','','');
+ foreach($html->getElementsByTagName('h2') as $title) {
+ if($title->getAttribute('class') !== 'SummaryHL') {
+ continue;
+ }
+
+ $item = array();
+
+ $cat = $title->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $cat = $cat->previousSibling;
+ $this->jumpToPreviousTag($cat);
+ $prefix = $this->getItemPrefix($cat, $cats);
+ $item['title'] = $prefix.' '.$title->textContent;
+ $items[] = array_merge($item, $this->getArticleContent($title));
+ }
+
+ return $items;
+ }
+}
+?>
diff --git a/bridges/LeBonCoinBridge.php b/bridges/LeBonCoinBridge.php
new file mode 100644
index 0000000..927b43e
--- /dev/null
+++ b/bridges/LeBonCoinBridge.php
@@ -0,0 +1,222 @@
+<?php
+class LeBonCoinBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'jacknumber';
+ const NAME = 'LeBonCoin';
+ const URI = 'https://www.leboncoin.fr/';
+ const DESCRIPTION = 'Returns most recent results from LeBonCoin';
+
+ const PARAMETERS = array(
+ array(
+ 'k' => array('name' => 'Mot Clé'),
+ 'r' => array(
+ 'name' => 'Région',
+ 'type' => 'list',
+ 'values' => array(
+ 'Toute la France' => '',
+ 'Alsace' => '1',
+ 'Aquitaine' => '2',
+ 'Auvergne' => '3',
+ 'Basse Normandie' => '4',
+ 'Bourgogne' => '5',
+ 'Bretagne' => '6',
+ 'Centre' => '7',
+ 'Champagne Ardenne' => '8',
+ 'Corse' => '9',
+ 'Franche Comté' => '10',
+ 'Haute Normandie' => '11',
+ 'Ile de France' => '12',
+ 'Languedoc Roussillon' => '13',
+ 'Limousin' => '14',
+ 'Lorraine' => '15',
+ 'Midi Pyrénées' => '16',
+ 'Nord Pas De Calais' => '17',
+ 'Pays de la Loire' => '18',
+ 'Picardie' => '19',
+ 'Poitou Charentes' => '20',
+ 'Provence Alpes Côte d\'Azur' => '21',
+ 'Rhône-Alpes' => '22',
+ 'Guadeloupe' => '23',
+ 'Martinique' => '24',
+ 'Guyane' => '25',
+ 'Réunion' => '26'
+ )
+ ),
+ 'c' => array(
+ 'name' => 'Catégorie',
+ 'type' => 'list',
+ 'values' => array(
+ 'Toutes catégories' => '',
+ 'EMPLOI' => array(
+ 'Emploi et recrutement' => '71',
+ 'Offres d\'emploi et jobs' => '33'
+ ),
+ 'VEHICULES' => array(
+ 'Tous' => '1',
+ 'Voitures' => '2',
+ 'Motos' => '3',
+ 'Caravaning' => '4',
+ 'Utilitaires' => '5',
+ 'Equipement Auto' => '6',
+ 'Equipement Moto' => '44',
+ 'Equipement Caravaning' => '50',
+ 'Nautisme' => '7',
+ 'Equipement Nautisme' => '51'
+ ),
+ 'IMMOBILIER' => array(
+ 'Tous' => '8',
+ 'Ventes immobilières' => '9',
+ 'Locations' => '10',
+ 'Colocations' => '11',
+ 'Bureaux & Commerces' => '13'
+ ),
+ 'VACANCES' => array(
+ 'Tous' => '66',
+ 'Locations & Gîtes' => '12',
+ 'Chambres d\'hôtes' => '67',
+ 'Campings' => '68',
+ 'Hôtels' => '69',
+ 'Hébergements insolites' => '70'
+ ),
+ 'MULTIMEDIA' => array(
+ 'Tous' => '14',
+ 'Informatique' => '15',
+ 'Consoles & Jeux vidéo' => '43',
+ 'Image & Son' => '16',
+ 'Téléphonie' => '17'
+ ),
+ 'LOISIRS' => array(
+ 'Tous' => '24',
+ 'DVD / Films' => '25',
+ 'CD / Musique' => '26',
+ 'Livres' => '27',
+ 'Animaux' => '28',
+ 'Vélos' => '55',
+ 'Sports & Hobbies' => '29',
+ 'Instruments de musique' => '30',
+ 'Collection' => '40',
+ 'Jeux & Jouets' => '41',
+ 'Vins & Gastronomie' => '48'
+ ),
+ 'MATERIEL PROFESSIONNEL' => array(
+ 'Tous' => '56',
+ 'Matériel Agricole' => '57',
+ 'Transport - Manutention' => '58',
+ 'BTP - Chantier Gros-oeuvre' => '59',
+ 'Outillage - Matériaux 2nd-oeuvre' => '60',
+ 'Équipements Industriels' => '32',
+ 'Restauration - Hôtellerie' => '61',
+ 'Fournitures de Bureau' => '62',
+ 'Commerces & Marchés' => '63',
+ 'Matériel Médical' => '64'
+ ),
+ 'SERVICES' => array(
+ 'Tous' => '31',
+ 'Prestations de services' => '34',
+ 'Billetterie' => '35',
+ 'Evénements' => '49',
+ 'Cours particuliers' => '36',
+ 'Covoiturage' => '65'
+ ),
+ 'MAISON' => array(
+ 'Tous' => '18',
+ 'Ameublement' => '19',
+ 'Electroménager' => '20',
+ 'Arts de la table' => '45',
+ 'Décoration' => '39',
+ 'Linge de maison' => '46',
+ 'Bricolage' => '21',
+ 'Jardinage' => '52',
+ 'Vêtements' => '22',
+ 'Chaussures' => '53',
+ 'Accessoires & Bagagerie' => '47',
+ 'Montres & Bijoux' => '42',
+ 'Equipement bébé' => '23',
+ 'Vêtements bébé' => '54',
+ ),
+ 'AUTRES' => '37'
+ )
+ ),
+ 'o' => array(
+ 'name' => 'Vendeur',
+ 'type' => 'list',
+ 'values' => array(
+ 'Tous' => '',
+ 'Particuliers' => 'private',
+ 'Professionnels' => 'pro',
+ )
+ )
+ )
+ );
+
+ public function collectData(){
+
+ $params = array(
+ 'text' => $this->getInput('k'),
+ 'region' => $this->getInput('r'),
+ 'category' => $this->getInput('c'),
+ 'owner_type' => $this->getInput('o'),
+ );
+
+ $url = self::URI . 'recherche/?' . http_build_query($params);
+ $html = getContents($url)
+ or returnServerError('Could not request LeBonCoin. Tried: ' . $url);
+
+ if(!preg_match('/^<script>window.FLUX_STATE[^\r\n]*/m', $html, $matches)) {
+ returnServerError('Could not parse JSON in page content.');
+ }
+
+ $clean_match = str_replace(
+ array('</script>', '<script>window.FLUX_STATE = '),
+ array('', ''),
+ $matches[0]
+ );
+ $json = json_decode($clean_match);
+
+ if($json->adSearch->data->total === 0) {
+ return;
+ }
+
+ foreach($json->adSearch->data->ads as $element) {
+
+ $item['title'] = $element->subject;
+ $item['content'] = $element->body;
+ $item['date'] = $element->index_date;
+ $item['timestamp'] = strtotime($element->index_date);
+ $item['uri'] = $element->url;
+ $item['ad_type'] = $element->ad_type;
+ $item['author'] = $element->owner->name;
+
+ if(isset($element->location->city)) {
+
+ $item['city'] = $element->location->city;
+ $item['content'] .= ' -- ' . $element->location->city;
+
+ }
+
+ if(isset($element->location->zipcode)) {
+ $item['zipcode'] = $element->location->zipcode;
+ }
+
+ if(isset($element->price)) {
+
+ $item['price'] = $element->price[0];
+ $item['content'] .= ' -- ' . current($element->price) . '€';
+
+ }
+
+ if(isset($element->images->urls)) {
+
+ $item['thumbnail'] = $element->images->thumb_url;
+ $item['enclosures'] = array();
+
+ foreach($element->images->urls as $image) {
+ $item['enclosures'][] = $image;
+ }
+
+ }
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/LeMondeInformatiqueBridge.php b/bridges/LeMondeInformatiqueBridge.php
new file mode 100644
index 0000000..706752f
--- /dev/null
+++ b/bridges/LeMondeInformatiqueBridge.php
@@ -0,0 +1,44 @@
+<?php
+class LeMondeInformatiqueBridge extends FeedExpander {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = 'Le Monde Informatique';
+ const URI = 'http://www.lemondeinformatique.fr/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ public function collectData(){
+ $this->collectExpandableDatas(self::URI . 'rss/rss.xml', 10);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $article_html = getSimpleHTMLDOMCached($item['uri'])
+ or returnServerError('Could not request LeMondeInformatique: ' . $item['uri']);
+ $item['content'] = $this->cleanArticle($article_html->find('div#article', 0)->innertext);
+ $item['title'] = $article_html->find('h1.cleanprint-title', 0)->plaintext;
+ return $item;
+ }
+
+ private function stripCDATA($string){
+ $string = str_replace('<![CDATA[', '', $string);
+ $string = str_replace(']]>', '', $string);
+ return $string;
+ }
+
+ private function stripWithDelimiters($string, $start, $end){
+ while(strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ }
+
+ return $string;
+ }
+
+ private function cleanArticle($article_html){
+ $article_html = $this->stripWithDelimiters($article_html, '<script', '</script>');
+ $article_html = $this->stripWithDelimiters($article_html, '<h1 class="cleanprint-title"', '</h1>');
+ return $article_html;
+ }
+}
diff --git a/bridges/LegifranceJOBridge.php b/bridges/LegifranceJOBridge.php
new file mode 100644
index 0000000..d97fcff
--- /dev/null
+++ b/bridges/LegifranceJOBridge.php
@@ -0,0 +1,68 @@
+<?php
+class LegifranceJOBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Pierre Mazière';
+ const NAME = 'Journal Officiel de la République Française';
+ const URI = 'https://www.legifrance.gouv.fr/affichJO.do';
+ const DESCRIPTION = 'Returns the laws and decrees officially registered daily in France';
+
+ const PARAMETERS = array();
+
+ private $author;
+ private $timestamp;
+ private $uri;
+
+ private function extractItem($section, $subsection = null, $origin = null){
+ $item = array();
+ $item['author'] = $this->author;
+ $item['timestamp'] = $this->timestamp;
+ $item['uri'] = $this->uri . '#' . count($this->items);
+ $item['title'] = $section->plaintext;
+
+ if(!is_null($origin)) {
+ $item['title'] = '[ ' . $item['title'] . ' / ' . $subsection->plaintext . ' ] ' . $origin->plaintext;
+ $data = $origin;
+ } elseif(!is_null($subsection)) {
+ $item['title'] = '[ ' . $item['title'] . ' ] ' . $subsection->plaintext;
+ $data = $subsection;
+ } else {
+ $data = $section;
+ }
+
+ $item['content'] = '';
+ foreach($data->nextSibling()->find('a') as $content) {
+ $text = $content->plaintext;
+ $href = $content->nextSibling()->getAttribute('resource');
+ $item['content'] .= '<p><a href="' . $href . '">' . $text . '</a></p>';
+ }
+ return $item;
+ }
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or $this->returnServer('Unable to download ' . self::URI);
+
+ $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/'), -5));
+
+ foreach($html->find('h3') as $section) {
+ $subsections = $section->nextSibling()->find('h4');
+ foreach($subsections as $subsection) {
+ $origins = $subsection->nextSibling()->find('h5');
+ foreach($origins as $origin) {
+ $this->items[] = $this->extractItem($section, $subsection, $origin);
+ }
+ if(!empty($origins)) {
+ continue;
+ }
+ $this->items[] = $this->extractItem($section, $subsection);
+ }
+ if(!empty($subsections)) {
+ continue;
+ }
+ $this->items[] = $this->extractItem($section);
+ }
+ }
+}
diff --git a/bridges/LesJoiesDuCodeBridge.php b/bridges/LesJoiesDuCodeBridge.php
new file mode 100644
index 0000000..5f61f95
--- /dev/null
+++ b/bridges/LesJoiesDuCodeBridge.php
@@ -0,0 +1,45 @@
+<?php
+class LesJoiesDuCodeBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'superbaillot.net';
+ const NAME = 'Les Joies Du Code';
+ const URI = 'http://lesjoiesducode.fr/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'LesJoiesDuCode';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request LesJoiesDuCode.');
+
+ foreach($html->find('div.blog-post') as $element) {
+ $item = array();
+ $temp = $element->find('h1 a', 0);
+ $titre = html_entity_decode($temp->innertext);
+ $url = $temp->href;
+
+ $temp = $element->find('div.blog-post-content', 0);
+
+ // retrieve .gif instead of static .jpg
+ $images = $temp->find('p img');
+ foreach($images as $image) {
+ $img_src = str_replace('.jpg', '.gif', $image->src);
+ $image->src = $img_src;
+ }
+ $content = $temp->innertext;
+
+ $auteur = $temp->find('i', 0);
+ $pos = strpos($auteur->innertext, 'by');
+
+ if($pos > 0) {
+ $auteur = trim(str_replace('*/', '', substr($auteur->innertext, ($pos + 2))));
+ $item['author'] = $auteur;
+ }
+
+ $item['content'] .= trim($content);
+ $item['uri'] = $url;
+ $item['title'] = trim($titre);
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/LichessBridge.php b/bridges/LichessBridge.php
new file mode 100644
index 0000000..bf7369f
--- /dev/null
+++ b/bridges/LichessBridge.php
@@ -0,0 +1,31 @@
+<?php
+class LichessBridge extends FeedExpander {
+
+ const MAINTAINER = 'AmauryCarrade';
+ const NAME = 'Lichess Blog';
+ const URI = 'http://fr.lichess.org/blog';
+ const DESCRIPTION = 'Returns the 5 newest posts from the Lichess blog (full text)';
+
+ public function collectData(){
+ $this->collectExpandableDatas(self::URI . '.atom', 5);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $item['content'] = $this->retrieveLichessPost($item['uri']);
+ return $item;
+ }
+
+ private function retrieveLichessPost($blog_post_uri){
+ $blog_post_html = getSimpleHTMLDOMCached($blog_post_uri);
+ $blog_post_div = $blog_post_html->find('#lichess_blog', 0);
+
+ $post_chapo = $blog_post_div->find('.shortlede', 0)->innertext;
+ $post_content = $blog_post_div->find('.body', 0)->innertext;
+
+ $content = '<p><em>' . $post_chapo . '</em></p>';
+ $content .= '<div>' . $post_content . '</div>';
+
+ return $content;
+ }
+}
diff --git a/bridges/LinkedInCompanyBridge.php b/bridges/LinkedInCompanyBridge.php
new file mode 100644
index 0000000..e629211
--- /dev/null
+++ b/bridges/LinkedInCompanyBridge.php
@@ -0,0 +1,37 @@
+<?php
+class LinkedInCompanyBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'regisenguehard';
+ const NAME = 'LinkedIn Company';
+ const URI = 'https://www.linkedin.com/';
+ const CACHE_TIMEOUT = 21600; //6
+ const DESCRIPTION = 'Returns most recent actus from Company on LinkedIn.
+ (https://www.linkedin.com/company/<strong style=\"font-weight:bold;\">apple</strong>)';
+
+ const PARAMETERS = array( array(
+ 'c' => array(
+ 'name' => 'Company name',
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $html = '';
+ $link = self::URI . 'company/' . $this->getInput('c');
+
+ $html = getSimpleHTMLDOM($link)
+ or returnServerError('Could not request LinkedIn.');
+
+ foreach($html->find('//*[@id="my-feed-post"]/li') as $element) {
+ $title = $element->find('span.share-body', 0)->innertext;
+ if($title) {
+ $item = array();
+ $item['uri'] = $link;
+ $item['title'] = mb_substr(strip_tags($element->find('span.share-body', 0)->innertext), 0, 100);
+ $item['content'] = strip_tags($element->find('span.share-body', 0)->innertext);
+ $this->items[] = $item;
+ $i++;
+ }
+ }
+ }
+}
diff --git a/bridges/LolibooruBridge.php b/bridges/LolibooruBridge.php
new file mode 100644
index 0000000..b5bbd75
--- /dev/null
+++ b/bridges/LolibooruBridge.php
@@ -0,0 +1,11 @@
+<?php
+require_once('MoebooruBridge.php');
+
+class LolibooruBridge extends MoebooruBridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Lolibooru';
+ const URI = 'https://lolibooru.moe/';
+ const DESCRIPTION = 'Returns images from given page and tags';
+
+}
diff --git a/bridges/MangareaderBridge.php b/bridges/MangareaderBridge.php
new file mode 100644
index 0000000..9153706
--- /dev/null
+++ b/bridges/MangareaderBridge.php
@@ -0,0 +1,249 @@
+<?php
+class MangareaderBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Mangareader Bridge';
+ const URI = 'http://www.mangareader.net';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the latest updates, popular mangas or manga updates (new chapters)';
+
+ const PARAMETERS = array(
+ 'Get latest updates' => array(),
+ 'Get popular mangas' => array(
+ 'category' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'required' => true,
+ 'values' => array(
+ 'All' => 'all',
+ 'Action' => 'action',
+ 'Adventure' => 'adventure',
+ 'Comedy' => 'comedy',
+ 'Demons' => 'demons',
+ 'Drama' => 'drama',
+ 'Ecchi' => 'ecchi',
+ 'Fantasy' => 'fantasy',
+ 'Gender Bender' => 'gender-bender',
+ 'Harem' => 'harem',
+ 'Historical' => 'historical',
+ 'Horror' => 'horror',
+ 'Josei' => 'josei',
+ 'Magic' => 'magic',
+ 'Martial Arts' => 'martial-arts',
+ 'Mature' => 'mature',
+ 'Mecha' => 'mecha',
+ 'Military' => 'military',
+ 'Mystery' => 'mystery',
+ 'One Shot' => 'one-shot',
+ 'Psychological' => 'psychological',
+ 'Romance' => 'romance',
+ 'School Life' => 'school-life',
+ 'Sci-Fi' => 'sci-fi',
+ 'Seinen' => 'seinen',
+ 'Shoujo' => 'shoujo',
+ 'Shoujoai' => 'shoujoai',
+ 'Shounen' => 'shounen',
+ 'Shounenai' => 'shounenai',
+ 'Slice of Life' => 'slice-of-life',
+ 'Smut' => 'smut',
+ 'Sports' => 'sports',
+ 'Super Power' => 'super-power',
+ 'Supernatural' => 'supernatural',
+ 'Tragedy' => 'tragedy',
+ 'Vampire' => 'vampire',
+ 'Yaoi' => 'yaoi',
+ 'Yuri' => 'yuri'
+ ),
+ 'exampleValue' => 'All',
+ 'title' => 'Select your category'
+ )
+ ),
+ 'Get manga updates' => array(
+ 'path' => array(
+ 'name' => 'Path',
+ 'required' => true,
+ 'pattern' => '[a-zA-Z0-9-_]*',
+ 'exampleValue' => 'bleach, umi-no-kishidan',
+ 'title' => 'URL part of desired manga'
+ ),
+ 'limit' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'defaultValue' => 10,
+ 'title' => 'Number of items to return [-1 returns all]'
+ )
+ )
+ );
+
+ private $request = '';
+
+ public function collectData(){
+ // We'll use the DOM parser for this as it makes navigation easier
+ $html = getContents($this->getURI());
+ if(!$html) {
+ returnClientError('Could not receive data for ' . $path . '!');
+ }
+ libxml_use_internal_errors(true);
+ $doc = new DomDocument;
+ @$doc->loadHTML($html);
+ libxml_clear_errors();
+
+ // Navigate via XPath
+ $xpath = new DomXPath($doc);
+
+ $this->request = '';
+ switch($this->queriedContext) {
+ case 'Get latest updates':
+ $this->request = 'Latest updates';
+ $this->getLatestUpdates($xpath);
+ break;
+ case 'Get popular mangas':
+ // Find manga name within "Popular mangas for ..."
+ $pagetitle = $xpath->query(".//*[@id='bodyalt']/h1")->item(0)->nodeValue;
+ $this->request = substr($pagetitle, 0, strrpos($pagetitle, ' -'));
+ $this->getPopularMangas($xpath);
+ break;
+ case 'Get manga updates':
+ $limit = $this->getInput('limit');
+ if(empty($limit)) {
+ $limit = self::PARAMETERS[$this->queriedContext]['limit']['defaultValue'];
+ }
+
+ $this->request = $xpath->query(".//*[@id='mangaproperties']//*[@class='aname']")
+ ->item(0)
+ ->nodeValue;
+
+ $this->getMangaUpdates($xpath, $limit);
+ break;
+ }
+
+ // Return some dummy-data if no content available
+ if(empty($this->items)) {
+ $item = array();
+ $item['content'] = '<p>No updates available</p>';
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function getLatestUpdates($xpath){
+ // Query each item (consists of Manga + chapters)
+ $nodes = $xpath->query("//*[@id='latestchapters']/table//td");
+
+ foreach ($nodes as $node) {
+ // Query the manga
+ $manga = $xpath->query("a[@class='chapter']", $node)->item(0);
+
+ // Collect the chapters for each Manga
+ $chapters = $xpath->query("a[@class='chaptersrec']", $node);
+
+ if (isset($manga) && $chapters->length >= 1) {
+ $item = array();
+ $item['uri'] = self::URI . htmlspecialchars($manga->getAttribute('href'));
+ $item['title'] = htmlspecialchars($manga->nodeValue);
+
+ // Add each chapter to the feed
+ $item['content'] = '';
+
+ foreach ($chapters as $chapter) {
+ if($item['content'] <> '') {
+ $item['content'] .= '<br>';
+ }
+ $item['content'] .= "<a href='"
+ . self::URI
+ . htmlspecialchars($chapter->getAttribute('href'))
+ . "'>"
+ . htmlspecialchars($chapter->nodeValue)
+ . '</a>';
+ }
+
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ private function getPopularMangas($xpath){
+ // Query all mangas
+ $mangas = $xpath->query("//*[@id='mangaresults']/*[@class='mangaresultitem']");
+
+ foreach ($mangas as $manga) {
+
+ // The thumbnail is encrypted in a css-style...
+ // format: "background-image:url('<the part which is actually interesting>')"
+ $mangaimgelement = $xpath->query(".//*[@class='imgsearchresults']", $manga)
+ ->item(0)
+ ->getAttribute('style');
+ $thumbnail = substr($mangaimgelement, 22, strlen($mangaimgelement) - 24);
+
+ $item = array();
+ $item['title'] = htmlspecialchars($xpath->query(".//*[@class='manga_name']//a", $manga)
+ ->item(0)
+ ->nodeValue);
+ $item['uri'] = self::URI . $xpath->query(".//*[@class='manga_name']//a", $manga)
+ ->item(0)
+ ->getAttribute('href');
+ $item['author'] = htmlspecialchars($xpath->query("//*[@class='author_name']", $manga)
+ ->item(0)
+ ->nodeValue);
+ $item['chaptercount'] = $xpath->query(".//*[@class='chapter_count']", $manga)
+ ->item(0)
+ ->nodeValue;
+ $item['genre'] = htmlspecialchars($xpath->query(".//*[@class='manga_genre']", $manga)
+ ->item(0)
+ ->nodeValue);
+ $item['content'] = <<<EOD
+<a href="{$item['uri']}"><img src="{$thumbnail}" alt="{$item['title']}" /></a>
+<p>{$item['genre']}</p>
+<p>{$item['chaptercount']}</p>
+EOD;
+ $this->items[] = $item;
+ }
+ }
+
+ private function getMangaUpdates($xpath, $limit){
+ $query = "(.//*[@id='listing']//tr)[position() > 1]";
+
+ if($limit !== -1) {
+ $query = "(.//*[@id='listing']//tr)[position() > 1][position() > last() - {$limit}]";
+ }
+
+ $chapters = $xpath->query($query);
+
+ foreach ($chapters as $chapter) {
+ $item = array();
+ $item['title'] = htmlspecialchars($xpath->query('td[1]', $chapter)
+ ->item(0)
+ ->nodeValue);
+ $item['uri'] = self::URI . $xpath->query('td[1]/a', $chapter)
+ ->item(0)
+ ->getAttribute('href');
+ $item['timestamp'] = strtotime($xpath->query('td[2]', $chapter)
+ ->item(0)
+ ->nodeValue);
+ array_unshift($this->items, $item);
+ }
+ }
+
+ public function getURI(){
+ switch($this->queriedContext) {
+ case 'Get latest updates':
+ $path = 'latest';
+ break;
+ case 'Get popular mangas':
+ $path = 'popular';
+ if($this->getInput('category') !== 'all') {
+ $path .= '/' . $this->getInput('category');
+ }
+ break;
+ case 'Get manga updates':
+ $path = $this->getInput('path');
+ break;
+ default: return parent::getURI();
+ }
+ return self::URI . '/' . $path;
+ }
+
+ public function getName(){
+ return (!empty($this->request) ? $this->request . ' - ' : '') . 'Mangareader Bridge';
+ }
+}
diff --git a/bridges/MilbooruBridge.php b/bridges/MilbooruBridge.php
new file mode 100644
index 0000000..c3b633e
--- /dev/null
+++ b/bridges/MilbooruBridge.php
@@ -0,0 +1,11 @@
+<?php
+require_once('Shimmie2Bridge.php');
+
+class MilbooruBridge extends Shimmie2Bridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Milbooru';
+ const URI = 'http://sheslostcontrol.net/moe/shimmie/';
+ const DESCRIPTION = 'Returns images from given page';
+
+}
diff --git a/bridges/MixCloudBridge.php b/bridges/MixCloudBridge.php
new file mode 100644
index 0000000..723f634
--- /dev/null
+++ b/bridges/MixCloudBridge.php
@@ -0,0 +1,53 @@
+<?php
+
+class MixCloudBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Alexis CHEMEL';
+ const NAME = 'MixCloud';
+ const URI = 'https://www.mixcloud.com';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Returns latest musics on user stream';
+
+ const PARAMETERS = array(array(
+ 'u' => array(
+ 'name' => 'username',
+ 'required' => true,
+ )
+ ));
+
+ public function getName(){
+ if(!is_null($this->getInput('u'))) {
+ return 'MixCloud - ' . $this->getInput('u');
+ }
+
+ return parent::getName();
+ }
+
+ 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'))
+ or returnServerError('Could not request MixCloud.');
+
+ foreach($html->find('section.card') as $element) {
+
+ $item = array();
+
+ $item['uri'] = self::URI . $element->find('hgroup.card-title h1 a', 0)->getAttribute('href');
+ $item['title'] = html_entity_decode(
+ $element->find('hgroup.card-title h1 a span', 0)->getAttribute('title'),
+ ENT_QUOTES
+ );
+
+ $image = $element->find('a.album-art img', 0);
+
+ if($image) {
+ $item['content'] = '<img src="' . $image->getAttribute('src') . '" />';
+ }
+
+ $item['author'] = trim($element->find('hgroup.card-title h2 a', 0)->innertext);
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/MoebooruBridge.php b/bridges/MoebooruBridge.php
new file mode 100644
index 0000000..9d9a625
--- /dev/null
+++ b/bridges/MoebooruBridge.php
@@ -0,0 +1,56 @@
+<?php
+class MoebooruBridge extends BridgeAbstract {
+
+ const NAME = 'Moebooru';
+ const URI = 'https://moe.dev.myconan.net/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns images from given page';
+ const MAINTAINER = 'pmaziere';
+
+ const PARAMETERS = array( array(
+ 'p' => array(
+ 'name' => 'page',
+ 'defaultValue' => 1,
+ 'type' => 'number'
+ ),
+ 't' => array(
+ 'name' => 'tags'
+ )
+ ));
+
+ protected function getFullURI(){
+ return $this->getURI()
+ . 'post?page='
+ . $this->getInput('p')
+ . '&tags='
+ . urlencode($this->getInput('t'));
+ }
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getFullURI())
+ or returnServerError('Could not request ' . $this->getName());
+
+ $input_json = explode('Post.register(', $html);
+ foreach($input_json as $element)
+ $data[] = preg_replace('/}\)(.*)/', '}', $element);
+ unset($data[0]);
+
+ foreach($data as $datai) {
+ $json = json_decode($datai, true);
+ $item = array();
+ $item['uri'] = $this->getURI() . '/post/show/' . $json['id'];
+ $item['postid'] = $json['id'];
+ $item['timestamp'] = $json['created_at'];
+ $item['imageUri'] = $json['file_url'];
+ $item['title'] = $this->getName() . ' | ' . $json['id'];
+ $item['content'] = '<a href="'
+ . $item['imageUri']
+ . '"><img src="'
+ . $json['preview_url']
+ . '" /></a><br>Tags: '
+ . $json['tags'];
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/MoinMoinBridge.php b/bridges/MoinMoinBridge.php
new file mode 100644
index 0000000..5b41924
--- /dev/null
+++ b/bridges/MoinMoinBridge.php
@@ -0,0 +1,327 @@
+<?php
+class MoinMoinBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'MoinMoin Bridge';
+ const URI = 'https://moinmo.in';
+ const DESCRIPTION = 'Generates feeds for pages of a MoinMoin (compatible) wiki';
+ const PARAMETERS = array(
+ array(
+ 'source' => array(
+ 'name' => 'Source',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert wiki page URI (e.g.: https://moinmo.in/MoinMoin)',
+ 'exampleValue' => 'https://moinmo.in/MoinMoin'
+ ),
+ 'separator' => array(
+ 'name' => 'Separator',
+ 'type' => 'list',
+ 'requied' => true,
+ 'title' => 'Defines the separtor for splitting content into feeds',
+ 'defaultValue' => 'h2',
+ 'values' => array(
+ 'Header (h1)' => 'h1',
+ 'Header (h2)' => 'h2',
+ 'Header (h3)' => 'h3',
+ 'List element (li)' => 'li',
+ 'Anchor (a)' => 'a'
+ )
+ ),
+ 'limit' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Number of items to return (from top)',
+ 'defaultValue' => -1
+ ),
+ 'content' => array(
+ 'name' => 'Content',
+ 'type' => 'list',
+ 'required' => false,
+ 'title' => 'Defines how feed contents are build',
+ 'defaultValue' => 'separator',
+ 'values' => array(
+ 'By separator' => 'separator',
+ 'Follow link (only for anchor)' => 'follow',
+ 'None' => 'none'
+ )
+ )
+ )
+ );
+
+ private $title = '';
+
+ public function collectData(){
+ /* MoinMoin uses a rather unpleasent representation of HTML. Instead of
+ * using tags like <article/>, <navigation/>, <header/>, etc... it uses
+ * <div/>, <span/> and <p/>. Also each line is literaly identified via
+ * IDs. The only way to distinguish content is via headers, though not
+ * in all cases.
+ *
+ * Example (indented for the sake of readability):
+ * ...
+ * <span class="anchor" id="line-1"></span>
+ * <span class="anchor" id="line-2"></span>
+ * <span class="anchor" id="line-3"></span>
+ * <span class="anchor" id="line-4"></span>
+ * <span class="anchor" id="line-5"></span>
+ * <span class="anchor" id="line-6"></span>
+ * <span class="anchor" id="line-7"></span>
+ * <span class="anchor" id="line-8"></span>
+ * <span class="anchor" id="line-9"></span>
+ * <p class="line867">MoinMoin is a Wiki software implemented in
+ * <a class="interwiki" href="/Python" title="MoinMoin">Python</a>
+ * and distributed as Free Software under
+ * <a class="interwiki" href="/GPL" title="MoinMoin">GNU GPL license</a>.
+ * ...
+ */
+ $html = getSimpleHTMLDOM($this->getInput('source'))
+ or returnServerError('Could not load ' . $this->getInput('source'));
+
+ // Some anchors link to local sites or local IDs (both don't work well
+ // in feeds)
+ $html = $this->fixAnchors($html);
+
+ $this->title = $html->find('title', 0)->innertext . ' | ' . self::NAME;
+
+ // Here we focus on simple author and timestamp information from the given
+ // page. Later we update this information in case the anchor is followed.
+ $author = $this->findAuthor($html);
+ $timestamp = $this->findTimestamp($html);
+
+ $sections = $this->splitSections($html);
+
+ foreach($sections as $section) {
+ $item = array();
+
+ $item['uri'] = $this->findSectionAnchor($section[0]);
+
+ switch($this->getInput('content')) {
+ case 'none': // Do not return any content
+ break;
+ case 'follow': // Follow the anchor
+ // We can only follow anchors (use default otherwise)
+ if($this->getInput('separator') === 'a') {
+ $content = $this->followAnchor($item['uri']);
+
+ // Return only actual content
+ $item['content'] = $content->find('div#page', 0)->innertext;
+
+ // Each page could have its own author and timestamp
+ $author = $this->findAuthor($content);
+ $timestamp = $this->findTimestamp($content);
+
+ break;
+ }
+ case 'separator':
+ default: // Use contents from the current page
+ $item['content'] = $this->cleanArticle($section[2]);
+ }
+
+ if(!is_null($author)) $item['author'] = $author;
+ if(!is_null($timestamp)) $item['timestamp'] = $timestamp;
+ $item['title'] = strip_tags($section[1]);
+
+ // Skip items with empty title
+ if(empty(trim($item['title']))) {
+ continue;
+ }
+
+ $this->items[] = $item;
+
+ if($this->getInput('limit') > 0
+ && count($this->items) >= $this->getInput('limit')) {
+ break;
+ }
+ }
+ }
+
+ public function getName(){
+ return $this->title ?: parent::getName();
+ }
+
+ public function getURI(){
+ return $this->getInput('source') ?: parent::getURI();
+ }
+
+ /**
+ * Splits the html into sections.
+ *
+ * Returns an array with one element per section. Each element consists of:
+ * [0] The entire section
+ * [1] The section title
+ * [2] The section content
+ */
+ private function splitSections($html){
+ $content = $html->find('div#page', 0)->innertext
+ or returnServerError('Unable to find <div id="page"/>!');
+
+ $sections = array();
+
+ $regex = implode(
+ '',
+ array(
+ "\<{$this->getInput('separator')}.+?(?=\>)\>",
+ "(.+?)(?=\<\/{$this->getInput('separator')}\>)",
+ "\<\/{$this->getInput('separator')}\>",
+ "(.+?)((?=\<{$this->getInput('separator')})|(?=\<div\sid=\"pagebottom\")){1}"
+ )
+ );
+
+ preg_match_all(
+ '/' . $regex . '/m',
+ $content,
+ $sections,
+ PREG_SET_ORDER
+ );
+
+ // Some pages don't use headers, return page as one feed
+ if(count($sections) === 0) {
+ return array(
+ array(
+ $content,
+ $html->find('title', 0)->innertext,
+ $content
+ )
+ );
+ }
+
+ return $sections;
+ }
+
+ /**
+ * Returns the anchor for a given section
+ */
+ private function findSectionAnchor($section){
+ $html = str_get_html($section);
+
+ // For IDs
+ $anchor = $html->find($this->getInput('separator') . '[id=]', 0);
+ if(!is_null($anchor)) {
+ return $this->getInput('source') . '#' . $anchor->id;
+ }
+
+ // For actual anchors
+ $anchor = $html->find($this->getInput('separator') . '[href=]', 0);
+ if(!is_null($anchor)) {
+ return $anchor->href;
+ }
+
+ // Nothing found
+ return $this->getInput('source');
+ }
+
+ /**
+ * Returns the author
+ *
+ * Notice: Some pages don't provide author information
+ */
+ private function findAuthor($html){
+ /* Example:
+ * <p id="pageinfo" class="info" dir="ltr" lang="en">MoinMoin: LocalSpellingWords
+ * (last edited 2017-02-16 15:36:31 by <span title="??? @ hosted-by.leaseweb.com
+ * [178.162.199.143]">hosted-by</span>)</p>
+ */
+ $pageinfo = $html->find('[id="pageinfo"]', 0);
+
+ if(is_null($pageinfo)) {
+ return null;
+ } else {
+ $author = $pageinfo->find('[title=]', 0);
+ if(is_null($author)) {
+ return null;
+ } else {
+ return trim(explode('@', $author->title)[0]);
+ }
+ }
+ }
+
+ /**
+ * Returns the time of last edit
+ *
+ * Notice: Some pages don't provide this information
+ */
+ private function findTimestamp($html){
+ // See example of findAuthor()
+ $pageinfo = $html->find('[id="pageinfo"]', 0);
+
+ if(is_null($pageinfo)) {
+ return null;
+ } else {
+ $timestamp = $pageinfo->innertext;
+ $matches = array();
+ preg_match('/.+?(?=\().+?(?=\d)([0-9\-\s\:]+)/m', $pageinfo, $matches);
+ return strtotime($matches[1]);
+ }
+ }
+
+ /**
+ * Returns the original HTML with all anchors fixed (makes relative anchors
+ * absolute)
+ */
+ private function fixAnchors($html, $source = null){
+
+ $source = $source ?: $this->getURI();
+
+ foreach($html->find('a') as $anchor) {
+ switch(substr($anchor->href, 0, 1)) {
+ case 'h': // http or https, no actions required
+ break;
+ case '/': // some relative path
+ $anchor->href = $this->findDomain($source) . $anchor->href;
+ break;
+ case '#': // it's an ID
+ default: // probably something like ? or &, skip empty ones
+ if(!isset($anchor->href))
+ break;
+ $anchor->href = $source . $anchor->href;
+ }
+ }
+
+ return $html;
+ }
+
+ /**
+ * Loads the full article of a given anchor (if the anchor is from the same
+ * wiki domain)
+ */
+ private function followAnchor($anchor){
+ if(strrpos($anchor, $this->findDomain($this->getInput('source')) === false)) {
+ return null;
+ }
+
+ $html = getSimpleHTMLDOMCached($anchor);
+ if(!$html) { // Cannot load article
+ return null;
+ }
+
+ return $this->fixAnchors($html, $anchor);
+ }
+
+ /**
+ * Finds the domain for a given URI
+ */
+ private function findDomain($uri){
+ $matches = array();
+ preg_match('/(http[s]{0,1}:\/\/.+?(?=\/))/', $uri, $matches);
+ return $matches[1];
+ }
+
+ /* This function is a copy from CNETBridge */
+ private function stripWithDelimiters($string, $start, $end){
+ while(strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ }
+
+ return $string;
+ }
+
+ /* This function is based on CNETBridge */
+ private function cleanArticle($article_html){
+ $article_html = $this->stripWithDelimiters($article_html, '<script', '</script>');
+ return $article_html;
+ }
+}
diff --git a/bridges/MondeDiploBridge.php b/bridges/MondeDiploBridge.php
new file mode 100644
index 0000000..85f771e
--- /dev/null
+++ b/bridges/MondeDiploBridge.php
@@ -0,0 +1,26 @@
+<?php
+class MondeDiploBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Pitchoule';
+ const NAME = 'Monde Diplomatique';
+ const URI = 'http://www.monde-diplomatique.fr/';
+ const CACHE_TIMEOUT = 21600; //6h
+ const DESCRIPTION = 'Returns most recent results from MondeDiplo.';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request MondeDiplo. for : ' . self::URI);
+
+ foreach($html->find('div.unarticle') as $article) {
+ $element = $article->parent();
+ $item = array();
+ $item['uri'] = self::URI . $element->href;
+ $item['title'] = $element->find('h3', 0)->plaintext;
+ $item['content'] = $element->find('div.dates_auteurs', 0)->plaintext
+ . '<br>'
+ . strstr($element->find('div', 0)->plaintext, $element->find('div.dates_auteurs', 0)->plaintext, true);
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/MsnMondeBridge.php b/bridges/MsnMondeBridge.php
new file mode 100644
index 0000000..12d3d2f
--- /dev/null
+++ b/bridges/MsnMondeBridge.php
@@ -0,0 +1,35 @@
+<?php
+class MsnMondeBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'kranack';
+ const NAME = 'MSN Actu Monde';
+ const URI = 'http://www.msn.com/';
+ const DESCRIPTION = 'Returns the 10 newest posts from MSN Actualités (full text)';
+
+ public function getURI(){
+ return self::URI . 'fr-fr/actualite/monde';
+ }
+
+ private function msnMondeExtractContent($url, &$item){
+ $html2 = getSimpleHTMLDOM($url);
+ $item['content'] = $html2->find('#content', 0)->find('article', 0)->find('section', 0)->plaintext;
+ $item['timestamp'] = strtotime($html2->find('.authorinfo-txt', 0)->find('time', 0)->datetime);
+ }
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request MsnMonde.');
+
+ $limit = 0;
+ foreach($html->find('.smalla') as $article) {
+ if($limit < 10) {
+ $item = array();
+ $item['title'] = utf8_decode($article->find('h4', 0)->innertext);
+ $item['uri'] = self::URI . utf8_decode($article->find('a', 0)->href);
+ $this->msnMondeExtractContent($item['uri'], $item);
+ $this->items[] = $item;
+ $limit++;
+ }
+ }
+ }
+}
diff --git a/bridges/MspabooruBridge.php b/bridges/MspabooruBridge.php
new file mode 100644
index 0000000..00a7bd7
--- /dev/null
+++ b/bridges/MspabooruBridge.php
@@ -0,0 +1,12 @@
+<?php
+require_once('GelbooruBridge.php');
+
+class MspabooruBridge extends GelbooruBridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Mspabooru';
+ const URI = 'http://mspabooru.com/';
+ const DESCRIPTION = 'Returns images from given page';
+ const PIDBYPAGE = 50;
+
+}
diff --git a/bridges/MydealsBridge.php b/bridges/MydealsBridge.php
new file mode 100644
index 0000000..163cf62
--- /dev/null
+++ b/bridges/MydealsBridge.php
@@ -0,0 +1,144 @@
+<?php
+
+require_once(__DIR__ . '/DealabsBridge.php');
+class MydealsBridge extends PepperBridgeAbstract {
+
+ const NAME = 'Mydeals bridge';
+ const URI = 'https://www.mydealz.de/';
+ const DESCRIPTION = 'Zeigt die Deals von mydeals.de';
+ const MAINTAINER = 'sysadminstory';
+ const PARAMETERS = array(
+ 'Suche nach Stichworten' => array (
+ 'q' => array(
+ 'name' => 'Stichworten',
+ 'type' => 'text',
+ 'required' => true
+ ),
+ 'hide_expired' => array(
+ 'name' => 'Abgelaufenes ausblenden',
+ 'type' => 'checkbox',
+ 'required' => 'true'
+ ),
+ 'hide_local' => array(
+ 'name' => 'Lokales ausblenden',
+ 'type' => 'checkbox',
+ 'title' => 'Deals im physischen Geschäft ausblenden',
+ 'required' => 'true'
+ ),
+ 'priceFrom' => array(
+ 'name' => 'Minimaler Preis',
+ 'type' => 'text',
+ 'title' => 'Minmaler Preis in Euros',
+ 'required' => 'false',
+ 'defaultValue' => ''
+ ),
+ 'priceTo' => array(
+ 'name' => 'Maximaler Preis',
+ 'type' => 'text',
+ 'title' => 'maximaler Preis in Euro',
+ 'required' => 'false',
+ 'defaultValue' => ''
+ ),
+ ),
+
+ 'Deals pro Gruppen' => array(
+ 'group' => array(
+ 'name' => 'Gruppen',
+ 'type' => 'list',
+ 'required' => 'true',
+ 'title' => 'Gruppe, deren Deals angezeigt werden müssen',
+ 'values' => array(
+ 'Elektronik' => 'elektronik',
+ 'Handy & Smartphone' => 'smartphone',
+ 'Gaming' => 'gaming',
+ 'Software' => 'apps-software',
+ 'Fashion Frauen' => 'fashion-frauen',
+ 'Fashion Männer' => 'fashion-accessoires',
+ 'Beauty & Gesundheit' => 'beauty',
+ 'Family & Kids' => 'family-kids',
+ 'Essen & Trinken' => 'food',
+ 'Freizeit & Reisen' => 'reisen',
+ 'Haushalt & Garten' => 'home-living',
+ 'Entertainment' => 'entertainment',
+ 'Verträge & Finanzen' => 'vertraege-finanzen',
+ 'Coupons' => 'coupons',
+
+ )
+ ),
+ 'order' => array(
+ 'name' => 'sortieren nach',
+ 'type' => 'list',
+ 'required' => 'true',
+ 'title' => 'Sortierung der deals',
+ 'values' => array(
+ 'Vom heißesten zum kältesten Deal' => '',
+ 'Vom jüngsten Deal zum ältesten' => '-new',
+ 'Vom am meisten kommentierten Deal zum am wenigsten kommentierten Deal' => '-discussed'
+ )
+ )
+ )
+ );
+
+ public $lang = array(
+ 'bridge-uri' => SELF::URI,
+ 'bridge-name' => SELF::NAME,
+ 'context-keyword' => 'Suche nach Stichworten',
+ 'context-group' => 'Deals pro Gruppen',
+ 'uri-group' => '/gruppe/',
+ 'request-error' => 'Could not request mydeals',
+ 'no-results' => 'Ups, wir konnten keine Deals zu',
+ 'relative-date-indicator' => array(
+ 'vor',
+ 'seit'
+ ),
+ 'price' => 'Preis',
+ 'shipping' => 'Versand',
+ 'origin' => 'Ursprung',
+ 'discount' => 'Rabatte',
+ 'title-keyword' => 'Suche',
+ 'title-group' => 'Gruppe',
+ 'local-months' => array(
+ 'Jan',
+ 'Feb',
+ 'Mär',
+ 'Apr',
+ 'Mai',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Okt',
+ 'Nov',
+ 'Dez',
+ '.'
+ ),
+ 'local-time-relative' => array(
+ 'eingestellt vor ',
+ 'm',
+ 'h,',
+ 'day',
+ 'days',
+ 'month',
+ 'year',
+ 'and '
+ ),
+ 'date-prefixes' => array(
+ 'eingestellt am ',
+ 'lokal ',
+ 'aktualisiert ',
+ ),
+ 'relative-date-alt-prefixes' => array(
+ 'aktualisiert vor ',
+ 'kommentiert vor ',
+ 'heiß seit '
+ ),
+ 'relative-date-ignore-suffix' => array(
+ '/von.*$/'
+ ),
+ 'localdeal' => array(
+ 'Lokal ',
+ 'Läuft bis '
+ )
+ );
+
+}
diff --git a/bridges/NasaApodBridge.php b/bridges/NasaApodBridge.php
new file mode 100644
index 0000000..8e293e0
--- /dev/null
+++ b/bridges/NasaApodBridge.php
@@ -0,0 +1,44 @@
+<?php
+class NasaApodBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'corenting';
+ const NAME = 'NASA APOD Bridge';
+ const URI = 'https://apod.nasa.gov/apod/';
+ const CACHE_TIMEOUT = 43200; // 12h
+ const DESCRIPTION = 'Returns the 3 latest NASA APOD pictures and explanations';
+
+ public function collectData(){
+
+ $html = getSimpleHTMLDOM(self::URI . 'archivepix.html')
+ or returnServerError('Error while downloading the website content');
+
+ $list = explode('<br>', $html->find('b', 0)->innertext);
+
+ for($i = 0; $i < 3; $i++) {
+ $line = $list[$i];
+ $item = array();
+
+ $uri_page = $html->find('a', $i + 3)->href;
+ $uri = self::URI . $uri_page;
+ $item['uri'] = $uri;
+
+ $picture_html = getSimpleHTMLDOM($uri);
+ $picture_html_string = $picture_html->innertext;
+
+ //Extract image and explanation
+ $media = $picture_html->find('p', 1)->innertext;
+ $media = strstr($media, '<br>');
+ $media = preg_replace('/<br>/', '', $media, 1);
+ $explanation = $picture_html->find('p', 2)->innertext;
+
+ //Extract date from the picture page
+ $date = explode(' ', $picture_html->find('p', 1)->innertext);
+ $item['timestamp'] = strtotime($date[4] . $date[3] . $date[2]);
+
+ //Other informations
+ $item['content'] = $media . '<br />' . $explanation;
+ $item['title'] = $picture_html->find('b', 0)->innertext;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/NeuviemeArtBridge.php b/bridges/NeuviemeArtBridge.php
new file mode 100644
index 0000000..d0954fc
--- /dev/null
+++ b/bridges/NeuviemeArtBridge.php
@@ -0,0 +1,57 @@
+<?php
+class NeuviemeArtBridge extends FeedExpander {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = '9ème Art Bridge';
+ const URI = 'http://www.9emeart.fr/';
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ private function stripWithDelimiters($string, $start, $end){
+ while(strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ }
+
+ return $string;
+ }
+
+ protected function parseItem($item){
+ $item = parent::parseItem($item);
+
+ $article_html = getSimpleHTMLDOMCached($item['uri']);
+ if(!$article_html) {
+ $item['content'] = 'Could not request 9eme Art: ' . $item['uri'];
+ return $item;
+ }
+
+ $article_image = '';
+ foreach ($article_html->find('img.img_full') as $img) {
+ if ($img->alt == $item['title']) {
+ $article_image = self::URI . $img->src;
+ break;
+ }
+ }
+
+ $article_content = '';
+ if($article_image) {
+ $article_content = '<p><img src="' . $article_image . '" /></p>';
+ }
+ $article_content .= str_replace(
+ 'src="/', 'src="' . self::URI,
+ $article_html->find('div.newsGenerique_con', 0)->innertext
+ );
+ $article_content = $this->stripWithDelimiters($article_content, '<script', '</script>');
+ $article_content = $this->stripWithDelimiters($article_content, '<style', '</style>');
+ $article_content = $this->stripWithDelimiters($article_content, '<link', '>');
+
+ $item['content'] = $article_content;
+
+ return $item;
+ }
+
+ public function collectData(){
+ $feedUrl = self::URI . '9emeart.rss';
+ $this->collectExpandableDatas($feedUrl);
+ }
+}
diff --git a/bridges/NextInpactBridge.php b/bridges/NextInpactBridge.php
new file mode 100644
index 0000000..5de5c8b
--- /dev/null
+++ b/bridges/NextInpactBridge.php
@@ -0,0 +1,34 @@
+<?php
+class NextInpactBridge extends FeedExpander {
+
+ const MAINTAINER = 'qwertygc';
+ const NAME = 'NextInpact Bridge';
+ const URI = 'https://www.nextinpact.com/';
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ public function collectData(){
+ $this->collectExpandableDatas(self::URI . 'rss/news.xml', 10);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $item['content'] = $this->extractContent($item['uri']);
+ return $item;
+ }
+
+ private function extractContent($url){
+ $html2 = getSimpleHTMLDOMCached($url);
+ $text = '<p><em>'
+ . $html2->find('span.sub_title', 0)->innertext
+ . '</em></p><p><img src="'
+ . $html2->find('div.container_main_image_article', 0)->find('img.dedicated', 0)->src
+ . '" alt="-" /></p><div>'
+ . $html2->find('div[itemprop=articleBody]', 0)->innertext
+ . '</div>';
+
+ $premium_article = $html2->find('h2.title_reserve_article', 0);
+ if (is_object($premium_article))
+ $text = $text . '<p><em>' . $premium_article->innertext . '</em></p>';
+ return $text;
+ }
+}
diff --git a/bridges/NextgovBridge.php b/bridges/NextgovBridge.php
new file mode 100644
index 0000000..370b0bf
--- /dev/null
+++ b/bridges/NextgovBridge.php
@@ -0,0 +1,74 @@
+<?php
+class NextgovBridge extends FeedExpander {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = 'Nextgov Bridge';
+ const URI = 'https://www.nextgov.com/';
+ const DESCRIPTION = 'USA Federal technology news, best practices, and web 2.0 tools.';
+
+ const PARAMETERS = array( array(
+ 'category' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => array(
+ 'All' => 'all',
+ 'Technology News' => 'technology-news',
+ 'CIO Briefing' => 'cio-briefing',
+ 'Emerging Tech' => 'emerging-tech',
+ 'Cloud' => 'cloud-computing',
+ 'Cybersecurity' => 'cybersecurity',
+ 'Mobile' => 'mobile',
+ 'Health' => 'health',
+ 'Defense' => 'defense',
+ 'Big Data' => 'big-data'
+ )
+ )
+ ));
+
+ public function collectData(){
+ $this->collectExpandableDatas(self::URI . 'rss/' . $this->getInput('category') . '/', 10);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+
+ $item['content'] = '';
+
+ $namespaces = $newsItem->getNamespaces(true);
+ if(isset($namespaces['media'])) {
+ $media = $newsItem->children($namespaces['media']);
+ if(isset($media->content)) {
+ $attributes = $media->content->attributes();
+ $item['content'] = '<img src="' . $attributes['url'] . '">';
+ }
+ }
+
+ $item['content'] .= $this->extractContent($item['uri']);
+ return $item;
+ }
+
+ private function stripWithDelimiters($string, $start, $end){
+ while (strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ }
+
+ return $string;
+ }
+
+ private function extractContent($url){
+ $article = getSimpleHTMLDOMCached($url)
+ or returnServerError('Could not request Nextgov: ' . $url);
+
+ $contents = $article->find('div.wysiwyg', 0)->innertext;
+ $contents = $this->stripWithDelimiters($contents, '<div class="ad-container">', '</div>');
+ $contents = $this->stripWithDelimiters($contents, '<div', '</div>'); //ad outer div
+ return $this->stripWithDelimiters($contents, '<script', '</script>');
+ $contents = ($article_thumbnail == '' ? '' : '<p><img src="' . $article_thumbnail . '" /></p>')
+ . '<p><b>'
+ . $article_subtitle
+ . '</b></p>'
+ . trim($contents);
+ }
+}
diff --git a/bridges/NiceMatinBridge.php b/bridges/NiceMatinBridge.php
new file mode 100644
index 0000000..117c779
--- /dev/null
+++ b/bridges/NiceMatinBridge.php
@@ -0,0 +1,32 @@
+<?php
+class NiceMatinBridge extends FeedExpander {
+
+ const MAINTAINER = 'pit-fgfjiudghdf';
+ const NAME = 'NiceMatin';
+ const URI = 'http://www.nicematin.com/';
+ const DESCRIPTION = 'Returns the 10 newest posts from NiceMatin (full text)';
+
+ public function collectData(){
+ $this->collectExpandableDatas(self::URI . 'derniere-minute/rss', 10);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $item['content'] = $this->extractContent($item['uri']);
+ return $item;
+ }
+
+ private function extractContent($url){
+ $html = getSimpleHTMLDOMCached($url);
+ if(!$html)
+ return 'Could not acquire content from url: ' . $url . '!';
+
+ $content = $html->find('article', 0);
+ if(!$content)
+ return 'Could not find \'section\'!';
+
+ $text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $content->innertext);
+ $text = strip_tags($text, '<p><a><img>');
+ return $text;
+ }
+}
diff --git a/bridges/NotAlwaysBridge.php b/bridges/NotAlwaysBridge.php
new file mode 100644
index 0000000..f5efff4
--- /dev/null
+++ b/bridges/NotAlwaysBridge.php
@@ -0,0 +1,57 @@
+<?php
+class NotAlwaysBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'mozes';
+ const NAME = 'Not Always family Bridge';
+ const URI = 'https://notalwaysright.com/';
+ const DESCRIPTION = 'Returns the latest stories';
+ const CACHE_TIMEOUT = 1800; // 30 minutes
+
+ const PARAMETERS = array( array(
+ 'filter' => array(
+ 'type' => 'list',
+ 'name' => 'Filter',
+ 'values' => array(
+ 'All' => 'all',
+ 'Right' => 'right',
+ 'Working' => 'working',
+ 'Romantic' => 'romantic',
+ 'Related' => 'related',
+ 'Learning' => 'learning',
+ 'Friendly' => 'friendly',
+ 'Hopeless' => 'hopeless',
+ 'Unfiltered' => 'unfiltered'
+ ),
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request NotAlways.');
+ foreach($html->find('.post') as $post) {
+ #print_r($post);
+ $item = array();
+ $item['uri'] = $post->find('h1', 0)->find('a', 0)->href;
+ $item['content'] = $post;
+ $item['title'] = $post->find('h1', 0)->find('a', 0)->innertext;
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('filter'))) {
+ return $this->getInput('filter') . ' - NotAlways Bridge';
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('filter'))) {
+ return self::URI . $this->getInput('filter') . '/';
+ }
+
+ return parent::getURI();
+ }
+}
diff --git a/bridges/NovelUpdatesBridge.php b/bridges/NovelUpdatesBridge.php
new file mode 100644
index 0000000..729eb48
--- /dev/null
+++ b/bridges/NovelUpdatesBridge.php
@@ -0,0 +1,69 @@
+<?php
+class NovelUpdatesBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'albirew';
+ const NAME = 'Novel Updates';
+ const URI = 'http://www.novelupdates.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns releases from Novel Updates';
+ const PARAMETERS = array( array(
+ 'n' => array(
+ 'name' => 'Novel name as found in the url',
+ 'exampleValue' => 'spirit-realm',
+ 'required' => true
+ )
+ ));
+
+ private $seriesTitle = '';
+
+ public function getURI(){
+ if(!is_null($this->getInput('n'))) {
+ return static::URI . '/series/' . $this->getInput('n') . '/';
+ }
+
+ return parent::getURI();
+ }
+
+ public function collectData(){
+ $fullhtml = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request NovelUpdates, novel "' . $this->getInput('n') . '" not found');
+
+ $this->seriesTitle = $fullhtml->find('h4.seriestitle', 0)->plaintext;
+ // dirty fix for nasty simpledom bug: https://github.com/sebsauvage/rss-bridge/issues/259
+ // forcefully removes tbody
+ $html = $fullhtml->find('table#myTable', 0)->innertext;
+ $html = stristr($html, '<tbody>'); //strip thead
+ $html = stristr($html, '<tr>'); //remove tbody
+ $html = str_get_html(stristr($html, '</tbody>', true)); //remove last tbody and get back as an array
+ foreach($html->find('tr') as $element) {
+ $item = array();
+ $item['uri'] = $element->find('td', 2)->find('a', 0)->href;
+ $item['title'] = $element->find('td', 2)->find('a', 0)->plaintext;
+ $item['team'] = $element->find('td', 1)->innertext;
+ $item['timestamp'] = strtotime($element->find('td', 0)->plaintext);
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '">'
+ . $this->seriesTitle
+ . ' - '
+ . $item['title']
+ . '</a> by '
+ . $item['team']
+ . '<br><a href="'
+ . $item['uri']
+ . '">'
+ . $fullhtml->find('div.seriesimg', 0)->innertext
+ . '</a>';
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName(){
+ if(!empty($this->seriesTitle)) {
+ return $this->seriesTitle . ' - ' . static::NAME;
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/OpenClassroomsBridge.php b/bridges/OpenClassroomsBridge.php
new file mode 100644
index 0000000..5f0daca
--- /dev/null
+++ b/bridges/OpenClassroomsBridge.php
@@ -0,0 +1,49 @@
+<?php
+class OpenClassroomsBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'sebsauvage';
+ const NAME = 'OpenClassrooms Bridge';
+ const URI = 'https://openclassrooms.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns latest tutorials from OpenClassrooms.';
+
+ const PARAMETERS = array( array(
+ 'u' => array(
+ 'name' => 'Catégorie',
+ 'type' => 'list',
+ 'required' => true,
+ 'values' => array(
+ 'Arts & Culture' => 'arts',
+ 'Code' => 'code',
+ 'Design' => 'design',
+ 'Entreprise' => 'business',
+ 'Numérique' => 'digital',
+ 'Sciences' => 'sciences',
+ 'Sciences Humaines' => 'humainities',
+ 'Systèmes d\'information' => 'it',
+ 'Autres' => 'others'
+ )
+ )
+ ));
+
+ public function getURI(){
+ if(!is_null($this->getInput('u'))) {
+ return self::URI . '/courses?categories=' . $this->getInput('u') . '&title=&sort=updatedAt+desc';
+ }
+
+ return parent::getURI();
+ }
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request OpenClassrooms.');
+
+ foreach($html->find('.courseListItem') as $element) {
+ $item = array();
+ $item['uri'] = self::URI . $element->find('a', 0)->href;
+ $item['title'] = $element->find('h3', 0)->plaintext;
+ $item['content'] = $element->find('slidingItem__descriptionContent', 0)->plaintext;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/ParuVenduImmoBridge.php b/bridges/ParuVenduImmoBridge.php
new file mode 100644
index 0000000..a2e2b33
--- /dev/null
+++ b/bridges/ParuVenduImmoBridge.php
@@ -0,0 +1,102 @@
+<?php
+class ParuVenduImmoBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'polo2ro';
+ const NAME = 'Paru Vendu Immobilier';
+ const URI = 'http://www.paruvendu.fr';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the ads from the first page of search result.';
+
+ const PARAMETERS = array( array(
+ 'minarea' => array(
+ 'name' => 'Minimal surface m²',
+ 'type' => 'number'
+ ),
+ 'maxprice' => array(
+ 'name' => 'Max price',
+ 'type' => 'number'
+ ),
+ 'pa' => array(
+ 'name' => 'Country code',
+ 'exampleValue' => 'FR'
+ ),
+ 'lo' => array(
+ 'name' => 'department numbers or postal codes, comma-separated'
+ )
+ ));
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request paruvendu.');
+
+ foreach($html->find('div.annonce a') as $element) {
+
+ if(!$element->title) {
+ continue;
+ }
+
+ $img = '';
+ foreach($element->find('span.img img') as $img) {
+ if($img->original) {
+ $img = '<img src="' . $img->original . '" />';
+ }
+ }
+
+ $desc = $element->find('span.desc')[0]->innertext;
+ $desc = str_replace("voir l'annonce", '', $desc);
+
+ $price = $element->find('span.price')[0]->innertext;
+
+ list($href) = explode('#', $element->href);
+
+ $item = array();
+ $item['uri'] = self::URI . $href;
+ $item['title'] = $element->title;
+ $item['content'] = $img . $desc . $price;
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI(){
+ $appartment = '&tbApp=1&tbDup=1&tbChb=1&tbLof=1&tbAtl=1&tbPla=1';
+ $maison = '&tbMai=1&tbVil=1&tbCha=1&tbPro=1&tbHot=1&tbMou=1&tbFer=1';
+ $link = self::URI
+ . '/immobilier/annonceimmofo/liste/listeAnnonces?tt=1'
+ . $appartment
+ . $maison;
+
+ if($this->getInput('minarea')) {
+ $link .= '&sur0=' . urlencode($this->getInput('minarea'));
+ }
+
+ if($this->getInput('maxprice')) {
+ $link .= '&px1=' . urlencode($this->getInput('maxprice'));
+ }
+
+ if($this->getInput('pa')) {
+ $link .= '&pa=' . urlencode($this->getInput('pa'));
+ }
+
+ if($this->getInput('lo')) {
+ $link .= '&lo=' . urlencode($this->getInput('lo'));
+ }
+ return $link;
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('minarea'))) {
+ $request = '';
+ $minarea = $this->getInput('minarea');
+ if(!empty($minarea)) {
+ $request .= ' ' . $minarea . ' m2';
+ }
+ $location = $this->getInput('lo');
+ if(!empty($location)) {
+ $request .= ' In: ' . $location;
+ }
+ return 'Paru Vendu Immobilier' . $request;
+ }
+
+ return parent::getName();
+ }
+}
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/PickyWallpapersBridge.php b/bridges/PickyWallpapersBridge.php
new file mode 100644
index 0000000..6c26df7
--- /dev/null
+++ b/bridges/PickyWallpapersBridge.php
@@ -0,0 +1,101 @@
+<?php
+class PickyWallpapersBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'nel50n';
+ const NAME = 'PickyWallpapers Bridge';
+ const URI = 'http://www.pickywallpapers.com/';
+ const CACHE_TIMEOUT = 43200; // 12h
+ const DESCRIPTION = 'Returns the latests wallpapers from PickyWallpapers';
+
+ const PARAMETERS = array( array(
+ 'c' => array(
+ 'name' => 'category',
+ 'required' => true
+ ),
+ 's' => array(
+ 'name' => 'subcategory'
+ ),
+ 'm' => array(
+ 'name' => 'Max number of wallpapers',
+ 'defaultValue' => 12,
+ 'type' => 'number'
+ ),
+ 'r' => array(
+ 'name' => 'resolution',
+ 'exampleValue' => '1920x1200, 1680x1050,…',
+ 'defaultValue' => '1920x1200',
+ 'pattern' => '[0-9]{3,4}x[0-9]{3,4}'
+ )
+ ));
+
+ public function collectData(){
+ $lastpage = 1;
+ $num = 0;
+ $max = $this->getInput('m');
+ $resolution = $this->getInput('r'); // Wide wallpaper default
+
+ for($page = 1; $page <= $lastpage; $page++) {
+ $html = getSimpleHTMLDOM($this->getURI() . '/page-' . $page . '/')
+ or returnServerError('No results for this query.');
+
+ if($page === 1) {
+ preg_match('/page-(\d+)\/$/', $html->find('.pages li a', -2)->href, $matches);
+ $lastpage = min($matches[1], ceil($max / 12));
+ }
+
+ foreach($html->find('.items li img') as $element) {
+ $item = array();
+ $item['uri'] = str_replace('www', 'wallpaper', self::URI)
+ . '/'
+ . $resolution
+ . '/'
+ . basename($element->src);
+
+ $item['timestamp'] = time();
+ $item['title'] = $element->alt;
+ $item['content'] = $item['title']
+ . '<br><a href="'
+ . $item['uri']
+ . '">'
+ . $element
+ . '</a>';
+
+ $this->items[] = $item;
+
+ $num++;
+ if ($num >= $max)
+ break 2;
+ }
+ }
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('s')) && !is_null($this->getInput('r')) && !is_null($this->getInput('c'))) {
+ $subcategory = $this->getInput('s');
+ $link = self::URI
+ . $this->getInput('r')
+ . '/'
+ . $this->getInput('c')
+ . '/'
+ . $subcategory;
+
+ return $link;
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('s'))) {
+ $subcategory = $this->getInput('s');
+ return 'PickyWallpapers - '
+ . $this->getInput('c')
+ . ($subcategory ? ' > ' . $subcategory : '')
+ . ' ['
+ . $this->getInput('r')
+ . ']';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/PinterestBridge.php b/bridges/PinterestBridge.php
new file mode 100644
index 0000000..d2dd890
--- /dev/null
+++ b/bridges/PinterestBridge.php
@@ -0,0 +1,121 @@
+<?php
+class PinterestBridge extends FeedExpander {
+
+ const MAINTAINER = 'pauder';
+ const NAME = 'Pinterest Bridge';
+ const URI = 'https://www.pinterest.com';
+ const DESCRIPTION = 'Returns the newest images on a board';
+
+ const PARAMETERS = array(
+ 'By username and board' => array(
+ 'u' => array(
+ 'name' => 'username',
+ 'required' => true
+ ),
+ 'b' => array(
+ 'name' => 'board',
+ 'required' => true
+ )
+ ),
+ 'From search' => array(
+ 'q' => array(
+ 'name' => 'Keyword',
+ 'required' => true
+ )
+ )
+ );
+
+ public function collectData(){
+ switch($this->queriedContext) {
+ case 'By username and board':
+ $this->collectExpandableDatas($this->getURI() . '.rss');
+ $this->fixLowRes();
+ break;
+ case 'From search':
+ default:
+ $html = getSimpleHTMLDOMCached($this->getURI());
+ $this->getSearchResults($html);
+ }
+ }
+
+ private function fixLowRes() {
+
+ $newitems = [];
+ $pattern = '/https\:\/\/i\.pinimg\.com\/[a-zA-Z0-9]*x\//';
+ foreach($this->items as $item) {
+
+ $item['content'] = preg_replace($pattern, 'https://i.pinimg.com/originals/', $item['content']);
+ $newitems[] = $item;
+ }
+ $this->items = $newitems;
+
+ }
+
+ private function getSearchResults($html){
+ $json = json_decode($html->find('#jsInit1', 0)->innertext, true);
+ $results = $json['resourceDataCache'][0]['data']['results'];
+
+ foreach($results as $result) {
+ $item = array();
+
+ $item['uri'] = self::URI . $result['board']['url'];
+
+ // 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['grid_description']);
+
+ $item['timestamp'] = strtotime($result['created_at']);
+ $item['username'] = $result['pinner']['username'];
+ $item['fullname'] = $result['pinner']['full_name'];
+ $item['avatar'] = $result['pinner']['image_small_url'];
+ $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>';
+
+ $item['enclosures'] = array($result['images']['orig']['url']);
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI(){
+ switch($this->queriedContext) {
+ case 'By username and board':
+ $uri = self::URI . '/' . urlencode($this->getInput('u')) . '/' . urlencode($this->getInput('b'));// . '.rss';
+ break;
+ case 'From search':
+ $uri = self::URI . '/search/?q=' . urlencode($this->getInput('q'));
+ break;
+ default: return parent::getURI();
+ }
+ return $uri;
+ }
+
+ public function getName(){
+ switch($this->queriedContext) {
+ case 'By username and board':
+ $specific = $this->getInput('u') . ' - ' . $this->getInput('b');
+ break;
+ case 'From search':
+ $specific = $this->getInput('q');
+ break;
+ default: return parent::getName();
+ }
+ return $specific . ' - ' . self::NAME;
+ }
+}
diff --git a/bridges/PixivBridge.php b/bridges/PixivBridge.php
new file mode 100644
index 0000000..3a4cc93
--- /dev/null
+++ b/bridges/PixivBridge.php
@@ -0,0 +1,73 @@
+<?php
+class PixivBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'Pixiv Bridge';
+ const URI = 'https://www.pixiv.net/';
+ const DESCRIPTION = 'Returns the tag search from pixiv.net';
+
+
+ const PARAMETERS = array( array(
+ 'tag' => array(
+ 'name' => 'Tag to search',
+ 'exampleValue' => 'example',
+ 'required' => true
+ ),
+ ));
+
+
+ public function collectData(){
+
+ $html = getContents(static::URI.'search.php?word=' . urlencode($this->getInput('tag')))
+ or returnClientError('Unable to query pixiv.net');
+ $regex = '/<input type="hidden"id="js-mount-point-search-result-list"data-items="([^"]*)/';
+ $timeRegex = '/img\/([0-9]{4})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\//';
+
+ preg_match_all($regex, $html, $matches, PREG_SET_ORDER, 0);
+ if(!$matches) return;
+
+ $content = json_decode(html_entity_decode($matches[0][1]), true);
+ $count = 0;
+ foreach($content as $result) {
+ if($count == 10) break;
+ $count++;
+
+ $item = array();
+ $item['id'] = $result['illustId'];
+ $item['uri'] = 'https://www.pixiv.net/member_illust.php?mode=medium&illust_id=' . $result['illustId'];
+ $item['title'] = $result['illustTitle'];
+ $item['author'] = $result['userName'];
+
+ preg_match_all($timeRegex, $result['url'], $dt, PREG_SET_ORDER, 0);
+ $elementDate = DateTime::createFromFormat('YmdHis',
+ $dt[0][1] . $dt[0][2] . $dt[0][3] . $dt[0][4] . $dt[0][5] . $dt[0][6]);
+ $item['timestamp'] = $elementDate->getTimestamp();
+
+ $item['content'] = "<img src='" . $this->cacheImage($result['url'], $item['id']) . "' />";
+ $this->items[] = $item;
+ }
+ }
+
+ public function cacheImage($url, $illustId) {
+
+ $url = str_replace('_master1200', '', $url);
+ $url = str_replace('c/240x240/img-master/', 'img-original/', $url);
+ $path = CACHE_DIR . '/pixiv_img';
+
+ if(!is_dir($path))
+ mkdir($path, 0755, true);
+
+ if(!is_file($path . '/' . $illustId . '.jpeg')) {
+ $headers = array('Referer: https://www.pixiv.net/member_illust.php?mode=medium&illust_id=' . $illustId);
+ $illust = getContents($url, $headers);
+ if(strpos($illust, '404 Not Found') !== false) {
+ $illust = getContents(str_replace('jpg', 'png', $url), $headers);
+ }
+ file_put_contents($path . '/' . $illustId . '.jpeg', $illust);
+ }
+
+ return 'cache/pixiv_img/' . $illustId . '.jpeg';
+
+ }
+
+}
diff --git a/bridges/RTBFBridge.php b/bridges/RTBFBridge.php
new file mode 100644
index 0000000..22cdaf4
--- /dev/null
+++ b/bridges/RTBFBridge.php
@@ -0,0 +1,66 @@
+<?php
+class RTBFBridge extends BridgeAbstract {
+ const NAME = 'RTBF Bridge';
+ const URI = 'http://www.rtbf.be/auvio/';
+ const CACHE_TIMEOUT = 21600; //6h
+ const DESCRIPTION = 'Returns the newest RTBF videos by series ID';
+ const MAINTAINER = 'Frenzie';
+
+ const PARAMETERS = array( array(
+ 'c' => array(
+ 'name' => 'series id',
+ 'exampleValue' => 9500,
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $html = '';
+ $limit = 10;
+ $count = 0;
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request RTBF.');
+
+ foreach($html->find('section[id!=widget-ml-avoiraussi-] .rtbf-media-grid article') as $element) {
+ if($count >= $limit) {
+ break;
+ }
+
+ $item = array();
+ $item['id'] = $element->getAttribute('data-id');
+ $item['uri'] = self::URI . 'detail?id=' . $item['id'];
+ $thumbnailUriSrcSet = explode(
+ ',',
+ $element->find('figure .www-img-16by9 img', 0)->getAttribute('data-srcset')
+ );
+
+ $thumbnailUriLastSrc = end($thumbnailUriSrcSet);
+ $thumbnailUri = explode(' ', $thumbnailUriLastSrc)[0];
+ $item['title'] = trim($element->find('h3', 0)->plaintext)
+ . ' - '
+ . trim($element->find('h4', 0)->plaintext);
+
+ $item['timestamp'] = strtotime($element->find('time', 0)->getAttribute('datetime'));
+ $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $thumbnailUri . '" /></a>';
+ $this->items[] = $item;
+ $count++;
+ }
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('c'))) {
+ return self::URI . 'emissions/detail?id=' . $this->getInput('c');
+ }
+
+ return parent::getURI() . 'emissions/';
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('c'))) {
+ return $this->getInput('c') .' - RTBF Bridge';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/RadioMelodieBridge.php b/bridges/RadioMelodieBridge.php
new file mode 100644
index 0000000..9b3772b
--- /dev/null
+++ b/bridges/RadioMelodieBridge.php
@@ -0,0 +1,30 @@
+<?php
+class RadioMelodieBridge extends BridgeAbstract {
+ const NAME = 'Radio Melodie Actu';
+ const URI = 'https://www.radiomelodie.com/';
+ const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie';
+ const MAINTAINER = 'sysadminstory';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI . 'actu')
+ or returnServerError('Could not request Radio Melodie.');
+ $list = $html->find('div[class=actuitem]');
+ foreach($list as $element) {
+ $item = array();
+
+ // Get picture URL
+ $pictureHTML = $element->find('div[class=picture]');
+ preg_match(
+ '/background-image:url\((.*)\);/',
+ $pictureHTML[0]->getAttribute('style'),
+ $pictures);
+ $pictureURL = $pictures[1];
+
+ $item['enclosures'] = array($pictureURL);
+ $item['uri'] = self::URI . $element->parent()->href;
+ $item['title'] = $element->find('h3', 0)->plaintext;
+ $item['content'] = $element->find('p', 0)->plaintext . '<br/><img src="'.$pictureURL.'"/>';
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/RainbowSixSiegeBridge.php b/bridges/RainbowSixSiegeBridge.php
new file mode 100644
index 0000000..d362bbd
--- /dev/null
+++ b/bridges/RainbowSixSiegeBridge.php
@@ -0,0 +1,36 @@
+<?php
+class RainbowSixSiegeBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'corenting';
+ const NAME = 'Rainbow Six Siege Blog';
+ const URI = 'https://rainbow6.ubisoft.com/siege/en-us/news/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Latest articles from the Rainbow Six Siege blog';
+
+ public function collectData(){
+ $dlUrl = 'https://prod-tridionservice.ubisoft.com/live/v1/News/Latest?templateId=tcm%3A152-7677';
+ $dlUrl .= '8-32&pageIndex=0&pageSize=10&language=en-US&detailPageId=tcm%3A152-194572-64';
+ $dlUrl .= '&keywordList=175426&siteId=undefined&useSeoFriendlyUrl=true';
+ $jsonString = getContents($dlUrl) or returnServerError('Error while downloading the website content');
+
+ $json = json_decode($jsonString, true);
+ $json = $json['items'];
+
+ // Start at index 2 to remove highlighted articles
+ for($i = 0; $i < count($json); $i++) {
+ $jsonItem = $json[$i]['Content'];
+ $article = str_get_html($jsonItem);
+
+ $item = array();
+
+ $uri = $article->find('h3 a', 0)->href;
+ $uri = 'https://rainbow6.ubisoft.com' . $uri;
+ $item['uri'] = $uri;
+ $item['title'] = $article->find('h3', 0)->plaintext;
+ $item['content'] = $article->find('img', 0)->outertext . '<br />' . $article->find('strong', 0)->plaintext;
+ $item['timestamp'] = strtotime($article->find('p.news_date', 0)->plaintext);
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/ReadComicsBridge.php b/bridges/ReadComicsBridge.php
new file mode 100644
index 0000000..739e6cc
--- /dev/null
+++ b/bridges/ReadComicsBridge.php
@@ -0,0 +1,44 @@
+<?php
+class ReadComicsBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'niawag';
+ const NAME = 'Read Comics';
+ const URI = 'http://www.readcomics.tv/';
+ const DESCRIPTION = 'Enter the comics as they appear in the website uri,
+ separated by semicolons, ex: good-comic-1;good-comic-2; ...';
+
+ const PARAMETERS = array( array(
+ 'q' => array(
+ 'name' => 'keywords, separated by semicolons',
+ 'exampleValue' => 'first list;second list;...',
+ 'required' => true
+ ),
+ ));
+
+ public function collectData(){
+
+ function parseDateTimestamp($element){
+ $guessedDate = $element->find('span', 0)->plaintext;
+ $guessedDate = strptime($guessedDate, '%m/%d/%Y');
+ $timestamp = mktime(0, 0, 0, $guessedDate['tm_mon'] + 1, $guessedDate['tm_mday'], date('Y'));
+
+ return $timestamp;
+ }
+
+ $keywordsList = explode(';', $this->getInput('q'));
+ foreach($keywordsList as $keywords) {
+ $html = $this->getSimpleHTMLDOM(self::URI . 'comic/' . rawurlencode($keywords))
+ or $this->returnServerError('Could not request readcomics.tv.');
+
+ foreach($html->find('li') as $element) {
+ $item = array();
+ $item['uri'] = $element->find('a.ch-name', 0)->href;
+ $item['id'] = $item['uri'];
+ $item['timestamp'] = parseDateTimestamp($element);
+ $item['title'] = $element->find('a.ch-name', 0)->plaintext;
+ if(isset($item['title']))
+ $this->items[] = $item;
+ }
+ }
+ }
+}
diff --git a/bridges/Releases3DSBridge.php b/bridges/Releases3DSBridge.php
new file mode 100644
index 0000000..a7e1778
--- /dev/null
+++ b/bridges/Releases3DSBridge.php
@@ -0,0 +1,136 @@
+<?php
+class Releases3DSBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = '3DS Scene Releases';
+ const URI = 'http://www.3dsdb.com/';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the newest scene releases.';
+
+ public function collectData(){
+
+ function extractFromDelimiters($string, $start, $end){
+ if(strpos($string, $start) !== false) {
+ $section_retrieved = substr($string, strpos($string, $start) + strlen($start));
+ $section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
+ return $section_retrieved;
+ }
+
+ return false;
+ }
+
+ function typeToString($type){
+ switch($type) {
+ case 1: return '3DS Game';
+ case 4: return 'eShop';
+ default: return '??? (' . $type . ')';
+ }
+ }
+
+ function cardToString($card){
+ switch($card) {
+ case 1: return 'Regular (CARD1)';
+ case 2: return 'NAND (CARD2)';
+ default: return '??? (' . $card . ')';
+ }
+ }
+
+ $dataUrl = self::URI . 'xml.php';
+ $xml = getContents($dataUrl)
+ or returnServerError('Could not request 3dsdb: ' . $dataUrl);
+ $limit = 0;
+
+ foreach(array_reverse(explode('<release>', $xml)) as $element) {
+ if($limit >= 5) {
+ break;
+ }
+
+ if(strpos($element, '</release>') === false) {
+ continue;
+ }
+
+ $releasename = extractFromDelimiters($element, '<releasename>', '</releasename>');
+ if(empty($releasename)) {
+ continue;
+ }
+
+ $id = extractFromDelimiters($element, '<id>', '</id>');
+ $name = extractFromDelimiters($element, '<name>', '</name>');
+ $publisher = extractFromDelimiters($element, '<publisher>', '</publisher>');
+ $region = extractFromDelimiters($element, '<region>', '</region>');
+ $group = extractFromDelimiters($element, '<group>', '</group>');
+ $imagesize = extractFromDelimiters($element, '<imagesize>', '</imagesize>');
+ $serial = extractFromDelimiters($element, '<serial>', '</serial>');
+ $titleid = extractFromDelimiters($element, '<titleid>', '</titleid>');
+ $imgcrc = extractFromDelimiters($element, '<imgcrc>', '</imgcrc>');
+ $filename = extractFromDelimiters($element, '<filename>', '</filename>');
+ $trimmedsize = extractFromDelimiters($element, '<trimmedsize>', '</trimmedsize>');
+ $firmware = extractFromDelimiters($element, '<firmware>', '</firmware>');
+ $type = extractFromDelimiters($element, '<type>', '</type>');
+ $card = extractFromDelimiters($element, '<card>', '</card>');
+
+ //Retrieve cover art and short desc from IGN?
+ $ignResult = false;
+ $ignDescription = '';
+ $ignLink = '';
+ $ignDate = time();
+ $ignCoverArt = '';
+
+ $ignSearchUrl = 'http://www.ign.com/search?q=' . urlencode($name);
+ if($ignResult = getSimpleHTMLDOM($ignSearchUrl)) {
+ $ignCoverArt = $ignResult->find('div.search-item-media', 0)->find('img', 0)->src;
+ $ignDesc = $ignResult->find('div.search-item-description', 0)->plaintext;
+ $ignLink = $ignResult->find('div.search-item-sub-title', 0)->find('a', 1)->href;
+ $ignDate = strtotime(trim($ignResult->find('span.publish-date', 0)->plaintext));
+ $ignDescription = '<div><img src="'
+ . $ignCoverArt
+ . '" /></div><div>'
+ . $ignDesc
+ . ' <a href="'
+ . $ignLink
+ . '">More at IGN</a></div>';
+ }
+
+ //Main section : Release description from 3DS database
+ $releaseDescription = '<h3>Release Details</h3><b>Release ID: </b>' . $id
+ . '<br /><b>Game Name: </b>' . $name
+ . '<br /><b>Publisher: </b>' . $publisher
+ . '<br /><b>Region: </b>' . $region
+ . '<br /><b>Group: </b>' . $group
+ . '<br /><b>Image size: </b>' . (intval($imagesize) / 8)
+ . 'MB<br /><b>Serial: </b>' . $serial
+ . '<br /><b>Title ID: </b>' . $titleid
+ . '<br /><b>Image CRC: </b>' . $imgcrc
+ . '<br /><b>File Name: </b>' . $filename
+ . '<br /><b>Release Name: </b>' . $releasename
+ . '<br /><b>Trimmed size: </b>' . intval(intval($trimmedsize) / 1048576)
+ . 'MB<br /><b>Firmware: </b>' . $firmware
+ . '<br /><b>Type: </b>' . typeToString($type)
+ . '<br /><b>Card: </b>' . cardToString($card)
+ . '<br />';
+
+ //Build search links section to facilitate release search using search engines
+ $releaseNameEncoded = urlencode(str_replace(' ', '+', $releasename));
+ $searchLinkGoogle = 'https://google.com/?q=' . $releaseNameEncoded;
+ $searchLinkDuckDuckGo = 'https://duckduckgo.com/?q=' . $releaseNameEncoded;
+ $searchLinkQwant = 'https://lite.qwant.com/?q=' . $releaseNameEncoded . '&t=web';
+ $releaseSearchLinks = '<h3>Search this release</h3><ul><li><a href="'
+ . $searchLinkGoogle
+ . '">Search using Google</a></li><li><a href="'
+ . $searchLinkDuckDuckGo
+ . '">Search using DuckDuckGo</a></li><li><a href="'
+ . $searchLinkQwant
+ . '">Search using Qwant</a></li></ul>';
+
+ //Build and add final item with the above three sections
+ $item = array();
+ $item['title'] = $name;
+ $item['author'] = $publisher;
+ $item['timestamp'] = $ignDate;
+ $item['uri'] = empty($ignLink) ? $searchLinkDuckDuckGo : $ignLink;
+ $item['content'] = $ignDescription . $releaseDescription . $releaseSearchLinks;
+ $this->items[] = $item;
+ $limit++;
+ }
+ }
+}
diff --git a/bridges/ReporterreBridge.php b/bridges/ReporterreBridge.php
new file mode 100644
index 0000000..438c55b
--- /dev/null
+++ b/bridges/ReporterreBridge.php
@@ -0,0 +1,47 @@
+<?php
+class ReporterreBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'nyutag';
+ const NAME = 'Reporterre Bridge';
+ const URI = 'http://www.reporterre.net/';
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ private function extractContent($url){
+ $html2 = getSimpleHTMLDOM($url);
+
+ foreach($html2->find('div[style=text-align:justify]') as $e) {
+ $text = $e->outertext;
+ }
+
+ $html2->clear();
+ unset($html2);
+
+ // Replace all relative urls with absolute ones
+ $text = preg_replace(
+ '/(href|src)(\=[\"\'])(?!http)([^"\']+)/ims',
+ '$1$2' . self::URI . '$3',
+ $text
+ );
+
+ $text = strip_tags($text, '<p><br><a><img>');
+ return $text;
+ }
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend')
+ or returnServerError('Could not request Reporterre.');
+ $limit = 0;
+
+ foreach($html->find('item') as $element) {
+ if($limit < 5) {
+ $item = array();
+ $item['title'] = html_entity_decode($element->find('title', 0)->plaintext);
+ $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);
+ $item['uri'] = $element->find('guid', 0)->innertext;
+ $item['content'] = html_entity_decode($this->extractContent($item['uri']));
+ $this->items[] = $item;
+ $limit++;
+ }
+ }
+ }
+}
diff --git a/bridges/Rue89Bridge.php b/bridges/Rue89Bridge.php
new file mode 100644
index 0000000..72f01eb
--- /dev/null
+++ b/bridges/Rue89Bridge.php
@@ -0,0 +1,25 @@
+<?php
+class Rue89Bridge extends FeedExpander {
+
+ const MAINTAINER = 'pit-fgfjiudghdf';
+ const NAME = 'Rue89';
+ const URI = 'http://rue89.nouvelobs.com/';
+ const DESCRIPTION = 'Returns the 5 newest posts from Rue89 (full text)';
+
+ protected function parseItem($item){
+ $item = parent::parseItem($item);
+
+ $url = 'http://api.rue89.nouvelobs.com/export/mobile2/node/'
+ . str_replace(' ', '', substr($item['uri'], -8))
+ . '/full';
+
+ $datas = json_decode(getContents($url), true);
+ $item['content'] = $datas['node']['body'];
+
+ return $item;
+ }
+
+ public function collectData(){
+ $this->collectExpandableDatas('http://api.rue89.nouvelobs.com/feed');
+ }
+}
diff --git a/bridges/Rule34Bridge.php b/bridges/Rule34Bridge.php
new file mode 100644
index 0000000..b46ec00
--- /dev/null
+++ b/bridges/Rule34Bridge.php
@@ -0,0 +1,12 @@
+<?php
+require_once('GelbooruBridge.php');
+
+class Rule34Bridge extends GelbooruBridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Rule34';
+ const URI = 'http://rule34.xxx/';
+ const DESCRIPTION = 'Returns images from given page';
+
+ const PIDBYPAGE = 50;
+}
diff --git a/bridges/Rule34pahealBridge.php b/bridges/Rule34pahealBridge.php
new file mode 100644
index 0000000..1a74616
--- /dev/null
+++ b/bridges/Rule34pahealBridge.php
@@ -0,0 +1,10 @@
+<?php
+require_once('Shimmie2Bridge.php');
+
+class Rule34pahealBridge extends Shimmie2Bridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Rule34paheal';
+ const URI = 'http://rule34.paheal.net/';
+ const DESCRIPTION = 'Returns images from given page';
+}
diff --git a/bridges/SafebooruBridge.php b/bridges/SafebooruBridge.php
new file mode 100644
index 0000000..d95e557
--- /dev/null
+++ b/bridges/SafebooruBridge.php
@@ -0,0 +1,12 @@
+<?php
+require_once('GelbooruBridge.php');
+
+class SafebooruBridge extends GelbooruBridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Safebooru';
+ const URI = 'http://safebooru.org/';
+ const DESCRIPTION = 'Returns images from given page';
+
+ const PIDBYPAGE = 40;
+}
diff --git a/bridges/SakugabooruBridge.php b/bridges/SakugabooruBridge.php
new file mode 100644
index 0000000..1d6cee0
--- /dev/null
+++ b/bridges/SakugabooruBridge.php
@@ -0,0 +1,11 @@
+<?php
+require_once('MoebooruBridge.php');
+
+class SakugabooruBridge extends MoebooruBridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Sakugabooru';
+ const URI = 'http://sakuga.yshi.org/';
+ const DESCRIPTION = 'Returns images from given page';
+
+}
diff --git a/bridges/ScmbBridge.php b/bridges/ScmbBridge.php
new file mode 100644
index 0000000..2107aa3
--- /dev/null
+++ b/bridges/ScmbBridge.php
@@ -0,0 +1,39 @@
+<?php
+class ScmbBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Astalaseven';
+ const NAME = 'Se Coucher Moins Bête Bridge';
+ const URI = 'http://secouchermoinsbete.fr';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns the newest anecdotes.';
+
+ public function collectData(){
+ $html = '';
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request Se Coucher Moins Bete.');
+
+ foreach($html->find('article') as $article) {
+ $item = array();
+ $item['uri'] = self::URI . $article->find('p.summary a', 0)->href;
+ $item['title'] = $article->find('header h1 a', 0)->innertext;
+
+ // remove text "En savoir plus" from anecdote content
+ $article->find('span.read-more', 0)->outertext = '';
+ $content = $article->find('p.summary a', 0)->innertext;
+
+ // remove superfluous spaces at the end
+ $content = substr($content, 0, strlen($content) - 17);
+
+ // get publication date
+ $str_date = $article->find('time', 0)->datetime;
+ list($date, $time) = explode(' ', $str_date);
+ list($y, $m, $d) = explode('-', $date);
+ list($h, $i) = explode(':', $time);
+ $timestamp = mktime($h, $i, 0, $m, $d, $y);
+ $item['timestamp'] = $timestamp;
+
+ $item['content'] = $content;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/ScoopItBridge.php b/bridges/ScoopItBridge.php
new file mode 100644
index 0000000..997837d
--- /dev/null
+++ b/bridges/ScoopItBridge.php
@@ -0,0 +1,42 @@
+<?php
+class ScoopItBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Pitchoule';
+ const NAME = 'ScoopIt';
+ const URI = 'http://www.scoop.it/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns most recent results from ScoopIt.';
+
+ const PARAMETERS = array( array(
+ 'u' => array(
+ 'name' => 'keyword',
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $this->request = $this->getInput('u');
+ $link = self::URI . 'search?q=' . urlencode($this->getInput('u'));
+
+ $html = getSimpleHTMLDOM($link)
+ or returnServerError('Could not request ScoopIt. for : ' . $link);
+
+ foreach($html->find('div.post-view') as $element) {
+ $item = array();
+ $item['uri'] = $element->find('a', 0)->href;
+ $item['title'] = preg_replace(
+ '~[[:cntrl:]]~',
+ '',
+ $element->find('div.tCustomization_post_title', 0)->plaintext
+ );
+
+ $item['content'] = preg_replace(
+ '~[[:cntrl:]]~',
+ '',
+ $element->find('div.tCustomization_post_description', 0)->plaintext
+ );
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/SensCritiqueBridge.php b/bridges/SensCritiqueBridge.php
new file mode 100644
index 0000000..7ac35f2
--- /dev/null
+++ b/bridges/SensCritiqueBridge.php
@@ -0,0 +1,97 @@
+<?php
+class SensCritiqueBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'kranack';
+ const NAME = 'Sens Critique';
+ const URI = 'http://www.senscritique.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Sens Critique news';
+
+ const PARAMETERS = array( array(
+ 'm' => array(
+ 'name' => 'Movies',
+ 'type' => 'checkbox'
+ ),
+ 's' => array(
+ 'name' => 'Series',
+ 'type' => 'checkbox'
+ ),
+ 'g' => array(
+ 'name' => 'Video Games',
+ 'type' => 'checkbox'
+ ),
+ 'b' => array(
+ 'name' => 'Books',
+ 'type' => 'checkbox'
+ ),
+ 'bd' => array(
+ 'name' => 'BD',
+ 'type' => 'checkbox'
+ ),
+ 'mu' => array(
+ 'name' => 'Music',
+ 'type' => 'checkbox'
+ )
+ ));
+
+ public function collectData(){
+ $categories = array();
+ foreach(self::PARAMETERS[$this->queriedContext] as $category => $properties) {
+ if($this->getInput($category)) {
+ $uri = self::URI;
+ switch($category) {
+ case 'm': $uri .= 'films/cette-semaine';
+ break;
+ case 's': $uri .= 'series/actualite';
+ break;
+ case 'g': $uri .= 'jeuxvideo/actualite';
+ break;
+ case 'b': $uri .= 'livres/actualite';
+ break;
+ case 'bd': $uri .= 'bd/actualite';
+ break;
+ case 'mu': $uri .= 'musique/actualite';
+ break;
+ }
+ $html = getSimpleHTMLDOM($uri)
+ or returnServerError('No results for this query.');
+ $list = $html->find('ul.elpr-list', 0);
+
+ $this->extractDataFromList($list);
+ }
+ }
+ }
+
+ private function extractDataFromList($list){
+ if($list === null) {
+ returnClientError('Cannot extract data from list');
+ }
+
+ foreach($list->find('li') as $movie) {
+ $item = array();
+ $item['author'] = htmlspecialchars_decode($movie->find('.elco-title a', 0)->plaintext, ENT_QUOTES)
+ . ' '
+ . $movie->find('.elco-date', 0)->plaintext;
+
+ $item['title'] = $movie->find('.elco-title a', 0)->plaintext
+ . ' '
+ . $movie->find('.elco-date', 0)->plaintext;
+
+ $item['content'] = '<em>'
+ . $movie->find('.elco-original-title', 0)->plaintext
+ . '</em><br><br>'
+ . $movie->find('.elco-baseline', 0)->plaintext
+ . '<br>'
+ . $movie->find('.elco-baseline', 1)->plaintext
+ . '<br><br>'
+ . $movie->find('.elco-description', 0)->plaintext
+ . '<br><br>'
+ . trim($movie->find('.erra-ratings .erra-global', 0)->plaintext)
+ . ' / 10';
+
+ $item['id'] = $this->getURI() . $movie->find('.elco-title a', 0)->href;
+ $item['uri'] = $this->getURI() . $movie->find('.elco-title a', 0)->href;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/SexactuBridge.php b/bridges/SexactuBridge.php
new file mode 100644
index 0000000..b0a7174
--- /dev/null
+++ b/bridges/SexactuBridge.php
@@ -0,0 +1,88 @@
+<?php
+class SexactuBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Riduidel';
+ const NAME = 'Sexactu';
+ const AUTHOR = 'Maïa Mazaurette';
+ const URI = 'http://www.gqmagazine.fr';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Sexactu via rss-bridge';
+
+ const REPLACED_ATTRIBUTES = array(
+ 'href' => 'href',
+ 'src' => 'src',
+ 'data-original' => 'src'
+ );
+
+ public function getURI(){
+ return self::URI . '/sexactu';
+ }
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request ' . $this->getURI());
+
+ $sexactu = $html->find('.container_sexactu', 0);
+ $rowList = $sexactu->find('.row');
+ foreach($rowList as $row) {
+ // only use first list as second one only contains pages numbers
+
+ $title = $row->find('.title', 0);
+ if($title) {
+ $item = array();
+ $item['author'] = self::AUTHOR;
+ $item['title'] = $title->plaintext;
+ $urlAttribute = 'data-href';
+ $uri = $title->$urlAttribute;
+ if($uri === false)
+ continue;
+ if(substr($uri, 0, 1) === 'h') { // absolute uri
+ $item['uri'] = $uri;
+ } else if(substr($uri, 0, 1) === '/') { // domain relative url
+ $item['uri'] = self::URI . $uri;
+ } else {
+ $item['uri'] = $this->getURI() . $uri;
+ }
+ $article = $this->loadFullArticle($item['uri']);
+ $item['content'] = $this->replaceUriInHtmlElement($article->find('.article_content', 0));
+
+ $publicationDate = $article->find('time[itemprop=datePublished]', 0);
+ $short_date = $publicationDate->datetime;
+ $item['timestamp'] = strtotime($short_date);
+ } else {
+ // Sometimes we get rubbish, ignore.
+ continue;
+ }
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Loads the full article and returns the contents
+ * @param $uri The article URI
+ * @return The article content
+ */
+ private function loadFullArticle($uri){
+ $html = getSimpleHTMLDOMCached($uri);
+
+ $content = $html->find('#article', 0);
+ if($content) {
+ return $content;
+ }
+
+ return null;
+ }
+
+ /**
+ * Replaces all relative URIs with absolute ones
+ * @param $element A simplehtmldom element
+ * @return The $element->innertext with all URIs replaced
+ */
+ private function replaceUriInHtmlElement($element){
+ $returned = $element->innertext;
+ foreach (self::REPLACED_ATTRIBUTES as $initial => $final) {
+ $returned = str_replace($initial . '="/', $final . '="' . self::URI . '/', $returned);
+ }
+ return $returned;
+ }
+}
diff --git a/bridges/ShanaprojectBridge.php b/bridges/ShanaprojectBridge.php
new file mode 100644
index 0000000..6eadcb1
--- /dev/null
+++ b/bridges/ShanaprojectBridge.php
@@ -0,0 +1,123 @@
+<?php
+class ShanaprojectBridge extends BridgeAbstract {
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Shanaproject Bridge';
+ const URI = 'http://www.shanaproject.com';
+ const DESCRIPTION = 'Returns a list of anime from the current Season Anime List';
+
+ // Returns an html object for the Season Anime List (latest season)
+ private function loadSeasonAnimeList(){
+ // First we need to find the URI to the latest season from the
+ // 'seasons' page searching for 'Season Anime List'
+ $html = getSimpleHTMLDOM($this->getURI() . '/seasons');
+ if(!$html)
+ returnServerError('Could not load \'seasons\' page!');
+
+ $season = $html->find('div.follows_menu/a', 1);
+ if(!$season)
+ returnServerError('Could not find \'Season Anime List\'!');
+
+ $html = getSimpleHTMLDOM($this->getURI() . $season->href);
+ if(!$html)
+ returnServerError(
+ 'Could not load \'Season Anime List\' from \''
+ . $season->innertext
+ . '\'!'
+ );
+
+ return $html;
+ }
+
+ // Extracts the anime title
+ private function extractAnimeTitle($anime){
+ $title = $anime->find('a', 0);
+ if(!$title)
+ returnServerError('Could not find anime title!');
+ return trim($title->innertext);
+ }
+
+ // Extracts the anime URI
+ private function extractAnimeUri($anime){
+ $uri = $anime->find('a', 0);
+ if(!$uri)
+ returnServerError('Could not find anime URI!');
+ return $this->getURI() . $uri->href;
+ }
+
+ // Extracts the anime release date (timestamp)
+ private function extractAnimeTimestamp($anime){
+ $timestamp = $anime->find('span.header_info_block', 1);
+ if(!$timestamp)
+ return null;
+ return strtotime($timestamp->innertext);
+ }
+
+ // Extracts the anime studio name (author)
+ private function extractAnimeAuthor($anime){
+ $author = $anime->find('span.header_info_block', 2);
+ if(!$author)
+ return; // Sometimes the studio is unknown, so leave empty
+ return trim($author->innertext);
+ }
+
+ // Extracts the episode information (x of y released)
+ private function extractAnimeEpisodeInformation($anime){
+ $episode = $anime->find('div.header_info_episode', 0);
+ if(!$episode)
+ returnServerError('Could not find anime episode information!');
+ return preg_replace('/\r|\n/', ' ', $episode->plaintext);
+ }
+
+ // Extracts the background image
+ private function extractAnimeBackgroundImage($anime){
+ // Getting the picture is a little bit tricky as it is part of the style.
+ // Luckily the style is part of the parent div :)
+
+ if(preg_match('/url\(\/\/([^\)]+)\)/i', $anime->parent->style, $matches))
+ return $matches[1];
+
+ returnServerError('Could not extract background image!');
+ }
+
+ // Builds an URI to search for a specific anime (subber is left empty)
+ private function buildAnimeSearchUri($anime){
+ return $this->getURI()
+ . '/search/?title='
+ . urlencode($this->extractAnimeTitle($anime))
+ . '&subber=';
+ }
+
+ // Builds the content string for a given anime
+ private function buildAnimeContent($anime){
+ // We'll use a template string to place our contents
+ return '<a href="'
+ . $this->extractAnimeUri($anime)
+ . '"><img src="http://'
+ . $this->extractAnimeBackgroundImage($anime)
+ . '" alt="'
+ . htmlspecialchars($this->extractAnimeTitle($anime))
+ . '" style="border: 1px solid black"></a><br><p>'
+ . $this->extractAnimeEpisodeInformation($anime)
+ . '</p><br><p><a href="'
+ . $this->buildAnimeSearchUri($anime)
+ . '">Search episodes</a></p>';
+ }
+
+ public function collectData(){
+ $html = $this->loadSeasonAnimeList();
+
+ $animes = $html->find('div.header_display_box_info');
+ if(!$animes)
+ returnServerError('Could not find anime headers!');
+
+ foreach($animes as $anime) {
+ $item = array();
+ $item['title'] = $this->extractAnimeTitle($anime);
+ $item['author'] = $this->extractAnimeAuthor($anime);
+ $item['uri'] = $this->extractAnimeUri($anime);
+ $item['timestamp'] = $this->extractAnimeTimestamp($anime);
+ $item['content'] = $this->buildAnimeContent($anime);
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/Shimmie2Bridge.php b/bridges/Shimmie2Bridge.php
new file mode 100644
index 0000000..bdbc504
--- /dev/null
+++ b/bridges/Shimmie2Bridge.php
@@ -0,0 +1,39 @@
+<?php
+require_once('DanbooruBridge.php');
+
+class Shimmie2Bridge extends DanbooruBridge {
+
+ const NAME = 'Shimmie v2';
+ const URI = 'http://shimmie.shishnet.org/v2/';
+ const DESCRIPTION = 'Returns images from given page';
+
+ const PATHTODATA = '.shm-thumb-link';
+ const IDATTRIBUTE = 'data-post-id';
+
+ protected function getFullURI(){
+ return $this->getURI()
+ . 'post/list/'
+ . $this->getInput('t')
+ . '/'
+ . $this->getInput('p');
+ }
+
+ protected function getItemFromElement($element){
+ $item = array();
+ $item['uri'] = $this->getURI() . $element->href;
+ $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
+ $item['timestamp'] = time();
+ $thumbnailUri = $this->getURI() . $element->find('img', 0)->src;
+ $item['tags'] = $element->getAttribute('data-tags');
+ $item['title'] = $this->getName() . ' | ' . $item['id'];
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $thumbnailUri
+ . '" /></a><br>Tags: '
+ . $item['tags'];
+
+ return $item;
+ }
+
+}
diff --git a/bridges/SoundcloudBridge.php b/bridges/SoundcloudBridge.php
new file mode 100644
index 0000000..bfd97cb
--- /dev/null
+++ b/bridges/SoundcloudBridge.php
@@ -0,0 +1,64 @@
+<?php
+class SoundCloudBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'kranack';
+ const NAME = 'Soundcloud Bridge';
+ const URI = 'https://soundcloud.com/';
+ const CACHE_TIMEOUT = 600; // 10min
+ const DESCRIPTION = 'Returns 10 newest music from user profile';
+
+ const PARAMETERS = array( array(
+ 'u' => array(
+ 'name' => 'username',
+ 'required' => true
+ )
+ ));
+
+ const CLIENT_ID = '4jkoEFmZEDaqjwJ9Eih4ATNhcH3vMVfp';
+
+ public function collectData(){
+
+ $res = json_decode(getContents(
+ 'https://api.soundcloud.com/resolve?url=http://www.soundcloud.com/'
+ . urlencode($this->getInput('u'))
+ . '&client_id='
+ . self::CLIENT_ID
+ )) or returnServerError('No results for this query');
+
+ $tracks = json_decode(getContents(
+ 'https://api.soundcloud.com/users/'
+ . urlencode($res->id)
+ . '/tracks?client_id='
+ . self::CLIENT_ID
+ )) or returnServerError('No results for this user');
+
+ for($i = 0; $i < 10; $i++) {
+ $item = array();
+ $item['author'] = $tracks[$i]->user->username . ' - ' . $tracks[$i]->title;
+ $item['title'] = $tracks[$i]->user->username . ' - ' . $tracks[$i]->title;
+ $item['content'] = '<audio src="'
+ . $tracks[$i]->uri
+ . '/stream?client_id='
+ . self::CLIENT_ID
+ . '">';
+
+ $item['id'] = self::URI
+ . urlencode($this->getInput('u'))
+ . '/'
+ . urlencode($tracks[$i]->permalink);
+ $item['uri'] = self::URI
+ . urlencode($this->getInput('u'))
+ . '/'
+ . urlencode($tracks[$i]->permalink);
+ $this->items[] = $item;
+ }
+
+ }
+ public function getName(){
+ if(!is_null($this->getInput('u'))) {
+ return self::NAME . ' - ' . $this->getInput('u');
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/SteamBridge.php b/bridges/SteamBridge.php
new file mode 100644
index 0000000..8ff456d
--- /dev/null
+++ b/bridges/SteamBridge.php
@@ -0,0 +1,157 @@
+<?php
+class SteamBridge extends BridgeAbstract {
+
+ const NAME = 'Steam Bridge';
+ const URI = 'https://store.steampowered.com/';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Returns apps list';
+ const MAINTAINER = 'jacknumber';
+ const PARAMETERS = array(
+ 'Wishlist' => array(
+ 'username' => array(
+ 'name' => 'Username',
+ 'required' => true,
+ ),
+ 'currency' => array(
+ 'name' => 'Currency',
+ 'type' => 'list',
+ 'values' => array(
+ // source: http://steam.steamlytics.xyz/currencies
+ 'USD' => 'us',
+ 'GBP' => 'gb',
+ 'EUR' => 'fr',
+ 'CHF' => 'ch',
+ 'RUB' => 'ru',
+ 'BRL' => 'br',
+ 'JPY' => 'jp',
+ 'SEK' => 'se',
+ 'IDR' => 'id',
+ 'MYR' => 'my',
+ 'PHP' => 'ph',
+ 'SGD' => 'sg',
+ 'THB' => 'th',
+ 'KRW' => 'kr',
+ 'TRY' => 'tr',
+ 'MXN' => 'mx',
+ 'CAD' => 'ca',
+ 'NZD' => 'nz',
+ 'CNY' => 'cn',
+ 'INR' => 'in',
+ 'CLP' => 'cl',
+ 'PEN' => 'pe',
+ 'COP' => 'co',
+ 'ZAR' => 'za',
+ 'HKD' => 'hk',
+ 'TWD' => 'tw',
+ 'SRD' => 'sr',
+ 'AED' => 'ae',
+ ),
+ ),
+ 'only_discount' => array(
+ 'name' => 'Only discount',
+ 'type' => 'checkbox',
+ )
+ )
+ );
+
+ public function collectData(){
+
+ $username = $this->getInput('username');
+ $params = array(
+ 'cc' => $this->getInput('currency')
+ );
+
+ $url = self::URI . 'wishlist/id/' . $username . '?' . http_build_query($params);
+
+ $targetVariable = 'g_rgAppInfo';
+ $sort = array();
+
+ $html = '';
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError("Could not request Steam Wishlist. Tried:\n - $url");
+
+ $jsContent = $html->find('.responsive_page_template_content script', 0)->innertext;
+
+ if(preg_match('/var ' . $targetVariable . ' = (.*?);/s', $jsContent, $matches)) {
+ $appsData = json_decode($matches[1]);
+ } else {
+ returnServerError("Could not parse JS variable ($targetVariable) in page content.");
+ }
+
+ foreach($appsData as $id => $element) {
+
+ $appType = $element->type;
+ $appIsBuyable = 0;
+ $appHasDiscount = 0;
+ $appIsFree = 0;
+
+ if($element->subs) {
+ $appIsBuyable = 1;
+
+ if($element->subs[0]->discount_pct) {
+
+ $appHasDiscount = 1;
+ $discountBlock = str_get_html($element->subs[0]->discount_block);
+ $appDiscountValue = $discountBlock->find('.discount_pct', 0)->plaintext;
+ $appOldPrice = $discountBlock->find('.discount_original_price', 0)->plaintext;
+ $appNewPrice = $discountBlock->find('.discount_final_price', 0)->plaintext;
+ $appPrice = $appNewPrice;
+
+ } else {
+
+ if($this->getInput('only_discount')) {
+ continue;
+ }
+
+ $appPrice = $element->subs[0]->price / 100;
+ }
+
+ } else {
+
+ if($this->getInput('only_discount')) {
+ continue;
+ }
+
+ if(isset($element->free) && $element->free = 1) {
+ $appIsFree = 1;
+ }
+ }
+
+ $item = array();
+ $item['uri'] = "http://store.steampowered.com/app/$id/";
+ $item['title'] = $element->name;
+ $item['type'] = $appType;
+ $item['cover'] = str_replace('_292x136', '', $element->capsule);
+ $item['timestamp'] = $element->added;
+ $item['isBuyable'] = $appIsBuyable;
+ $item['hasDiscount'] = $appHasDiscount;
+ $item['isFree'] = $appIsFree;
+ $item['priority'] = $element->priority;
+
+ if($appIsBuyable) {
+ $item['price'] = floatval(str_replace(',', '.', $appPrice));
+ }
+
+ if($appHasDiscount) {
+
+ $item['discount']['value'] = $appDiscountValue;
+ $item['discount']['oldPrice'] = floatval(str_replace(',', '.', $appOldPrice));
+ $item['discount']['newPrice'] = floatval(str_replace(',', '.', $appNewPrice));
+
+ }
+
+ $item['enclosures'] = array();
+ $item['enclosures'][] = str_replace('_292x136', '', $element->capsule);
+
+ foreach($element->screenshots as $screenshot) {
+ $item['enclosures'][] = substr($element->capsule, 0, -31) . $screenshot;
+ }
+
+ $sort[$id] = $element->priority;
+
+ $this->items[] = $item;
+ }
+
+ array_multisort($sort, SORT_ASC, $this->items);
+ }
+}
diff --git a/bridges/StripeAPIChangeLogBridge.php b/bridges/StripeAPIChangeLogBridge.php
new file mode 100644
index 0000000..22ef381
--- /dev/null
+++ b/bridges/StripeAPIChangeLogBridge.php
@@ -0,0 +1,23 @@
+<?php
+class StripeAPIChangeLogBridge extends BridgeAbstract {
+ const MAINTAINER = 'Pierre Mazière';
+ const NAME = 'Stripe API Changelog';
+ const URI = 'https://stripe.com/docs/upgrades';
+ const CACHE_TIMEOUT = 86400; // 24h
+ const DESCRIPTION = 'Returns the changes made to the stripe.com API';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('No results for Stripe API Changelog');
+
+ foreach($html->find('h3') as $change) {
+ $item = array();
+ $item['title'] = trim($change->plaintext);
+ $item['uri'] = self::URI . '#' . $item['title'];
+ $item['author'] = 'stripe';
+ $item['content'] = $change->nextSibling()->outertext;
+ $item['timestamp'] = strtotime($item['title']);
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/SupInfoBridge.php b/bridges/SupInfoBridge.php
new file mode 100644
index 0000000..40160e4
--- /dev/null
+++ b/bridges/SupInfoBridge.php
@@ -0,0 +1,57 @@
+<?php
+class SupInfoBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'SupInfoBridge';
+ const URI = 'https://www.supinfo.com';
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ const PARAMETERS = array(array(
+ 'tag' => array(
+ 'name' => 'Category (not mandatory)',
+ 'type' => 'text',
+ )
+ ));
+
+ public function collectData() {
+
+ if(empty($this->getInput('tag'))) {
+ $html = getSimpleHTMLDOM(self::URI . '/articles/')
+ or returnServerError('Unable to fetch articles !');
+ } else {
+ $html = getSimpleHTMLDOM(self::URI . '/articles/tag/' . $this->getInput('tag'))
+ or returnServerError('Unable to fetch articles !');
+ }
+ $content = $html->find('#latest', 0)->find('ul[class=courseContent]', 0);
+
+ for($i = 0; $i < 5; $i++) {
+
+ $this->items[] = $this->fetchArticle($content->find('h4', $i)->find('a', 0)->href);
+
+ }
+ }
+
+ public function fetchArticle($link) {
+
+ $articleHTML = getSimpleHTMLDOM(self::URI . $link)
+ or returnServerError('Unable to fetch article !');
+
+ $article = $articleHTML->find('div[id=courseDocZero]', 0);
+ $item = array();
+ $item['author'] = $article->find('#courseMetas', 0)->find('a', 0)->plaintext;
+ $item['id'] = $link;
+ $item['uri'] = self::URI . $link;
+ $item['title'] = $article->find('h1', 0)->plaintext;
+ $date = explode(' ', $article->find('#courseMetas', 0)->find('span', 1)->plaintext);
+ $item['timestamp'] = DateTime::createFromFormat('d/m/Y H:i:s', $date[2] . ' ' . $date[4])->getTimestamp();
+
+ $article->find('div[id=courseHeader]', 0)->innertext = '';
+ $article->find('div[id=author-infos]', 0)->innertext = '';
+ $article->find('div[id=cartouche-tete]', 0)->innertext = '';
+ $item['content'] = $article;
+
+ return $item;
+
+ }
+
+}
diff --git a/bridges/SuperSmashBlogBridge.php b/bridges/SuperSmashBlogBridge.php
new file mode 100644
index 0000000..9216ef6
--- /dev/null
+++ b/bridges/SuperSmashBlogBridge.php
@@ -0,0 +1,45 @@
+<?php
+class SuperSmashBlogBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'corenting';
+ const NAME = 'Super Smash Blog';
+ const URI = 'https://www.smashbros.com/en_US/blog/index.html';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Latest articles from the Super Smash Blog blog';
+
+ public function collectData(){
+ $dlUrl = 'https://www.smashbros.com/data/bs/en_US/json/en_US.json';
+
+ $jsonString = getContents($dlUrl) or returnServerError('Error while downloading the website content');
+ $json = json_decode($jsonString, true);
+
+ foreach($json as $article) {
+
+ // Build content
+ $picture = $article['acf']['image1']['url'];
+ if (strlen($picture) != 0) {
+ $picture = str_get_html('<img src="https://www.smashbros.com/' . substr($picture, 8) . '"/>');
+ } else {
+ $picture = '';
+ }
+
+ $video = $article['acf']['link_url'];
+ if (strlen($video) != 0) {
+ $video = str_get_html('<a href="' . $video .'">Youtube video</a>');
+ } else {
+ $video = '';
+ }
+ $text = str_get_html($article['acf']['editor']);
+ $content = $picture . $video . $text;
+
+ // Build final item
+ $item = array();
+ $item['title'] = $article['title']['rendered'];
+ $item['timestamp'] = strtotime($article['date']);
+ $item['content'] = $content;
+ $item['uri'] = self::URI . '?post=' . $article['id'];
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/SuperbWallpapersBridge.php b/bridges/SuperbWallpapersBridge.php
new file mode 100644
index 0000000..610dd32
--- /dev/null
+++ b/bridges/SuperbWallpapersBridge.php
@@ -0,0 +1,70 @@
+<?php
+class SuperbWallpapersBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'nel50n';
+ const NAME = 'Superb Wallpapers Bridge';
+ const URI = 'http://www.superbwallpapers.com/';
+ const CACHE_TIMEOUT = 43200; // 12h
+ const DESCRIPTION = 'Returns the latests wallpapers from SuperbWallpapers';
+
+ const PARAMETERS = array( array(
+ 'c' => array(
+ 'name' => 'category',
+ 'required' => true
+ ),
+ 'm' => array(
+ 'name' => 'Max number of wallpapers',
+ 'type' => 'number'
+ ),
+ 'r' => array(
+ 'name' => 'resolution',
+ 'exampleValue' => '1920x1200, 1680x1050,…',
+ 'defaultValue' => '1920x1200'
+ )
+ ));
+
+ public function collectData(){
+ $category = $this->getInput('c');
+ $resolution = $this->getInput('r'); // Wide wallpaper default
+
+ $num = 0;
+ $max = $this->getInput('m') ?: 36;
+ $lastpage = 1;
+
+ // Get last page number
+ $link = self::URI . '/' . $category . '/9999.html';
+ $html = getSimpleHTMLDOM($link)
+ or returnServerError('Could not load ' . $link);
+
+ $lastpage = min($html->find('.paging .cpage', 0)->innertext(), ceil($max / 36));
+
+ for($page = 1; $page <= $lastpage; $page++) {
+ $link = self::URI . '/' . $category . '/' . $page . '.html';
+ $html = getSimpleHTMLDOM($link)
+ or returnServerError('No results for this query.');
+
+ foreach($html->find('.wpl .i a') as $element) {
+ $thumbnail = $element->find('img', 0);
+
+ $item = array();
+ $item['uri'] = str_replace('200x125', $this->resolution, $thumbnail->src);
+ $item['timestamp'] = time();
+ $item['title'] = $element->title;
+ $item['content'] = $item['title'] . '<br><a href="' . $item['uri'] . '">' . $thumbnail . '</a>';
+ $this->items[] = $item;
+
+ $num++;
+ if ($num >= $max)
+ break 2;
+ }
+ }
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('c')) && !is_null($this->getInput('r'))) {
+ return self::NAME . '- ' . $this->getInput('c') . ' [' . $this->getInput('r') . ']';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/TagBoardBridge.php b/bridges/TagBoardBridge.php
new file mode 100644
index 0000000..b79847e
--- /dev/null
+++ b/bridges/TagBoardBridge.php
@@ -0,0 +1,49 @@
+<?php
+class TagBoardBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Pitchoule';
+ const NAME = 'TagBoard';
+ const URI = 'http://www.TagBoard.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns most recent results from TagBoard.';
+
+ const PARAMETERS = array( array(
+ 'u' => array(
+ 'name' => 'keyword',
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $link = 'https://post-cache.tagboard.com/search/' . $this->getInput('u');
+
+ $html = getSimpleHTMLDOM($link)
+ or returnServerError('Could not request TagBoard for : ' . $link);
+ $parsed_json = json_decode($html);
+
+ foreach($parsed_json->{'posts'} as $element) {
+ $item = array();
+ $item['uri'] = $element->{'permalink'};
+ $item['title'] = $element->{'text'};
+ $thumbnailUri = $element->{'photos'}[0]->{'m'};
+ if(isset($thumbnailUri)) {
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $thumbnailUri
+ . '" /></a>';
+ } else {
+ $item['content'] = $element->{'html'};
+ }
+ $this->items[] = $item;
+ }
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('u'))) {
+ return 'tagboard - ' . $this->getInput('u');
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/TbibBridge.php b/bridges/TbibBridge.php
new file mode 100644
index 0000000..edb761e
--- /dev/null
+++ b/bridges/TbibBridge.php
@@ -0,0 +1,12 @@
+<?php
+require_once('GelbooruBridge.php');
+
+class TbibBridge extends GelbooruBridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Tbib';
+ const URI = 'http://tbib.org/';
+ const DESCRIPTION = 'Returns images from given page';
+
+ const PIDBYPAGE = 50;
+}
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/TheCodingLoveBridge.php b/bridges/TheCodingLoveBridge.php
new file mode 100644
index 0000000..2a639e3
--- /dev/null
+++ b/bridges/TheCodingLoveBridge.php
@@ -0,0 +1,46 @@
+<?php
+class TheCodingLoveBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'superbaillot.net';
+ const NAME = 'The Coding Love';
+ const URI = 'http://thecodinglove.com/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'The Coding Love';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request The Coding Love.');
+
+ foreach($html->find('div.post') as $element) {
+ $item = array();
+ $temp = $element->find('h3 a', 0);
+
+ $titre = $temp->innertext;
+ $url = $temp->href;
+
+ $temp = $element->find('div.bodytype', 0);
+
+ // retrieve .gif instead of static .jpg
+ $images = $temp->find('p.e img');
+ foreach($images as $image) {
+ $img_src = str_replace('.jpg', '.gif', $image->src);
+ $image->src = $img_src;
+ }
+ $content = $temp->innertext;
+
+ $auteur = $temp->find('i', 0);
+ $pos = strpos($auteur->innertext, 'by');
+
+ if($pos > 0) {
+ $auteur = trim(str_replace('*/', '', substr($auteur->innertext, ($pos + 2))));
+ $item['author'] = $auteur;
+ }
+
+ $item['content'] .= trim($content);
+ $item['uri'] = $url;
+ $item['title'] = trim($titre);
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/TheHackerNewsBridge.php b/bridges/TheHackerNewsBridge.php
new file mode 100644
index 0000000..4106658
--- /dev/null
+++ b/bridges/TheHackerNewsBridge.php
@@ -0,0 +1,80 @@
+<?php
+class TheHackerNewsBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = 'The Hacker News Bridge';
+ const URI = 'https://thehackernews.com/';
+ const DESCRIPTION = 'Cyber Security, Hacking, Technology News.';
+
+ public function collectData(){
+
+ function stripWithDelimiters($string, $start, $end){
+ while(strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ }
+
+ return $string;
+ }
+
+ function stripRecursiveHtmlSection($string, $tag_name, $tag_start){
+ $open_tag = '<' . $tag_name;
+ $close_tag = '</' . $tag_name . '>';
+ $close_tag_length = strlen($close_tag);
+ if(strpos($tag_start, $open_tag) === 0) {
+ while(strpos($string, $tag_start) !== false) {
+ $max_recursion = 100;
+ $section_to_remove = null;
+ $section_start = strpos($string, $tag_start);
+ $search_offset = $section_start;
+ do {
+ $max_recursion--;
+ $section_end = strpos($string, $close_tag, $search_offset);
+ $search_offset = $section_end + $close_tag_length;
+ $section_to_remove = substr(
+ $string,
+ $section_start,
+ $section_end - $section_start + $close_tag_length
+ );
+
+ $open_tag_count = substr_count($section_to_remove, $open_tag);
+ $close_tag_count = substr_count($section_to_remove, $close_tag);
+ } while($open_tag_count > $close_tag_count && $max_recursion > 0);
+ $string = str_replace($section_to_remove, '', $string);
+ }
+ }
+ return $string;
+ }
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request TheHackerNews: ' . $this->getURI());
+ $limit = 0;
+
+ foreach($html->find('article') as $element) {
+ if($limit < 5) {
+
+ $article_url = $element->find('a.entry-title', 0)->href;
+ $article_author = trim($element->find('span.vcard', 0)->plaintext);
+ $article_title = $element->find('a.entry-title', 0)->plaintext;
+ $article_timestamp = strtotime($element->find('span.updated', 0)->plaintext);
+ $article = getSimpleHTMLDOM($article_url)
+ or returnServerError('Could not request TheHackerNews: ' . $article_url);
+
+ $contents = $article->find('div.articlebodyonly', 0)->innertext;
+ $contents = stripRecursiveHtmlSection($contents, 'div', '<div class=\'clear\'');
+ $contents = stripWithDelimiters($contents, '<script', '</script>');
+
+ $item = array();
+ $item['uri'] = $article_url;
+ $item['title'] = $article_title;
+ $item['author'] = $article_author;
+ $item['timestamp'] = $article_timestamp;
+ $item['content'] = trim($contents);
+ $this->items[] = $item;
+ $limit++;
+ }
+ }
+
+ }
+}
diff --git a/bridges/ThePirateBayBridge.php b/bridges/ThePirateBayBridge.php
new file mode 100644
index 0000000..0deaded
--- /dev/null
+++ b/bridges/ThePirateBayBridge.php
@@ -0,0 +1,174 @@
+<?php
+class ThePirateBayBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'The Pirate Bay';
+ const URI = 'https://thepiratebay.org/';
+ const DESCRIPTION = 'Returns results for the keywords. You can put several
+ list of keywords by separating them with a semicolon (e.g. "one show;another
+ show"). Category based search needs the category number as input. User based
+ search takes the Uploader name. Search can be done in a specified category';
+
+ const PARAMETERS = array( array(
+ 'q' => array(
+ 'name' => 'keywords/username/category, separated by semicolons',
+ 'exampleValue' => 'first list;second list;…',
+ 'required' => true
+ ),
+ 'crit' => array(
+ 'type' => 'list',
+ 'name' => 'Search type',
+ 'values' => array(
+ 'search' => 'search',
+ 'category' => 'cat',
+ 'user' => 'usr'
+ )
+ ),
+ 'catCheck' => array(
+ 'type' => 'checkbox',
+ 'name' => 'Specify category for keyword search ?',
+ ),
+ 'cat' => array(
+ 'name' => 'Category number',
+ 'exampleValue' => '100, 200… See TPB for category number'
+ ),
+ 'trusted' => array(
+ 'type' => 'checkbox',
+ 'name' => 'Only get results from Trusted or VIP users ?',
+ ),
+ ));
+
+ public function collectData(){
+
+ function parseDateTimestamp($element){
+ $guessedDate = $element->find('font', 0)->plaintext;
+ $guessedDate = explode('Uploaded ', $guessedDate)[1];
+ $guessedDate = explode(',', $guessedDate)[0];
+
+ if(count(explode(':', $guessedDate)) == 1) {
+ $guessedDate = strptime($guessedDate, '%m-%d&nbsp;%Y');
+ $timestamp = mktime(
+ 0,
+ 0,
+ 0,
+ $guessedDate['tm_mon'] + 1,
+ $guessedDate['tm_mday'],
+ 1900 + $guessedDate['tm_year']
+ );
+ } elseif(explode('&nbsp;', $guessedDate)[0] == 'Today') {
+ $guessedDate = strptime(
+ explode('&nbsp;', $guessedDate)[1], '%H:%M'
+ );
+
+ $timestamp = mktime(
+ $guessedDate['tm_hour'],
+ $guessedDate['tm_min'],
+ 0,
+ date('m'),
+ date('d'),
+ date('Y')
+ );
+ } elseif(explode('&nbsp;', $guessedDate)[0] == 'Y-day') {
+ $guessedDate = strptime(
+ explode('&nbsp;', $guessedDate)[1], '%H:%M'
+ );
+
+ $timestamp = mktime(
+ $guessedDate['tm_hour'],
+ $guessedDate['tm_min'],
+ 0,
+ date('m', time() - 24 * 60 * 60),
+ date('d', time() - 24 * 60 * 60),
+ date('Y', time() - 24 * 60 * 60)
+ );
+ } else {
+ $guessedDate = strptime($guessedDate, '%m-%d&nbsp;%H:%M');
+ $timestamp = mktime(
+ $guessedDate['tm_hour'],
+ $guessedDate['tm_min'],
+ 0,
+ $guessedDate['tm_mon'] + 1,
+ $guessedDate['tm_mday'],
+ date('Y'));
+ }
+ return $timestamp;
+ }
+
+ $catBool = $this->getInput('catCheck');
+ if($catBool) {
+ $catNum = $this->getInput('cat');
+ }
+ $critList = $this->getInput('crit');
+
+ $trustedBool = $this->getInput('trusted');
+ $keywordsList = explode(';', $this->getInput('q'));
+ foreach($keywordsList as $keywords) {
+ switch($critList) {
+ case 'search':
+ if($catBool == false) {
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'search/' .
+ rawurlencode($keywords) .
+ '/0/3/0'
+ ) or returnServerError('Could not request TPB.');
+ } else {
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'search/' .
+ rawurlencode($keywords) .
+ '/0/3/' .
+ rawurlencode($catNum)
+ ) or returnServerError('Could not request TPB.');
+ }
+ break;
+ case 'cat':
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'browse/' .
+ rawurlencode($keywords) .
+ '/0/3/0'
+ ) or returnServerError('Could not request TPB.');
+ break;
+ case 'usr':
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'user/' .
+ rawurlencode($keywords) .
+ '/0/3/0'
+ ) or returnServerError('Could not request TPB.');
+ break;
+ }
+
+ if ($html->find('table#searchResult', 0) == false)
+ returnServerError('No result for query ' . $keywords);
+
+ foreach($html->find('tr') as $element) {
+
+ if(!$trustedBool
+ || !is_null($element->find('img[alt=VIP]', 0))
+ || !is_null($element->find('img[alt=Trusted]', 0))) {
+ $item = array();
+ $item['uri'] = $element->find('a', 3)->href;
+ $item['id'] = self::URI . $element->find('a.detLink', 0)->href;
+ $item['timestamp'] = parseDateTimestamp($element);
+ $item['author'] = $element->find('a.detDesc', 0)->plaintext;
+ $item['title'] = $element->find('a.detLink', 0)->plaintext;
+ $item['seeders'] = (int)$element->find('td', 2)->plaintext;
+ $item['leechers'] = (int)$element->find('td', 3)->plaintext;
+ $item['content'] = $element->find('font', 0)->plaintext
+ . '<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/TheTVDBBridge.php b/bridges/TheTVDBBridge.php
new file mode 100644
index 0000000..63af1ea
--- /dev/null
+++ b/bridges/TheTVDBBridge.php
@@ -0,0 +1,205 @@
+<?php
+
+class TheTVDBBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Astyan';
+ const NAME = 'TheTVDB';
+ const URI = 'http://thetvdb.com/';
+ const APIURI = 'https://api.thetvdb.com/';
+ const CACHE_TIMEOUT = 43200; // 12h
+ const DESCRIPTION = 'Returns latest episodes of a serie with theTVDB api. You can contribute to theTVDB.';
+ const PARAMETERS = array(
+ array(
+ 'serie_id' => array(
+ 'type' => 'number',
+ 'name' => 'ID',
+ 'required' => true,
+ ),
+ 'nb_episode' => array(
+ 'type' => 'number',
+ 'name' => 'Number of episodes',
+ 'defaultValue' => 10,
+ 'required' => true,
+ ),
+ )
+ );
+ const APIACCOUNT = 'RSSBridge';
+ const APIKEY = '76DE1887EA401C9A';
+ const APIUSERKEY = 'B52869AC6005330F';
+
+ private function getApiUri(){
+ return self::APIURI;
+ }
+
+ private function getToken(){
+ //login and get token, don't use curlJob to do less adaptations
+ $login_array = array(
+ 'apikey' => self::APIKEY,
+ 'username' => self::APIACCOUNT,
+ 'userkey' => self::APIUSERKEY
+ );
+
+ $login_json = json_encode($login_array);
+ $ch = curl_init($this->getApiUri() . 'login');
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $login_json);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+ 'Content-Type: application/json',
+ 'Accept: application/json'
+ )
+ );
+
+ curl_setopt($ch, CURLOPT_TIMEOUT, 5);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
+ $result = curl_exec($ch);
+ curl_close($ch);
+ $token_json = (array)json_decode($result);
+ if(isset($token_json['Error'])) {
+ throw new Exception($token_json['Error']);
+ die;
+ }
+ $token = $token_json['token'];
+ return $token;
+ }
+
+ private function curlJob($token, $url){
+ $token_header = 'Authorization: Bearer ' . $token;
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+ 'Accept: application/json',
+ $token_header
+ )
+ );
+ curl_setopt($ch, CURLOPT_TIMEOUT, 5);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
+ $result = curl_exec($ch);
+ curl_close($ch);
+ $result_array = (array)json_decode($result);
+ if(isset($result_array['Error'])) {
+ throw new Exception($result_array['Error']);
+ die;
+ }
+ return $result_array;
+ }
+
+ private function getLatestSeasonNumber($token, $serie_id){
+ // get the last season
+ $url = $this->getApiUri() . 'series/' . $serie_id . '/episodes/summary';
+ $summary = $this->curlJob($token, $url);
+ return max($summary['data']->airedSeasons);
+ }
+
+ private function getSerieName($token, $serie_id){
+ $url = $this->getApiUri() . 'series/' . $serie_id;
+ $serie = $this->curlJob($token, $url);
+ return $serie['data']->seriesName;
+ }
+
+ private function getSeasonEpisodes($token,
+ $serie_id,
+ $season,
+ $seriename,
+ &$episodelist,
+ $nbepisodemin,
+ $page = 1){
+ $url = $this->getApiUri()
+ . 'series/'
+ . $serie_id
+ . '/episodes/query?airedSeason='
+ . $season
+ . '?page='
+ . $page;
+
+ $episodes = $this->curlJob($token, $url);
+ // we don't check the number of page because we assume there is less
+ //than 100 episodes in every season
+ $episodes = (array)$episodes['data'];
+ $episodes = array_slice($episodes, -$nbepisodemin, $nbepisodemin);
+ foreach($episodes as $episode) {
+ $episodedata = array();
+ $episodedata['uri'] = $this->getURI()
+ . '?tab=episode&seriesid='
+ . $serie_id
+ . '&seasonid='
+ . $episode->airedSeasonID
+ . '&id='
+ . $episode->id;
+
+ // check if the absoluteNumber exist
+ if(isset($episode->absoluteNumber)) {
+ $episodedata['title'] = 'S'
+ . $episode->airedSeason
+ . 'E'
+ . $episode->airedEpisodeNumber
+ . '('
+ . $episode->absoluteNumber
+ . ') : '
+ . $episode->episodeName;
+ } else {
+ $episodedata['title'] = 'S'
+ . $episode->airedSeason
+ . 'E'
+ . $episode->airedEpisodeNumber
+ . ' : '
+ . $episode->episodeName;
+ }
+ $episodedata['author'] = $seriename;
+ $date = DateTime::createFromFormat(
+ 'Y-m-d H:i:s',
+ $episode->firstAired . ' 00:00:00'
+ );
+
+ $episodedata['timestamp'] = $date->getTimestamp();
+ $episodedata['content'] = $episode->overview;
+ $episodelist[] = $episodedata;
+ }
+ }
+
+ public function collectData(){
+ $serie_id = $this->getInput('serie_id');
+ $nbepisode = $this->getInput('nb_episode');
+ $episodelist = array();
+ $token = $this->getToken();
+ $maxseason = $this->getLatestSeasonNumber($token, $serie_id);
+ $seriename = $this->getSerieName($token, $serie_id);
+ $season = $maxseason;
+ while(sizeof($episodelist) < $nbepisode && $season >= 1) {
+ $nbepisodetmp = $nbepisode - sizeof($episodelist);
+ $this->getSeasonEpisodes(
+ $token,
+ $serie_id,
+ $season,
+ $seriename,
+ $episodelist,
+ $nbepisodetmp
+ );
+
+ $season = $season - 1;
+ }
+ // add the 10 last specials episodes
+ try { // catch to avoid error if empty
+ $this->getSeasonEpisodes(
+ $token,
+ $serie_id,
+ 0,
+ $seriename,
+ $episodelist,
+ $nbepisode
+ );
+ } catch(Exception $e) {
+ unset($e);
+ }
+ // sort and keep the 10 last episodes, works bad with the netflix serie
+ // (all episode lauch at once)
+ usort(
+ $episodelist,
+ function ($a, $b){
+ return $a['timestamp'] < $b['timestamp'];
+ }
+ );
+ $this->items = array_slice($episodelist, 0, $nbepisode);
+ }
+}
diff --git a/bridges/Torrent9Bridge.php b/bridges/Torrent9Bridge.php
new file mode 100644
index 0000000..40db4ac
--- /dev/null
+++ b/bridges/Torrent9Bridge.php
@@ -0,0 +1,102 @@
+<?php
+class Torrent9Bridge extends BridgeAbstract {
+
+ const MAINTAINER = 'lagaisse';
+ const NAME = 'Torrent9 Bridge';
+ const URI = 'http://www.torrent9.pe';
+ const CACHE_TIMEOUT = 86400; // 24h = 86400s
+ const DESCRIPTION = 'Returns latest torrents';
+
+ const PAGE_SERIES = 'torrents_series';
+ const PAGE_SERIES_VOSTFR = 'torrents_series_vostfr';
+ const PAGE_SERIES_FR = 'torrents_series_french';
+
+ const PARAMETERS = array(
+ 'From search' => array(
+ 'q' => array(
+ 'name' => 'Search',
+ 'required' => true,
+ 'title' => 'Type your search'
+ )
+ ),
+ 'By page' => array(
+ 'page' => array(
+ 'name' => 'Page',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => array(
+ 'Series' => self::PAGE_SERIES,
+ 'Series VOST' => self::PAGE_SERIES_VOSTFR,
+ 'Series FR' => self::PAGE_SERIES_FR,
+ ),
+ 'defaultValue' => self::PAGE_SERIES
+ )
+ )
+ );
+
+ public function collectData(){
+
+ if($this->queriedContext === 'From search') {
+ $request = str_replace(' ', '-', trim($this->getInput('q')));
+ $page = self::URI . '/search_torrent/' . urlencode($request) . '.html';
+ } else {
+ $request = $this->getInput('page');
+ $page = self::URI . '/' . $request . '.html';
+ }
+
+ $html = getSimpleHTMLDOM($page)
+ or returnServerError('No results for this query.');
+
+ foreach($html->find('table', 0)->find('tr') as $episode) {
+ if($episode->parent->tag == 'tbody') {
+
+ $urlepisode = self::URI . $episode->find('a', 0)->getAttribute('href');
+
+ //30 years = forever
+ $htmlepisode = getSimpleHTMLDOMCached($urlepisode, 86400 * 366 * 30);
+
+ $item = array();
+ $item['author'] = $episode->find('a', 0)->text();
+ $item['title'] = $episode->find('a', 0)->text();
+ $item['id'] = $episode->find('a', 0)->getAttribute('href');
+ $item['pubdate'] = $this->getCachedDate($urlepisode);
+
+ $textefiche = $htmlepisode->find('.movie-information', 0)->find('p', 1);
+ if(isset($textefiche)) {
+ $item['content'] = $textefiche->text();
+ } else {
+ $p = $htmlepisode->find('.movie-information', 0)->find('p');
+ if(!empty($p)) {
+ $item['content'] = $htmlepisode->find('.movie-information', 0)->find('p', 0)->text();
+ }
+ }
+
+ $item['id'] = $episode->find('a', 0)->getAttribute('href');
+ $item['uri'] = self::URI . $htmlepisode->find('.download', 0)->getAttribute('href');
+
+ $this->items[] = $item;
+ }
+ }
+ }
+
+
+ public function getName(){
+ if(!is_null($this->getInput('q'))) {
+ return $this->getInput('q') . ' : ' . self::NAME;
+ }
+
+ return parent::getName();
+ }
+
+ private function getCachedDate($url){
+ debugMessage('getting pubdate from url ' . $url . '');
+ // Initialize cache
+ $cache = Cache::create('FileCache');
+ $cache->setPath(CACHE_DIR . '/pages');
+ $params = [$url];
+ $cache->setParameters($params);
+ // Get cachefile timestamp
+ $time = $cache->getTime();
+ return ($time !== false ? $time : time());
+ }
+}
diff --git a/bridges/TwitterBridge.php b/bridges/TwitterBridge.php
new file mode 100644
index 0000000..aedb372
--- /dev/null
+++ b/bridges/TwitterBridge.php
@@ -0,0 +1,327 @@
+<?php
+class TwitterBridge extends BridgeAbstract {
+ const NAME = 'Twitter Bridge';
+ const URI = 'https://twitter.com/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'returns tweets';
+ const MAINTAINER = 'pmaziere';
+ const PARAMETERS = array(
+ 'global' => array(
+ 'nopic' => array(
+ 'name' => 'Hide profile pictures',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to hide profile pictures in content'
+ ),
+ 'noimg' => array(
+ 'name' => 'Hide images in tweets',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to hide images in tweets'
+ )
+ ),
+ 'By keyword or hashtag' => array(
+ 'q' => array(
+ 'name' => 'Keyword or #hashtag',
+ 'required' => true,
+ 'exampleValue' => 'rss-bridge, #rss-bridge',
+ 'title' => 'Insert a keyword or hashtag'
+ )
+ ),
+ 'By username' => array(
+ 'u' => array(
+ 'name' => 'username',
+ 'required' => true,
+ 'exampleValue' => 'sebsauvage',
+ 'title' => 'Insert a user name'
+ ),
+ 'norep' => array(
+ 'name' => 'Without replies',
+ 'type' => 'checkbox',
+ 'title' => 'Only return initial tweets'
+ ),
+ 'noretweet' => array(
+ 'name' => 'Without retweets',
+ 'required' => false,
+ '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'
+ )
+ )
+ );
+
+ public function getName(){
+ switch($this->queriedContext) {
+ case 'By keyword or hashtag':
+ $specific = 'search ';
+ $param = 'q';
+ break;
+ case 'By username':
+ $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);
+ }
+
+ public function getURI(){
+ switch($this->queriedContext) {
+ case 'By keyword or hashtag':
+ return self::URI
+ . 'search?q='
+ . urlencode($this->getInput('q'))
+ . '&f=tweets';
+ case 'By username':
+ return self::URI
+ . 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();
+ }
+ }
+
+ public function collectData(){
+ $html = '';
+
+ $html = getSimpleHTMLDOM($this->getURI());
+ if(!$html) {
+ switch($this->queriedContext) {
+ case 'By keyword or hashtag':
+ 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');
+ }
+ }
+
+ $hidePictures = $this->getInput('nopic');
+
+ foreach($html->find('div.js-stream-tweet') as $tweet) {
+
+ // Skip retweets?
+ if($this->getInput('noretweet')
+ && $tweet->getAttribute('data-screen-name') !== $this->getInput('u')) {
+ continue;
+ }
+
+ // remove 'invisible' content
+ foreach($tweet->find('.invisible') as $invisible) {
+ $invisible->outertext = '';
+ }
+
+ // Skip protmoted tweets
+ $heading = $tweet->previousSibling();
+ if(!is_null($heading) &&
+ $heading->getAttribute('class') === 'promoted-tweet-heading'
+ ) {
+ continue;
+ }
+
+ $item = array();
+ // extract username and sanitize
+ $item['username'] = $tweet->getAttribute('data-screen-name');
+ // extract fullname (pseudonym)
+ $item['fullname'] = $tweet->getAttribute('data-name');
+ // get author
+ $item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
+ // get avatar link
+ $item['avatar'] = $tweet->find('img', 0)->src;
+ // get TweetID
+ $item['id'] = $tweet->getAttribute('data-tweet-id');
+ // get tweet link
+ $item['uri'] = self::URI . substr($tweet->find('a.js-permalink', 0)->getAttribute('href'), 1);
+ // extract tweet timestamp
+ $item['timestamp'] = $tweet->find('span.js-short-timestamp', 0)->getAttribute('data-time');
+ // 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);
+
+ // get tweet text
+ $cleanedTweet = str_replace(
+ 'href="/',
+ 'href="' . self::URI,
+ $tweet->find('p.js-tweet-text', 0)->innertext
+ );
+
+ // fix anchors missing spaces in-between
+ $cleanedTweet = $this->fixAnchorSpacing($cleanedTweet);
+
+ // Add picture to content
+ $picture_html = '';
+ if(!$hidePictures) {
+ $picture_html = <<<EOD
+<a href="https://twitter.com/{$item['username']}">
+<img
+ style="align:top; width:75px; border:1px solid black;"
+ alt="{$item['username']}"
+ src="{$item['avatar']}"
+ title="{$item['fullname']}" />
+</a>
+EOD;
+ }
+
+ // Add embeded image to content
+ $image_html = '';
+ $image = $this->getImageURI($tweet);
+ if(!$this->getInput('noimg') && !is_null($image)) {
+ // add enclosures
+ $item['enclosures'] = array($image . ':orig');
+
+ $image_html = <<<EOD
+<a href="{$image}:orig">
+<img
+ style="align:top; max-width:558px; border:1px solid black;"
+ src="{$image}:thumb" />
+</a>
+EOD;
+ }
+
+ // add content
+ $item['content'] = <<<EOD
+<div style="display: inline-block; vertical-align: top;">
+ {$picture_html}
+</div>
+<div style="display: inline-block; vertical-align: top;">
+ <blockquote>{$cleanedTweet}</blockquote>
+</div>
+<div style="display: block; vertical-align: top;">
+ <blockquote>{$image_html}</blockquote>
+</div>
+EOD;
+
+ // add quoted tweet
+ $quotedTweet = $tweet->find('div.QuoteTweet', 0);
+ if($quotedTweet) {
+ // get tweet text
+ $cleanedQuotedTweet = str_replace(
+ 'href="/',
+ 'href="' . self::URI,
+ $quotedTweet->find('div.tweet-text', 0)->innertext
+ );
+
+ $this->processContentLinks($quotedTweet);
+ $this->processEmojis($quotedTweet);
+
+ // Add embeded image to content
+ $quotedImage_html = '';
+ $quotedImage = $this->getQuotedImageURI($tweet);
+ if(!$this->getInput('noimg') && !is_null($quotedImage)) {
+ // add enclosures
+ $item['enclosures'] = array($quotedImage . ':orig');
+
+ $quotedImage_html = <<<EOD
+<a href="{$quotedImage}:orig">
+<img
+ style="align:top; max-width:558px; border:1px solid black;"
+ src="{$quotedImage}:thumb" />
+</a>
+EOD;
+ }
+
+ $item['content'] = <<<EOD
+<div style="display: inline-block; vertical-align: top;">
+ <blockquote>{$cleanedQuotedTweet}</blockquote>
+</div>
+<div style="display: block; vertical-align: top;">
+ <blockquote>{$quotedImage_html}</blockquote>
+</div>
+<hr>
+{$item['content']}
+EOD;
+ }
+
+ // put out
+ $this->items[] = $item;
+ }
+ }
+
+ private function processEmojis($tweet){
+ // process emojis (reduce size)
+ foreach($tweet->find('img.Emoji') as $img) {
+ $img->style .= ' height: 1em;';
+ }
+ }
+
+ private function processContentLinks($tweet){
+ // processing content links
+ foreach($tweet->find('a') as $link) {
+ if($link->hasAttribute('data-expanded-url')) {
+ $link->href = $link->getAttribute('data-expanded-url');
+ }
+ $link->removeAttribute('data-expanded-url');
+ $link->removeAttribute('data-query-source');
+ $link->removeAttribute('rel');
+ $link->removeAttribute('class');
+ $link->removeAttribute('target');
+ $link->removeAttribute('title');
+ }
+ }
+
+ private function fixAnchorSpacing($content){
+ // fix anchors missing spaces in-between
+ return str_replace(
+ '<a',
+ ' <a',
+ $content
+ );
+ }
+
+ private function getImageURI($tweet){
+ // Find media in tweet
+ $container = $tweet->find('div.AdaptiveMedia-container', 0);
+ if($container && $container->find('img', 0)) {
+ return $container->find('img', 0)->src;
+ }
+
+ return null;
+ }
+
+ private function getQuotedImageURI($tweet){
+ // Find media in tweet
+ $container = $tweet->find('div.QuoteMedia-container', 0);
+ if($container && $container->find('img', 0)) {
+ return $container->find('img', 0)->src;
+ }
+
+ return null;
+ }
+}
diff --git a/bridges/UnsplashBridge.php b/bridges/UnsplashBridge.php
new file mode 100644
index 0000000..ee1040a
--- /dev/null
+++ b/bridges/UnsplashBridge.php
@@ -0,0 +1,77 @@
+<?php
+class UnsplashBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'nel50n';
+ const NAME = 'Unsplash Bridge';
+ const URI = 'http://unsplash.com/';
+ const CACHE_TIMEOUT = 43200; // 12h
+ const DESCRIPTION = 'Returns the latests photos from Unsplash';
+
+ const PARAMETERS = array( array(
+ 'm' => array(
+ 'name' => 'Max number of photos',
+ 'type' => 'number',
+ 'defaultValue' => 20
+ ),
+ 'w' => array(
+ 'name' => 'Width',
+ 'exampleValue' => '1920, 1680, …',
+ 'defaultValue' => '1920'
+ ),
+ 'q' => array(
+ 'name' => 'JPEG quality',
+ 'type' => 'number',
+ 'defaultValue' => 75
+ )
+ ));
+
+ public function collectData(){
+ $width = $this->getInput('w');
+ $num = 0;
+ $max = $this->getInput('m');
+ $quality = $this->getInput('q');
+ $lastpage = 1;
+
+ for($page = 1; $page <= $lastpage; $page++) {
+ $link = self::URI . '/grid?page=' . $page;
+ $html = getSimpleHTMLDOM($link)
+ or returnServerError('No results for this query.');
+
+ if($page === 1) {
+ preg_match(
+ '/=(\d+)$/',
+ $html->find('.pagination > a[!class]', -1)->href,
+ $matches
+ );
+
+ $lastpage = min($matches[1], ceil($max / 40));
+ }
+
+ foreach($html->find('.photo') as $element) {
+ $thumbnail = $element->find('img', 0);
+ $thumbnail->src = str_replace('https://', 'http://', $thumbnail->src);
+
+ $item = array();
+ $item['uri'] = str_replace(
+ array('q=75', 'w=400'),
+ array("q=$quality", "w=$width"),
+ $thumbnail->src).'.jpg'; // '.jpg' only for format hint
+
+ $item['timestamp'] = time();
+ $item['title'] = $thumbnail->alt;
+ $item['content'] = $item['title']
+ . '<br><a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $thumbnail->src
+ . '" /></a>';
+
+ $this->items[] = $item;
+
+ $num++;
+ if ($num >= $max)
+ break 2;
+ }
+ }
+ }
+}
diff --git a/bridges/UsbekEtRicaBridge.php b/bridges/UsbekEtRicaBridge.php
new file mode 100644
index 0000000..4d5ba16
--- /dev/null
+++ b/bridges/UsbekEtRicaBridge.php
@@ -0,0 +1,110 @@
+<?php
+class UsbekEtRicaBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Usbek & Rica Bridge';
+ const URI = 'https://usbeketrica.com';
+ const DESCRIPTION = 'Returns latest articles from the front page';
+
+ const PARAMETERS = array(
+ array(
+ 'limit' => array(
+ 'name' => 'Number of articles to return',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Specifies the maximum number of articles to return',
+ 'defaultValue' => -1
+ ),
+ 'fullarticle' => array(
+ 'name' => 'Load full article',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'title' => 'Activate to load full articles',
+ )
+ )
+ );
+
+ public function collectData(){
+ $limit = $this->getInput('limit');
+ $fullarticle = $this->getInput('fullarticle');
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ $articles = $html->find('div.details');
+
+ foreach($articles as $article) {
+ $item = array();
+
+ $title = $article->find('div.card-title', 0);
+ if($title) {
+ $item['title'] = $title->plaintext;
+ } else {
+ // Sometimes we get rubbish, ignore.
+ continue;
+ }
+
+ $author = $article->find('div.author span', 0);
+ if($author) {
+ $item['author'] = $author->plaintext;
+ }
+
+ $uri = $article->find('a.read', 0)->href;
+ if(substr($uri, 0, 1) === 'h') { // absolute uri
+ $item['uri'] = $uri;
+ } else { // relative uri
+ $item['uri'] = $this->getURI() . $uri;
+ }
+
+ if($fullarticle) {
+ $content = $this->loadFullArticle($item['uri']);
+ }
+
+ if($fullarticle && !is_null($content)) {
+ $item['content'] = $content;
+ } else {
+ $excerpt = $article->find('div.card-excerpt', 0);
+ if($excerpt) {
+ $item['content'] = $excerpt->plaintext;
+ }
+ }
+
+ $image = $article->find('div.card-img img', 0);
+ if($image) {
+ $item['enclosures'] = array(
+ $image->src
+ );
+ }
+
+ $this->items[] = $item;
+
+ if($limit > 0 && count($this->items) >= $limit) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Loads the full article and returns the contents
+ * @param $uri The article URI
+ * @return The article content
+ */
+ private function loadFullArticle($uri){
+ $html = getSimpleHTMLDOMCached($uri);
+
+ $content = $html->find('section.main', 0);
+ if($content) {
+ return $this->replaceUriInHtmlElement($content);
+ }
+
+ return null;
+ }
+
+ /**
+ * Replaces all relative URIs with absolute ones
+ * @param $element A simplehtmldom element
+ * @return The $element->innertext with all URIs replaced
+ */
+ private function replaceUriInHtmlElement($element){
+ return str_replace('href="/', 'href="' . $this->getURI() . '/', $element->innertext);
+ }
+
+}
diff --git a/bridges/ViadeoCompanyBridge.php b/bridges/ViadeoCompanyBridge.php
new file mode 100644
index 0000000..3f76188
--- /dev/null
+++ b/bridges/ViadeoCompanyBridge.php
@@ -0,0 +1,37 @@
+<?php
+class ViadeoCompanyBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'regisenguehard';
+ const NAME = 'Viadeo Company';
+ const URI = 'https://www.viadeo.com/';
+ const CACHE_TIMEOUT = 21600; // 6h
+ const DESCRIPTION = 'Returns most recent actus from Company on Viadeo.
+ (http://www.viadeo.com/fr/company/<strong style="font-weight:bold;">apple</strong>)';
+
+ const PARAMETERS = array( array(
+ 'c' => array(
+ 'name' => 'Company name',
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $html = '';
+ $link = self::URI . 'fr/company/' . $this->getInput('c');
+
+ $html = getSimpleHTMLDOM($link)
+ or returnServerError('Could not request Viadeo.');
+
+ foreach($html->find('//*[@id="company-newsfeed"]/ul/li') as $element) {
+ $title = $element->find('p', 0)->innertext;
+ if($title) {
+ $item = array();
+ $item['uri'] = $link;
+ $item['title'] = mb_substr($element->find('p', 0)->innertext, 0, 100);
+ $item['content'] = $element->find('p', 0)->innertext;;
+ $this->items[] = $item;
+ $i++;
+ }
+ }
+ }
+}
diff --git a/bridges/VkBridge.php b/bridges/VkBridge.php
new file mode 100644
index 0000000..70c0db4
--- /dev/null
+++ b/bridges/VkBridge.php
@@ -0,0 +1,339 @@
+<?php
+
+class VkBridge extends BridgeAbstract
+{
+
+ const MAINTAINER = 'ahiles3005';
+ const NAME = 'VK.com';
+ const URI = 'https://vk.com/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Working with open pages';
+ const PARAMETERS = array(
+ array(
+ 'u' => array(
+ 'name' => 'Group or user name',
+ 'required' => true
+ )
+ )
+ );
+
+ protected $pageName;
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('u'))) {
+ return static::URI . urlencode($this->getInput('u'));
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ if ($this->pageName) {
+ return $this->pageName;
+ }
+
+ return parent::getName();
+ }
+
+ 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);
+ // makes album link generating work correctly
+ $text_html = str_replace('"class="page_album_link">', '" class="page_album_link">', $text_html);
+ $html = str_get_html($text_html);
+ $pageName = $html->find('.page_name', 0);
+ if (is_object($pageName)) {
+ $pageName = $pageName->plaintext;
+ $this->pageName = htmlspecialchars_decode($pageName);
+ }
+ $pinned_post_item = null;
+ $last_post_id = 0;
+
+ foreach ($html->find('.post') as $post) {
+
+ $is_pinned_post = false;
+ if (strpos($post->getAttribute('class'), 'post_fixed') !== false) {
+ $is_pinned_post = true;
+ }
+
+ if (is_object($post->find('a.wall_post_more', 0))) {
+ //delete link "show full" in content
+ $post->find('a.wall_post_more', 0)->outertext = '';
+ }
+
+ $content_suffix = '';
+
+ // looking for external links
+ $external_link_selectors = array(
+ 'a.page_media_link_title',
+ 'div.page_media_link_title > a',
+ 'div.media_desc > a.lnk',
+ );
+
+ foreach($external_link_selectors as $sel) {
+ if (is_object($post->find($sel, 0))) {
+ $a = $post->find($sel, 0);
+ $innertext = $a->innertext;
+ $parsed_url = parse_url($a->getAttribute('href'));
+ if (strpos($parsed_url['path'], '/away.php') !== 0) continue;
+ parse_str($parsed_url['query'], $parsed_query);
+ $content_suffix .= "<br>External link: <a href='" . $parsed_query['to'] . "'>$innertext</a>";
+ }
+ }
+
+ // remove external link from content
+ $external_link_selectors_to_remove = array(
+ 'div.page_media_thumbed_link',
+ 'div.page_media_link_desc_wrap',
+ 'div.media_desc > a.lnk',
+ );
+
+ foreach($external_link_selectors_to_remove as $sel) {
+ if (is_object($post->find($sel, 0))) {
+ $post->find($sel, 0)->outertext = '';
+ }
+ }
+
+ // looking for article
+ $article = $post->find('a.article_snippet', 0);
+ if (is_object($article)) {
+ if (strpos($article->getAttribute('class'), 'article_snippet_mini') !== false) {
+ $article_title_selector = 'div.article_snippet_mini_title';
+ $article_author_selector = 'div.article_snippet_mini_info > .mem_link,
+ div.article_snippet_mini_info > .group_link';
+ $article_thumb_selector = 'div.article_snippet_mini_thumb';
+ } else {
+ $article_title_selector = 'div.article_snippet__title';
+ $article_author_selector = 'div.article_snippet__author';
+ $article_thumb_selector = 'div.article_snippet__image';
+ }
+ $article_title = $article->find($article_title_selector, 0)->innertext;
+ $article_author = $article->find($article_author_selector, 0)->innertext;
+ $article_link = self::URI . ltrim($article->getAttribute('href'), '/');
+ $article_img_element_style = $article->find($article_thumb_selector, 0)->getAttribute('style');
+ preg_match('/background-image: url\((.*)\)/', $article_img_element_style, $matches);
+ if (count($matches) > 0) {
+ $content_suffix .= "<br><img src='" . $matches[1] . "'>";
+ }
+ $content_suffix .= "<br>Article: <a href='$article_link'>$article_title ($article_author)</a>";
+ $article->outertext = '';
+ }
+
+ // get video on post
+ $video = $post->find('div.post_video_desc', 0);
+ if (is_object($video)) {
+ $video_title = $video->find('div.post_video_title', 0)->plaintext;
+ $video_link = self::URI . ltrim( $video->find('a.lnk', 0)->getAttribute('href'), '/' );
+ $content_suffix .= "<br>Video: <a href='$video_link'>$video_title</a>";
+ $video->outertext = '';
+ }
+
+ // get all other videos
+ foreach($post->find('a.page_post_thumb_video') as $a) {
+ $video_title = $a->getAttribute('aria-label');
+ $temp = explode(' ', $video_title, 2);
+ if (count($temp) > 1) $video_title = $temp[1];
+ $video_link = self::URI . ltrim( $a->getAttribute('href'), '/' );
+ $content_suffix .= "<br>Video: <a href='$video_link'>$video_title</a>";
+ $a->outertext = '';
+ }
+
+ // get all photos
+ foreach($post->find('div.wall_text > a.page_post_thumb_wrap') as $a) {
+ $result = $this->getPhoto($a);
+ if ($result == null) continue;
+ $a->outertext = '';
+ $content_suffix .= "<br>$result";
+ }
+
+ // get albums
+ foreach($post->find('.page_album_wrap') as $el) {
+ $a = $el->find('.page_album_link', 0);
+ $album_title = $a->find('.page_album_title_text', 0)->getAttribute('title');
+ $album_link = self::URI . ltrim($a->getAttribute('href'), '/');
+ $el->outertext = '';
+ $content_suffix .= "<br>Album: <a href='$album_link'>$album_title</a>";
+ }
+
+ // get photo documents
+ foreach($post->find('a.page_doc_photo_href') as $a) {
+ $doc_link = self::URI . ltrim($a->getAttribute('href'), '/');
+ $doc_gif_label_element = $a->find('.page_gif_label', 0);
+ $doc_title_element = $a->find('.doc_label', 0);
+
+ if (is_object($doc_gif_label_element)) {
+ $gif_preview_img = backgroundToImg($a->find('.page_doc_photo', 0));
+ $content_suffix .= "<br>Gif: <a href='$doc_link'>$gif_preview_img</a>";
+
+ } else if (is_object($doc_title_element)) {
+ $doc_title = $doc_title_element->innertext;
+ $content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>";
+
+ } else {
+ continue;
+
+ }
+
+ $a->outertext = '';
+ }
+
+ // get other documents
+ foreach($post->find('div.page_doc_row') as $div) {
+ $doc_title_element = $div->find('a.page_doc_title', 0);
+
+ if (is_object($doc_title_element)) {
+ $doc_title = $doc_title_element->innertext;
+ $doc_link = self::URI . ltrim($doc_title_element->getAttribute('href'), '/');
+ $content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>";
+
+ } else {
+ continue;
+
+ }
+
+ $div->outertext = '';
+ }
+
+ // get polls
+ foreach($post->find('div.page_media_poll_wrap') as $div) {
+ $poll_title = $div->find('.page_media_poll_title', 0)->innertext;
+ $content_suffix .= "<br>Poll: $poll_title";
+ foreach($div->find('div.page_poll_text') as $poll_stat_title) {
+ $content_suffix .= '<br>- ' . $poll_stat_title->innertext;
+ }
+ $div->outertext = '';
+ }
+
+ // get sign
+ $post_author = $pageName;
+ foreach($post->find('a.wall_signed_by') as $a) {
+ $post_author = $a->innertext;
+ $a->outertext = '';
+ }
+
+ if (is_object($post->find('div.copy_quote', 0))) {
+ $copy_quote = $post->find('div.copy_quote', 0);
+ if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) {
+ $copy_post_header->outertext = '';
+ }
+ $copy_quote_content = $copy_quote->innertext;
+ $copy_quote->outertext = "<br>Reposted: <br>$copy_quote_content";
+ }
+
+ $item = array();
+ $item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<br><img>');
+ $item['content'] .= $content_suffix;
+
+ // get post link
+ $post_link = $post->find('a.post_link', 0)->getAttribute('href');
+ preg_match('/wall-?\d+_(\d+)/', $post_link, $preg_match_result);
+ $item['post_id'] = intval($preg_match_result[1]);
+ if (substr(self::URI, -1) == '/') {
+ $post_link = self::URI . ltrim($post_link, '/');
+ } else {
+ $post_link = self::URI . $post_link;
+ }
+ $item['uri'] = $post_link;
+ $item['timestamp'] = $this->getTime($post);
+ $item['title'] = $this->getTitle($item['content']);
+ $item['author'] = $post_author;
+ if ($is_pinned_post) {
+ // do not append it now
+ $pinned_post_item = $item;
+ } else {
+ $last_post_id = $item['post_id'];
+ $this->items[] = $item;
+ }
+
+ }
+
+ if (is_null($pinned_post_item)) {
+ return;
+ } else if (count($this->items) == 0) {
+ $this->items[] = $pinned_post_item;
+ } else if ($last_post_id < $pinned_post_item['post_id']) {
+ $this->items[] = $pinned_post_item;
+ usort($this->items, function ($item1, $item2) {
+ return $item2['post_id'] - $item1['post_id'];
+ });
+ }
+ }
+
+ private function getPhoto($a) {
+ $onclick = $a->getAttribute('onclick');
+ preg_match('/return showPhoto\(.+?({.*})/', $onclick, $preg_match_result);
+ if (count($preg_match_result) == 0) return;
+
+ $arg = htmlspecialchars_decode( str_replace('queue:1', '"queue":1', $preg_match_result[1]) );
+ $data = json_decode($arg, true);
+ if ($data == null) return;
+
+ $thumb = $data['temp']['base'] . $data['temp']['x_'][0] . '.jpg';
+ $original = '';
+ foreach(array('y_', 'z_', 'w_') as $key) {
+ if (!isset($data['temp'][$key])) continue;
+ if (!isset($data['temp'][$key][0])) continue;
+ if (substr($data['temp'][$key][0], 0, 4) == 'http') {
+ $base = '';
+ } else {
+ $base = $data['temp']['base'];
+ }
+ $original = $base . $data['temp'][$key][0] . '.jpg';
+ }
+
+ if ($original) {
+ return "<a href='$original'><img src='$thumb'></a>";
+ } else {
+ return "<img src='$thumb'>";
+ }
+ }
+
+ private function getTitle($content)
+ {
+ preg_match('/^["\w\ \p{Cyrillic}\(\)\?#«»-]+/mu', htmlspecialchars_decode($content), $result);
+ if (count($result) == 0) return 'untitled';
+ return $result[0];
+ }
+
+ 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');
+
+ $header = array('Accept-language: en', 'Cookie: remixlang=3');
+
+ return getContents($this->getURI(), $header);
+ }
+
+
+}
diff --git a/bridges/WallpaperStopBridge.php b/bridges/WallpaperStopBridge.php
new file mode 100644
index 0000000..3578e71
--- /dev/null
+++ b/bridges/WallpaperStopBridge.php
@@ -0,0 +1,107 @@
+<?php
+class WallpaperStopBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'nel50n';
+ const NAME = 'WallpaperStop Bridge';
+ const URI = 'http://www.wallpaperstop.com';
+ const CACHE_TIMEOUT = 43200; // 12h
+ const DESCRIPTION = 'Returns the latests wallpapers from WallpaperStop';
+
+ const PARAMETERS = array( array(
+ 'c' => array(
+ 'name' => 'Category'
+ ),
+ 's' => array(
+ 'name' => 'subcategory'
+ ),
+ 'm' => array(
+ 'name' => 'Max number of wallpapers',
+ 'type' => 'number',
+ 'defaultValue' => 20
+ ),
+ 'r' => array(
+ 'name' => 'resolution',
+ 'exampleValue' => '1920x1200, 1680x1050,…',
+ 'defaultValue' => '1920x1200'
+ )
+ ));
+
+ public function collectData(){
+ $category = $this->getInput('c');
+ $subcategory = $this->getInput('s');
+ $resolution = $this->getInput('r');
+
+ $num = 0;
+ $max = $this->getInput('m');
+ $lastpage = 1;
+
+ for($page = 1; $page <= $lastpage; $page++) {
+ $link = self::URI
+ . '/'
+ . $category
+ . '-wallpaper/'
+ . (!empty($subcategory) ? $subcategory . '-wallpaper/' : '')
+ . 'desktop-wallpaper-'
+ . $page
+ . '.html';
+
+ $html = getSimpleHTMLDOM($link)
+ or returnServerError('No results for this query.');
+
+ if($page === 1) {
+ preg_match('/-(\d+)\.html$/', $html->find('.pagination > .last', 0)->href, $matches);
+ $lastpage = min($matches[1], ceil($max / 20));
+ }
+
+ foreach($html->find('article.item') as $element) {
+ $wplink = $element->getAttribute('data-permalink');
+ if(preg_match('%^' . self::URI . '/(.+)/([^/]+)-(\d+)\.html$%', $wplink, $matches)) {
+ $thumbnail = $element->find('img', 0);
+
+ $item = array();
+ $item['uri'] = self::URI
+ . '/wallpapers/'
+ . str_replace('wallpaper', 'wallpapers', $matches[1])
+ . '/'
+ . $matches[2]
+ . '-'
+ . $resolution
+ . '-'
+ . $matches[3]
+ . '.jpg';
+
+ $item['id'] = $matches[3];
+ $item['timestamp'] = time();
+ $item['title'] = $thumbnail->title;
+ $item['content'] = $item['title']
+ . '<br><a href="'
+ . $wplink
+ . '"><img src="'
+ . self::URI
+ . $thumbnail->src
+ . '" /></a>';
+
+ $this->items[] = $item;
+
+ $num++;
+ if ($num >= $max)
+ break 2;
+ }
+ }
+ }
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('s')) && !is_null($this->getInput('c')) && !is_null($this->getInput('r'))) {
+ $subcategory = $this->getInput('s');
+ return 'WallpaperStop - '
+ . $this->getInput('c')
+ . (!empty($subcategory) ? ' > ' . $subcategory : '')
+ . ' ['
+ . $this->getInput('r')
+ . ']';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/WeLiveSecurityBridge.php b/bridges/WeLiveSecurityBridge.php
new file mode 100644
index 0000000..466a4b2
--- /dev/null
+++ b/bridges/WeLiveSecurityBridge.php
@@ -0,0 +1,45 @@
+<?php
+class WeLiveSecurityBridge extends FeedExpander {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = 'We Live Security';
+ const URI = 'http://www.welivesecurity.com/';
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ private function stripWithDelimiters($string, $start, $end){
+ while(strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ }
+
+ return $string;
+ }
+
+
+ protected function parseItem($item){
+ $item = parent::parseItem($item);
+
+ $article_html = getSimpleHTMLDOMCached($item['uri']);
+ if(!$article_html) {
+ $item['content'] .= '<p>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</p>';
+ return $item;
+ }
+
+ $article_content = $article_html->find('div.wlistingsingletext', 0)->innertext;
+ $article_content = $this->stripWithDelimiters($article_content, '<script', '</script>');
+ $article_content = '<p><b>'
+ . $item['content']
+ . '</b></p>'
+ . trim($article_content);
+
+ $item['content'] = $article_content;
+
+ return $item;
+ }
+
+ public function collectData(){
+ $feed = static::URI . 'feed/';
+ $this->collectExpandableDatas($feed);
+ }
+}
diff --git a/bridges/WebfailBridge.php b/bridges/WebfailBridge.php
new file mode 100644
index 0000000..2a63740
--- /dev/null
+++ b/bridges/WebfailBridge.php
@@ -0,0 +1,149 @@
+<?php
+class WebfailBridge extends BridgeAbstract {
+ const MAINTAINER = 'logmanoriginal';
+ const URI = 'https://webfail.com';
+ const NAME = 'Webfail';
+ const DESCRIPTION = 'Returns the latest fails';
+ const PARAMETERS = array(
+ 'By content type' => array(
+ 'language' => array(
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'title' => 'Select your language',
+ 'values' => array(
+ 'English' => 'en',
+ 'German' => 'de'
+ ),
+ 'defaultValue' => 'English'
+ ),
+ 'type' => array(
+ 'name' => 'Type',
+ 'type' => 'list',
+ 'title' => 'Select your content type',
+ 'values' => array(
+ 'None' => '/',
+ 'Facebook' => '/ffdts',
+ 'Images' => '/images',
+ 'Videos' => '/videos',
+ 'Gifs' => '/gifs'
+ ),
+ 'defaultValue' => 'None'
+ )
+ )
+ );
+
+ public function getURI(){
+ if(is_null($this->getInput('language')))
+ return parent::getURI();
+
+ // e.g.: https://en.webfail.com
+ return 'https://' . $this->getInput('language') . '.webfail.com';
+ }
+
+ 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($this->getURI() . $this->getInput('type'));
+
+ $type = array_search($this->getInput('type'),
+ self::PARAMETERS[$this->queriedContext]['type']['values']);
+
+ switch(strtolower($type)) {
+ case 'facebook':
+ case 'videos':
+ $this->extractNews($html, $type);
+ break;
+ case 'none':
+ case 'images':
+ case 'gifs':
+ $this->extractArticle($html);
+ break;
+ default: returnClientError('Unknown type: ' . $type);
+ }
+ }
+
+ private function extractNews($html, $type){
+ $news = $html->find('#main', 0)->find('a.wf-list-news');
+ foreach($news as $element) {
+ $item = array();
+ $item['title'] = $this->fixTitle($element->find('div.wf-news-title', 0)->innertext);
+ $item['uri'] = $this->getURI() . $element->href;
+
+ $img = $element->find('img.wf-image', 0)->src;
+ // Load high resolution image for 'facebook'
+ switch(strtolower($type)) {
+ case 'facebook':
+ $img = $this->getImageHiResUri($item['uri']);
+ break;
+ default:
+ }
+
+ $description = '';
+ if(!is_null($element->find('div.wf-news-description', 0))) {
+ $description = $element->find('div.wf-news-description', 0)->innertext;
+ }
+
+ $item['content'] = '<p>'
+ . $description
+ . '</p><br><a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $img
+ . '"></a>';
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function extractArticle($html){
+ $articles = $html->find('article');
+ foreach($articles as $article) {
+ $item = array();
+ $item['title'] = $this->fixTitle($article->find('a', 1)->innertext);
+
+ // Images, videos and gifs are provided in their own unique way
+ if(!is_null($article->find('img.wf-image', 0))) { // Image type
+ $item['uri'] = $this->getURI() . $article->find('a', 2)->href;
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="'
+ . $article->find('img.wf-image', 0)->src
+ . '"></a>';
+ } elseif(!is_null($article->find('div.wf-video', 0))) { // Video type
+ $videoId = $this->getVideoId($article->find('div.wf-play', 0)->onclick);
+ $item['uri'] = 'https://youtube.com/watch?v=' . $videoId;
+ $item['content'] = '<a href="'
+ . $item['uri']
+ . '"><img src="http://img.youtube.com/vi/'
+ . $videoId
+ . '/0.jpg"></a>';
+ } elseif(!is_null($article->find('video[id*=gif-]', 0))) { // Gif type
+ $item['uri'] = $this->getURI() . $article->find('a', 2)->href;
+ $item['content'] = '<video controls src="'
+ . $article->find('video[id*=gif-]', 0)->src
+ . '" poster="'
+ . $article->find('video[id*=gif-]', 0)->poster
+ . '"></video>';
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function fixTitle($title){
+ // This fixes titles that include umlauts (in German language)
+ return html_entity_decode($title, ENT_QUOTES | ENT_HTML401, 'UTF-8');
+ }
+
+ private function getVideoId($onclick){
+ return substr($onclick, 21, 11);
+ }
+
+ private function getImageHiResUri($url){
+ // https://de.webfail.com/ef524fae509?tag=ffdt
+ // http://cdn.webfail.com/upl/img/ef524fae509/post2.jpg
+ $id = substr($url, strrpos($url, '/') + 1, strlen($url) - strrpos($url, '?') + 2);
+ return 'http://cdn.webfail.com/upl/img/' . $id . '/post2.jpg';
+ }
+}
diff --git a/bridges/WhydBridge.php b/bridges/WhydBridge.php
new file mode 100644
index 0000000..347db6e
--- /dev/null
+++ b/bridges/WhydBridge.php
@@ -0,0 +1,56 @@
+<?php
+class WhydBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'kranack';
+ const NAME = 'Whyd Bridge';
+ const URI = 'http://www.whyd.com/';
+ const CACHE_TIMEOUT = 600; // 10min
+ const DESCRIPTION = 'Returns 10 newest music from user profile';
+
+ const PARAMETERS = array( array(
+ 'u' => array(
+ 'name' => 'username/id',
+ 'required' => true
+ )
+ ));
+
+ private $userName = '';
+
+ public function collectData(){
+ $html = '';
+ if(strlen(preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))) == 24) {
+ // is input the userid ?
+ $html = getSimpleHTMLDOM(
+ self::URI . 'u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))
+ ) or returnServerError('No results for this query.');
+ } else { // input may be the username
+ $html = getSimpleHTMLDOM(
+ self::URI . 'search?q=' . urlencode($this->getInput('u'))
+ ) or returnServerError('No results for this query.');
+
+ for($j = 0; $j < 5; $j++) {
+ if(strtolower($html->find('div.user', $j)->find('a', 0)->plaintext) == strtolower($this->getInput('u'))) {
+ $html = getSimpleHTMLDOM(
+ self::URI . $html->find('div.user', $j)->find('a', 0)->getAttribute('href')
+ ) or returnServerError('No results for this query');
+ break;
+ }
+ }
+ }
+ $this->userName = $html->find('div#profileTop', 0)->find('h1', 0)->plaintext;
+
+ for($i = 0; $i < 10; $i++) {
+ $track = $html->find('div.post', $i);
+ $item = array();
+ $item['author'] = $track->find('h2', 0)->plaintext;
+ $item['title'] = $track->find('h2', 0)->plaintext;
+ $item['content'] = $track->find('a.thumb', 0) . '<br/>' . $track->find('h2', 0)->plaintext;
+ $item['id'] = self::URI . $track->find('a.no-ajaxy', 0)->getAttribute('href');
+ $item['uri'] = self::URI . $track->find('a.no-ajaxy', 0)->getAttribute('href');
+ $this->items[] = $item;
+ }
+ }
+ public function getName(){
+ return (!empty($this->userName) ? $this->userName . ' - ' : '') . 'Whyd Bridge';
+ }
+}
diff --git a/bridges/WikiLeaksBridge.php b/bridges/WikiLeaksBridge.php
new file mode 100644
index 0000000..c5b9bb6
--- /dev/null
+++ b/bridges/WikiLeaksBridge.php
@@ -0,0 +1,129 @@
+<?php
+class WikiLeaksBridge extends BridgeAbstract {
+ const NAME = 'WikiLeaks';
+ const URI = 'https://wikileaks.org';
+ const DESCRIPTION = 'Returns the latest news or articles from WikiLeaks';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = array(
+ array(
+ 'category' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'required' => true,
+ 'title' => 'Select your category',
+ 'values' => array(
+ 'News' => '-News-',
+ 'Leaks' => array(
+ 'All' => '-Leaks-',
+ 'Intelligence' => '+-Intelligence-+',
+ 'Global Economy' => '+-Global-Economy-+',
+ 'International Politics' => '+-International-Politics-+',
+ 'Corporations' => '+-Corporations-+',
+ 'Government' => '+-Government-+',
+ 'War & Military' => '+-War-Military-+'
+ )
+ ),
+ 'defaultValue' => 'news'
+ ),
+ 'teaser' => array(
+ 'name' => 'Show teaser',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'title' => 'If checked feeds will display the teaser',
+ 'defaultValue' => true
+ )
+ )
+ );
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI());
+
+ // News are presented differently
+ switch($this->getInput('category')) {
+ case '-News-':
+ $this->loadNewsItems($html);
+ break;
+ default:
+ $this->loadLeakItems($html);
+ }
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('category'))) {
+ return static::URI . '/' . $this->getInput('category') . '.html';
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('category'))) {
+ $category = array_search(
+ $this->getInput('category'),
+ static::PARAMETERS[0]['category']['values']
+ );
+
+ if($category === false) {
+ $category = array_search(
+ $this->getInput('category'),
+ static::PARAMETERS[0]['category']['values']['Leaks']
+ );
+ }
+
+ return $category . ' - ' . static::NAME;
+ }
+
+ return parent::getName();
+ }
+
+ private function loadNewsItems($html){
+ $articles = $html->find('div.news-articles ul li');
+
+ if(is_null($articles) || count($articles) === 0) {
+ return;
+ }
+
+ foreach($articles as $article) {
+ $item = array();
+
+ $item['title'] = $article->find('h3', 0)->plaintext;
+ $item['uri'] = static::URI . $article->find('h3 a', 0)->href;
+ $item['content'] = $article->find('div.introduction', 0)->plaintext;
+ $item['timestamp'] = strtotime($article->find('div.timestamp', 0)->plaintext);
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function loadLeakItems($html){
+ $articles = $html->find('li.tile');
+
+ if(is_null($articles) || count($articles) === 0) {
+ return;
+ }
+
+ foreach($articles as $article) {
+ $item = array();
+
+ $item['title'] = $article->find('h2', 0)->plaintext;
+ $item['uri'] = static::URI . $article->find('a', 0)->href;
+
+ $teaser = static::URI . '/' . $article->find('div.teaser img', 0)->src;
+
+ if($this->getInput('teaser')) {
+ $item['content'] = '<img src="'
+ . $teaser
+ . '" /><p>'
+ . $article->find('div.intro', 0)->plaintext
+ . '</p>';
+ } else {
+ $item['content'] = $article->find('div.intro', 0)->plaintext;
+ }
+
+ $item['timestamp'] = strtotime($article->find('div.timestamp', 0)->plaintext);
+ $item['enclosures'] = array($teaser);
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/WikipediaBridge.php b/bridges/WikipediaBridge.php
new file mode 100644
index 0000000..6b53440
--- /dev/null
+++ b/bridges/WikipediaBridge.php
@@ -0,0 +1,304 @@
+<?php
+
+define('WIKIPEDIA_SUBJECT_TFA', 0); // Today's featured article
+define('WIKIPEDIA_SUBJECT_DYK', 1); // Did you know...
+
+class WikipediaBridge extends BridgeAbstract {
+ const MAINTAINER = 'logmanoriginal';
+ const NAME = 'Wikipedia bridge for many languages';
+ const URI = 'https://www.wikipedia.org/';
+ const DESCRIPTION = 'Returns articles for a language of your choice';
+
+ const PARAMETERS = array( array(
+ 'language' => array(
+ 'name' => 'Language',
+ 'type' => 'list',
+ 'required' => true,
+ 'title' => 'Select your language',
+ 'exampleValue' => 'English',
+ 'values' => array(
+ 'English' => 'en',
+ 'Dutch' => 'nl',
+ 'Esperanto' => 'eo',
+ 'French' => 'fr',
+ 'German' => 'de',
+ )
+ ),
+ 'subject' => array(
+ 'name' => 'Subject',
+ 'type' => 'list',
+ 'required' => true,
+ 'title' => 'What subject are you interested in?',
+ 'exampleValue' => 'Today\'s featured article',
+ 'values' => array(
+ 'Today\'s featured article' => 'tfa',
+ 'Did you know…' => 'dyk'
+ )
+ ),
+ 'fullarticle' => array(
+ 'name' => 'Load full article',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to always load the full article'
+ )
+ ));
+
+ public function getURI(){
+ if(!is_null($this->getInput('language'))) {
+ return 'https://'
+ . strtolower($this->getInput('language'))
+ . '.wikipedia.org';
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName(){
+ switch($this->getInput('subject')) {
+ case 'tfa':
+ $subject = WIKIPEDIA_SUBJECT_TFA;
+ break;
+ case 'dyk':
+ $subject = WIKIPEDIA_SUBJECT_DYK;
+ break;
+ default: return parent::getName();
+ }
+
+ switch($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $name = 'Today\'s featured article from '
+ . strtolower($this->getInput('language'))
+ . '.wikipedia.org';
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $name = 'Did you know? - articles from '
+ . strtolower($this->getInput('language'))
+ . '.wikipedia.org';
+ break;
+ default:
+ $name = 'Articles from '
+ . strtolower($this->getInput('language'))
+ . '.wikipedia.org';
+ break;
+ }
+ return $name;
+ }
+
+ public function collectData(){
+
+ switch($this->getInput('subject')) {
+ case 'tfa':
+ $subject = WIKIPEDIA_SUBJECT_TFA;
+ break;
+ case 'dyk':
+ $subject = WIKIPEDIA_SUBJECT_DYK;
+ break;
+ default:
+ $subject = WIKIPEDIA_SUBJECT_TFA;
+ break;
+ }
+
+ $fullArticle = $this->getInput('fullarticle');
+
+ // This will automatically send us to the correct main page in any language (try it!)
+ $html = getSimpleHTMLDOM($this->getURI() . '/wiki');
+
+ if(!$html)
+ returnServerError('Could not load site: ' . $this->getURI() . '!');
+
+ /*
+ * Now read content depending on the language (make sure to create one function per language!)
+ * We build the function name automatically, just make sure you create a private function ending
+ * with your desired language code, where the language code is upper case! (en -> getContentsEN).
+ */
+ $function = 'getContents' . ucfirst(strtolower($this->getInput('language')));
+
+ if(!method_exists($this, $function))
+ returnServerError('A function to get the contents for your language is missing (\'' . $function . '\')!');
+
+ /*
+ * The method takes care of creating all items.
+ */
+ $this->$function($html, $subject, $fullArticle);
+ }
+
+ /**
+ * Replaces all relative URIs with absolute ones
+ * @param $element A simplehtmldom element
+ * @return The $element->innertext with all URIs replaced
+ */
+ private function replaceUriInHtmlElement($element){
+ return str_replace('href="/', 'href="' . $this->getURI() . '/', $element->innertext);
+ }
+
+ /*
+ * Adds a new item to $items using a generic operation (should work for most
+ * (all?) wikis) $anchorText can be specified if the wiki in question doesn't
+ * use '...' (like Dutch, French and Italian) $anchorFallbackIndex can be
+ * used to specify a different fallback link than the first
+ * (e.g., -1 for the last)
+ */
+ private function addTodaysFeaturedArticleGeneric($element,
+ $fullArticle,
+ $anchorText = '...',
+ $anchorFallbackIndex = 0){
+ // Clean the bottom of the featured article
+ if ($element->find('div', -1))
+ $element->find('div', -1)->outertext = '';
+
+ // The title and URI of the article can be found in an anchor containing
+ // the string '...' in most wikis ('full article ...')
+ $target = $element->find('p/a', $anchorFallbackIndex);
+ foreach($element->find('//a') as $anchor) {
+ if(strpos($anchor->innertext, $anchorText) !== false) {
+ $target = $anchor;
+ break;
+ }
+ }
+
+ $item = array();
+ $item['uri'] = $this->getURI() . $target->href;
+ $item['title'] = $target->title;
+
+ if(!$fullArticle)
+ $item['content'] = strip_tags($this->replaceUriInHtmlElement($element), '<a><p><br><img>');
+ else
+ $item['content'] = $this->loadFullArticle($item['uri']);
+
+ $this->items[] = $item;
+ }
+
+ /*
+ * Adds a new item to $items using a generic operation (should work for most (all?) wikis)
+ */
+ private function addDidYouKnowGeneric($element, $fullArticle){
+ foreach($element->find('ul', 0)->find('li') as $entry) {
+ $item = array();
+
+ // We can only use the first anchor, there is no way of finding the 'correct' one if there are multiple
+ $item['uri'] = $this->getURI() . $entry->find('a', 0)->href;
+ $item['title'] = strip_tags($entry->innertext);
+
+ if(!$fullArticle)
+ $item['content'] = $this->replaceUriInHtmlElement($entry);
+ else
+ $item['content'] = $this->loadFullArticle($item['uri']);
+
+ $this->items[] = $item;
+ }
+ }
+
+ /**
+ * Loads the full article from a given URI
+ */
+ private function loadFullArticle($uri){
+ $content_html = getSimpleHTMLDOMCached($uri);
+
+ if(!$content_html)
+ returnServerError('Could not load site: ' . $uri . '!');
+
+ $content = $content_html->find('#mw-content-text', 0);
+
+ if(!$content)
+ returnServerError('Could not find content in page: ' . $uri . '!');
+
+ // Let's remove a couple of things from the article
+ $table = $content->find('#toc', 0); // Table of contents
+ if(!$table === false)
+ $table->outertext = '';
+
+ foreach($content->find('ol.references') as $reference) // References
+ $reference->outertext = '';
+
+ return str_replace('href="/', 'href="' . $this->getURI() . '/', $content->innertext);
+ }
+
+ /**
+ * Implementation for de.wikipedia.org
+ */
+ private function getContentsDe($html, $subject, $fullArticle){
+ switch($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $element = $html->find('div[id=mf-tfa]', 0);
+ $this->addTodaysFeaturedArticleGeneric($element, $fullArticle);
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $element = $html->find('div[id=mf-dyk]', 0);
+ $this->addDidYouKnowGeneric($element, $fullArticle);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Implementation for fr.wikipedia.org
+ */
+ private function getContentsFr($html, $subject, $fullArticle){
+ switch($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $element = $html->find('div[class=accueil_2017_cadre]', 0);
+ $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, 'Lire la suite');
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $element = $html->find('div[class=accueil_2017_cadre]', 2);
+ $this->addDidYouKnowGeneric($element, $fullArticle);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Implementation for en.wikipedia.org
+ */
+ private function getContentsEn($html, $subject, $fullArticle){
+ switch($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $element = $html->find('div[id=mp-tfa]', 0);
+ $this->addTodaysFeaturedArticleGeneric($element, $fullArticle);
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $element = $html->find('div[id=mp-dyk]', 0);
+ $this->addDidYouKnowGeneric($element, $fullArticle);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Implementation for eo.wikipedia.org
+ */
+ private function getContentsEo($html, $subject, $fullArticle){
+ switch($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $element = $html->find('div[id=mf-artikolo-de-la-semajno]', 0);
+ $this->addTodaysFeaturedArticleGeneric($element, $fullArticle);
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $element = $html->find('div[id=mw-content-text]', 0)->find('table', 4)->find('td', 4);
+ $this->addDidYouKnowGeneric($element, $fullArticle);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Implementation for nl.wikipedia.org
+ */
+ private function getContentsNl($html, $subject, $fullArticle){
+ switch($subject) {
+ case WIKIPEDIA_SUBJECT_TFA:
+ $element = $html->find('div[id=mf-uitgelicht]', 0);
+ $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, 'Lees meer');
+ break;
+ case WIKIPEDIA_SUBJECT_DYK:
+ $element = $html->find('div[id=mw-content-text]', 0)->find('table', 4)->find('td', 2);
+ $this->addDidYouKnowGeneric($element, $fullArticle);
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/bridges/WordPressBridge.php b/bridges/WordPressBridge.php
new file mode 100644
index 0000000..b367adc
--- /dev/null
+++ b/bridges/WordPressBridge.php
@@ -0,0 +1,76 @@
+<?php
+class WordPressBridge extends FeedExpander {
+ const MAINTAINER = 'aledeg';
+ const NAME = 'Wordpress Bridge';
+ const URI = 'https://wordpress.org/';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the newest full posts of a Wordpress powered website';
+
+ const PARAMETERS = array( array(
+ 'url' => array(
+ 'name' => 'Blog URL',
+ 'required' => true
+ )
+ ));
+
+ private function clearContent($content){
+ $content = preg_replace('/<script[^>]*>[^<]*<\/script>/', '', $content);
+ $content = preg_replace('/<div class="wpa".*/', '', $content);
+ $content = preg_replace('/<form.*\/form>/', '', $content);
+ return $content;
+ }
+
+ protected function parseItem($newItem){
+ $item = parent::parseItem($newItem);
+
+ $article_html = getSimpleHTMLDOMCached($item['uri']);
+
+ $article = null;
+ switch(true) {
+ case !is_null($article_html->find('article', 0)):
+ // most common content div
+ $article = $article_html->find('article', 0);
+ break;
+ case !is_null($article_html->find('.single-content', 0)):
+ // another common content div
+ $article = $article_html->find('.single-content', 0);
+ break;
+ case !is_null($article_html->find('.post-content', 0)):
+ // another common content div
+ $article = $article_html->find('.post-content', 0);
+ break;
+
+ case !is_null($article_html->find('.post', 0)):
+ // for old WordPress themes without HTML5
+ $article = $article_html->find('.post', 0);
+ break;
+ }
+
+ if(!is_null($article)) {
+ $item['content'] = $this->clearContent($article->innertext);
+ }
+
+ return $item;
+ }
+
+ public function getURI(){
+ $url = $this->getInput('url');
+ if(empty($url)) {
+ $url = parent::getURI();
+ }
+ return $url;
+ }
+
+ public function collectData(){
+ if($this->getInput('url') && substr($this->getInput('url'), 0, strlen('http')) !== 'http') {
+ // just in case someone find a way to access local files by playing with the url
+ returnClientError('The url parameter must either refer to http or https protocol.');
+ }
+ try{
+ $this->collectExpandableDatas($this->getURI() . '/feed/atom/');
+ } catch (HttpException $e) {
+ $this->collectExpandableDatas($this->getURI() . '/?feed=atom');
+ }
+
+ }
+}
diff --git a/bridges/WordPressPluginUpdateBridge.php b/bridges/WordPressPluginUpdateBridge.php
new file mode 100644
index 0000000..cb57df8
--- /dev/null
+++ b/bridges/WordPressPluginUpdateBridge.php
@@ -0,0 +1,87 @@
+<?php
+class WordPressPluginUpdateBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'WordPress Plugins Update Bridge';
+ const URI = 'https://wordpress.org/plugins/';
+ const CACHE_TIMEOUT = 86400; // 24h = 86400s
+ const DESCRIPTION = 'Returns latest updates of WordPress.com plugins.';
+
+ const PARAMETERS = array(
+ array(
+ 'pluginUrl' => array(
+ 'name' => 'URL to the plugin',
+ 'required' => true
+ )
+ )
+ );
+
+ public function collectData(){
+
+ $request = str_replace('/', '', $this->getInput('pluginUrl'));
+ $page = self::URI . $request . '/changelog/';
+
+ $html = getSimpleHTMLDOM($page)
+ or returnServerError('No results for this query.');
+
+ $content = $html->find('.block-content', 0);
+
+ $item = array();
+ $item['content'] = '';
+ $version = null;
+
+ foreach($content->children() as $element) {
+
+ if($element->tag != 'h4') {
+
+ $item['content'] .= $element;
+
+ } else {
+
+ if($version == null) {
+
+ $version = $element;
+
+ } else {
+
+ $item['title'] = $version;
+ $item['uri'] = 'https://downloads.wordpress.org/plugin/' . $request . '.' . strip_tags($version) . '.zip';
+ $this->items[] = $item;
+
+ $version = $element;
+ $item = array();
+ $item['content'] = '';
+
+ }
+
+ }
+
+ }
+
+ $item['uri'] = 'https://downloads.wordpress.org/plugin/' . $request . '.' . strip_tags($version) . '.zip';
+ $item['title'] = $version;
+ $this->items[] = $item;
+
+ }
+
+
+ public function getName(){
+ if(!is_null($this->getInput('q'))) {
+ return $this->getInput('q') . ' : ' . self::NAME;
+ }
+
+ return parent::getName();
+ }
+
+ private function getCachedDate($url){
+ debugMessage('getting pubdate from url ' . $url . '');
+ // Initialize cache
+ $cache = Cache::create('FileCache');
+ $cache->setPath(CACHE_DIR . '/pages');
+ $params = [$url];
+ $cache->setParameters($params);
+ // Get cachefile timestamp
+ $time = $cache->getTime();
+ return ($time !== false ? $time : time());
+ }
+}
diff --git a/bridges/WorldOfTanksBridge.php b/bridges/WorldOfTanksBridge.php
new file mode 100644
index 0000000..46dd588
--- /dev/null
+++ b/bridges/WorldOfTanksBridge.php
@@ -0,0 +1,52 @@
+<?php
+class WorldOfTanksBridge extends FeedExpander {
+
+ const MAINTAINER = 'Riduidel';
+ const NAME = 'World of Tanks';
+ const URI = 'http://worldoftanks.eu/';
+ const DESCRIPTION = 'News about the tank slaughter game.';
+
+ const PARAMETERS = array( array(
+ 'lang' => array(
+ 'name' => 'Langue',
+ 'type' => 'list',
+ 'values' => array(
+ 'Français' => 'fr',
+ 'English' => 'en',
+ 'Español' => 'es',
+ 'Deutsch' => 'de',
+ 'Čeština' => 'cs',
+ 'Polski' => 'pl',
+ 'Türkçe' => 'tr'
+ )
+ )
+ ));
+
+ public function collectData() {
+ $this->collectExpandableDatas(sprintf('https://worldoftanks.eu/%s/rss/news/', $this->getInput('lang')));
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $item['content'] = $this->loadFullArticle($item['uri']);
+ return $item;
+ }
+
+ /**
+ * Loads the full article and returns the contents
+ * @param $uri The article URI
+ * @return The article content
+ */
+ private function loadFullArticle($uri){
+ $html = getSimpleHTMLDOMCached($uri);
+
+ $content = $html->find('article', 0);
+
+ // Remove the scripts, please
+ foreach($content->find('script') as $script) {
+ $script->outertext = '';
+ }
+
+ return $content->innertext;
+ }
+}
diff --git a/bridges/XbooruBridge.php b/bridges/XbooruBridge.php
new file mode 100644
index 0000000..d3605be
--- /dev/null
+++ b/bridges/XbooruBridge.php
@@ -0,0 +1,12 @@
+<?php
+require_once('GelbooruBridge.php');
+
+class XbooruBridge extends GelbooruBridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Xbooru';
+ const URI = 'http://xbooru.com/';
+ const DESCRIPTION = 'Returns images from given page';
+
+ const PIDBYPAGE = 50;
+}
diff --git a/bridges/YGGTorrentBridge.php b/bridges/YGGTorrentBridge.php
new file mode 100644
index 0000000..bc434d3
--- /dev/null
+++ b/bridges/YGGTorrentBridge.php
@@ -0,0 +1,143 @@
+<?php
+
+/* This is a mashup of FlickrExploreBridge by sebsauvage and FlickrTagBridge
+ * by erwang.providing the functionality of both in one.
+ */
+class YGGTorrentBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'Yggtorrent Bridge';
+ const URI = 'https://yggtorrent.is';
+ const DESCRIPTION = 'Returns torrent search from Yggtorrent';
+
+ const PARAMETERS = array(
+ array(
+ 'cat' => array(
+ 'name' => 'category',
+ 'type' => 'list',
+ 'values' => array(
+ 'Toute les catégories' => 'all.all',
+ 'Film/Vidéo - Toutes les sous-catégories' => '2145.all',
+ 'Film/Vidéo - Animation' => '2145.2178',
+ 'Film/Vidéo - Animation Série' => '2145.2179',
+ 'Film/Vidéo - Concert' => '2145.2180',
+ 'Film/Vidéo - Documentaire' => '2145.2181',
+ 'Film/Vidéo - Émission TV' => '2145.2182',
+ 'Film/Vidéo - Film' => '2145.2183',
+ 'Film/Vidéo - Série TV' => '2145.2184',
+ 'Film/Vidéo - Spectacle' => '2145.2185',
+ 'Film/Vidéo - Sport' => '2145.2186',
+ 'Film/Vidéo - Vidéo-clips' => '2145.2186',
+ 'Audio - Toutes les sous-catégories' => '2139.all',
+ 'Audio - Karaoké' => '2139.2147',
+ 'Audio - Musique' => '2139.2148',
+ 'Audio - Podcast Radio' => '2139.2150',
+ 'Audio - Samples' => '2139.2149',
+ 'Jeu vidéo - Toutes les sous-catégories' => '2142.all',
+ 'Jeu vidéo - Autre' => '2142.2167',
+ 'Jeu vidéo - Linux' => '2142.2159',
+ 'Jeu vidéo - MacOS' => '2142.2160',
+ 'Jeu vidéo - Microsoft' => '2142.2162',
+ 'Jeu vidéo - Nintendo' => '2142.2163',
+ 'Jeu vidéo - Smartphone' => '2142.2165',
+ 'Jeu vidéo - Sony' => '2142.2164',
+ 'Jeu vidéo - Tablette' => '2142.2166',
+ 'Jeu vidéo - Windows' => '2142.2161',
+ 'eBook - Toutes les sous-catégories' => '2140.all',
+ 'eBook - Audio' => '2140.2151',
+ 'eBook - Bds' => '2140.2152',
+ 'eBook - Comics' => '2140.2153',
+ 'eBook - Livres' => '2140.2154',
+ 'eBook - Mangas' => '2140.2155',
+ 'eBook - Presse' => '2140.2156',
+ 'Emulation - Toutes les sous-catégories' => '2141.all',
+ 'Emulation - Emulateurs' => '2141.2157',
+ 'Emulation - Roms' => '2141.2158',
+ 'GPS - Toutes les sous-catégories' => '2141.all',
+ 'GPS - Applications' => '2141.2168',
+ 'GPS - Cartes' => '2141.2169',
+ 'GPS - Divers' => '2141.2170'
+ )
+ ),
+ 'nom' => array(
+ 'name' => 'Nom',
+ 'description' => 'Nom du torrent',
+ 'type' => 'text'
+ ),
+ 'description' => array(
+ 'name' => 'Description',
+ 'description' => 'Description du torrent',
+ 'type' => 'text'
+ ),
+ 'fichier' => array(
+ 'name' => 'Fichier',
+ 'description' => 'Fichier du torrent',
+ 'type' => 'text'
+ ),
+ 'uploader' => array(
+ 'name' => 'Uploader',
+ 'description' => 'Uploader du torrent',
+ 'type' => 'text'
+ ),
+
+ )
+ );
+
+ public function collectData() {
+
+ $catInfo = explode('.', $this->getInput('cat'));
+ $category = $catInfo[0];
+ $subcategory = $catInfo[1];
+
+ $html = getSimpleHTMLDOM(self::URI . '/engine/search?name='
+ . $this->getInput('nom')
+ . '&description='
+ . $this->getInput('description')
+ . '&fichier='
+ . $this->getInput('fichier')
+ . '&file='
+ . $this->getInput('uploader')
+ . '&category='
+ . $category
+ . '&sub_category='
+ . $subcategory
+ . '&do=search')
+ or returnServerError('Unable to query Yggtorrent !');
+
+ $count = 0;
+ $results = $html->find('.results', 0);
+ if(!$results) return;
+
+ foreach($results->find('tr') as $row) {
+ $count++;
+ if($count == 1) continue;
+ if($count == 12) break;
+ $item = array();
+ $item['timestamp'] = $row->find('.hidden', 1)->plaintext;
+ $item['title'] = $row->find('a', 1)->plaintext;
+ $torrentData = $this->collectTorrentData($row->find('a', 1)->href);
+ $item['author'] = $torrentData['author'];
+ $item['content'] = $torrentData['content'];
+ $item['seeders'] = $row->find('td', 7)->plaintext;
+ $item['leechers'] = $row->find('td', 8)->plaintext;
+ $item['size'] = $row->find('td', 5)->plaintext;
+
+ $this->items[] = $item;
+ }
+
+ }
+
+ public function collectTorrentData($url) {
+
+ //For weird reason, the link we get can be invalid, we fix it.
+ $url_full = explode('/', $url);
+ $url_full[4] = urlencode($url_full[4]);
+ $url_full[5] = urlencode($url_full[5]);
+ $url_full[6] = urlencode($url_full[6]);
+ $url = implode('/', $url_full);
+ $page = getSimpleHTMLDOM($url) or returnServerError('Unable to query Yggtorrent page !');
+ $author = $page->find('.informations', 0)->find('a', 4)->plaintext;
+ $content = $page->find('.default', 1);
+ return array('author' => $author, 'content' => $content);
+ }
+}
diff --git a/bridges/YandereBridge.php b/bridges/YandereBridge.php
new file mode 100644
index 0000000..df8b30e
--- /dev/null
+++ b/bridges/YandereBridge.php
@@ -0,0 +1,11 @@
+<?php
+require_once('MoebooruBridge.php');
+
+class YandereBridge extends MoebooruBridge {
+
+ const MAINTAINER = 'mitsukarenai';
+ const NAME = 'Yande.re';
+ const URI = 'https://yande.re/';
+ const DESCRIPTION = 'Returns images from given page and tags';
+
+}
diff --git a/bridges/YoutubeBridge.php b/bridges/YoutubeBridge.php
new file mode 100644
index 0000000..e597fe3
--- /dev/null
+++ b/bridges/YoutubeBridge.php
@@ -0,0 +1,230 @@
+<?php
+/**
+* RssBridgeYoutube
+* Returns the newest videos
+* WARNING: to parse big playlists (over ~90 videos), you need to edit simple_html_dom.php:
+* change: define('MAX_FILE_SIZE', 600000);
+* into: define('MAX_FILE_SIZE', 900000); (or more)
+*/
+class YoutubeBridge extends BridgeAbstract {
+
+ const NAME = 'YouTube Bridge';
+ const URI = 'https://www.youtube.com/';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the 10 newest videos by username/channel/playlist or search';
+ const MAINTAINER = 'mitsukarenai';
+
+ const PARAMETERS = array(
+ 'By username' => array(
+ 'u' => array(
+ 'name' => 'username',
+ 'exampleValue' => 'test',
+ 'required' => true
+ )
+ ),
+ 'By channel id' => array(
+ 'c' => array(
+ 'name' => 'channel id',
+ 'exampleValue' => '15',
+ 'required' => true
+ )
+ ),
+ 'By playlist Id' => array(
+ 'p' => array(
+ 'name' => 'playlist id',
+ 'exampleValue' => '15'
+ )
+ ),
+ 'Search result' => array(
+ 's' => array(
+ 'name' => 'search keyword',
+ 'exampleValue' => 'test'
+ ),
+ 'pa' => array(
+ 'name' => 'page',
+ 'type' => 'number',
+ 'exampleValue' => 1
+ )
+ )
+ );
+
+ private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time){
+ $html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid");
+
+ // 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){
+ $item = array();
+ $item['id'] = $vid;
+ $item['title'] = $title;
+ $item['author'] = $author;
+ $item['timestamp'] = $time;
+ $item['uri'] = self::URI . 'watch?v=' . $vid;
+ $thumbnailUri = str_replace('/www.', '/img.', self::URI) . 'vi/' . $vid . '/0.jpg';
+ $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $thumbnailUri . '" /></a><br />' . $desc;
+ $this->items[] = $item;
+ }
+
+ private function ytBridgeParseXmlFeed($xml) {
+ foreach($xml->find('entry') as $element) {
+ $title = $this->ytBridgeFixTitle($element->find('title', 0)->plaintext);
+ $author = $element->find('name', 0)->plaintext;
+ $desc = $element->find('media:description', 0)->innertext;
+
+ // Make sure the description is easy on the eye :)
+ $desc = htmlspecialchars($desc);
+ $desc = nl2br($desc);
+ $desc = preg_replace('/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims',
+ '<a href="$1" target="_blank">$1</a> ',
+ $desc);
+
+ $vid = str_replace('yt:video:', '', $element->find('id', 0)->plaintext);
+ $time = strtotime($element->find('published', 0)->plaintext);
+ if(strpos($vid, 'googleads') === false)
+ $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
+ }
+ $this->feedName = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext); // feedName will be used by getName()
+ }
+
+ private function ytBridgeParseHtmlListing($html, $element_selector, $title_selector, $add_parsed_items = true) {
+ $limit = $add_parsed_items ? 10 : INF;
+ $count = 0;
+ foreach($html->find($element_selector) as $element) {
+ if($count < $limit) {
+ $author = '';
+ $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]' && strpos($vid, 'googleads') === false) {
+ if ($add_parsed_items) {
+ $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
+ $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
+ }
+ $count++;
+ }
+ }
+ }
+ return $count;
+ }
+
+ private function ytBridgeFixTitle($title) {
+ // convert both &#1234; and &quot; to UTF-8
+ return html_entity_decode($title, ENT_QUOTES, 'UTF-8');
+ }
+
+ private function ytGetSimpleHTMLDOM($url){
+ return getSimpleHTMLDOM($url,
+ $header = array(),
+ $opts = array(),
+ $lowercase = true,
+ $forceTagsClosed = true,
+ $target_charset = DEFAULT_TARGET_CHARSET,
+ $stripRN = false,
+ $defaultBRText = DEFAULT_BR_TEXT,
+ $defaultSpanText = DEFAULT_SPAN_TEXT);
+ }
+
+ public function collectData(){
+
+ $xml = '';
+ $html = '';
+ $url_feed = '';
+ $url_listing = '';
+
+ if($this->getInput('u')) { /* User and Channel modes */
+ $this->request = $this->getInput('u');
+ $url_feed = self::URI . 'feeds/videos.xml?user=' . urlencode($this->request);
+ $url_listing = self::URI . 'user/' . urlencode($this->request) . '/videos';
+ } elseif($this->getInput('c')) {
+ $this->request = $this->getInput('c');
+ $url_feed = self::URI . 'feeds/videos.xml?channel_id=' . urlencode($this->request);
+ $url_listing = self::URI . 'channel/' . urlencode($this->request) . '/videos';
+ }
+
+ if(!empty($url_feed) && !empty($url_listing)) {
+ if($xml = $this->ytGetSimpleHTMLDOM($url_feed)) {
+ $this->ytBridgeParseXmlFeed($xml);
+ } elseif($html = $this->ytGetSimpleHTMLDOM($url_listing)) {
+ $this->ytBridgeParseHtmlListing($html, 'li.channels-content-item', 'h3');
+ } else {
+ returnServerError("Could not request YouTube. Tried:\n - $url_feed\n - $url_listing");
+ }
+ } elseif($this->getInput('p')) { /* playlist mode */
+ $this->request = $this->getInput('p');
+ $url_feed = self::URI . 'feeds/videos.xml?playlist_id=' . urlencode($this->request);
+ $url_listing = self::URI . 'playlist?list=' . urlencode($this->request);
+ $html = $this->ytGetSimpleHTMLDOM($url_listing)
+ or returnServerError("Could not request YouTube. Tried:\n - $url_listing");
+ $item_count = $this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a', false);
+ if ($item_count <= 15 && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) {
+ $this->ytBridgeParseXmlFeed($xml);
+ } else {
+ $this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a');
+ }
+ $this->feedName = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
+ usort($this->items, function ($item1, $item2) {
+ return $item2['timestamp'] - $item1['timestamp'];
+ });
+ } elseif($this->getInput('s')) { /* search mode */
+ $this->request = $this->getInput('s');
+ $page = 1;
+ if($this->getInput('pa'))
+ $page = (int)preg_replace('/[^0-9]/', '', $this->getInput('pa'));
+
+ $url_listing = self::URI
+ . 'results?search_query='
+ . urlencode($this->request)
+ . '&page='
+ . $page
+ . '&filters=video&search_sort=video_date_uploaded';
+
+ $html = $this->ytGetSimpleHTMLDOM($url_listing)
+ or returnServerError("Could not request YouTube. Tried:\n - $url_listing");
+
+ $this->ytBridgeParseHtmlListing($html, 'div.yt-lockup', 'h3 > a');
+ $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=...)");
+ }
+ }
+
+ public function getName(){
+ // Name depends on queriedContext:
+ switch($this->queriedContext) {
+ case 'By username':
+ case 'By channel id':
+ case 'By playlist Id':
+ case 'Search result':
+ return $this->feedName . ' - YouTube'; // We already know it's a bridge, right?
+ default:
+ return parent::getName();
+ }
+ }
+}
diff --git a/bridges/ZDNetBridge.php b/bridges/ZDNetBridge.php
new file mode 100644
index 0000000..86e4b49
--- /dev/null
+++ b/bridges/ZDNetBridge.php
@@ -0,0 +1,302 @@
+<?php
+class ZDNetBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'ORelio';
+ const NAME = 'ZDNet Bridge';
+ const URI = 'http://www.zdnet.com/';
+ const DESCRIPTION = 'Technology News, Analysis, Comments and Product Reviews for IT Professionals.';
+
+ //http://www.zdnet.com/zdnet.opml
+ const PARAMETERS = array( array(
+ 'feed' => array(
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'values' => array(
+ 'Subscribe to ZDNet RSS Feeds' => array(
+ 'All Blogs' => 'blog',
+ 'Just News' => 'news',
+ 'All Reviews' => 'topic/reviews',
+ 'Latest Downloads' => 'downloads!recent',
+ 'Latest Articles' => '/',
+ 'Latest Australia Articles' => 'au',
+ 'Latest UK Articles' => 'uk',
+ 'Latest US Articles' => 'us',
+ 'Latest Asia Articles' => 'as'
+ ),
+ 'Keep up with ZDNet Blogs RSS:' => array(
+ 'Transforming the Datacenter' => 'blog/transforming-datacenter',
+ 'SMB India' => 'blog/smb-india',
+ 'Indonesia BizTech' => 'blog/indonesia-biztech',
+ 'Hong Kong Techie' => 'blog/hong-kong-techie',
+ 'Tech Taiwan' => 'blog/tech-taiwan',
+ 'Startup India' => 'blog/startup-india',
+ 'Starting Up Asia' => 'blog/starting-up-asia',
+ 'Next-Gen Partner' => 'blog/partner',
+ 'Post-PC Developments' => 'blog/post-pc',
+ 'Benelux' => 'blog/benelux',
+ 'Heat Sink' => 'blog/heat-sink',
+ 'Italy\'s got tech' => 'blog/italy',
+ 'African Enterprise' => 'blog/african-enterprise',
+ 'New Tech for Old India' => 'blog/new-india',
+ 'Estonia Uncovered' => 'blog/estonia',
+ 'IT Iberia' => 'blog/iberia',
+ 'Brazil Tech' => 'blog/brazil',
+ '500 words into the future' => 'blog/500-words-into-the-future',
+ 'ÜberTech' => 'blog/ubertech',
+ 'All About Microsoft' => 'blog/microsoft',
+ 'Back office' => 'blog/back-office',
+ 'Barker Bites Back' => 'blog/barker-bites-back',
+ 'Between the Lines' => 'blog/btl',
+ 'Big on Data' => 'blog/big-data',
+ 'bootstrappr' => 'blog/bootstrappr',
+ 'By The Way' => 'blog/by-the-way',
+ 'Central European Processing' => 'blog/central-europe',
+ 'Cloud Builders' => 'blog/cloud-builders',
+ 'Communication Breakdown' => 'blog/communication-breakdown',
+ 'Collaboration 2.0' => 'blog/collaboration',
+ 'Constellation Research' => 'blog/constellation',
+ 'Consumerization: BYOD' => 'blog/consumerization',
+ 'DIY-IT' => 'blog/diy-it',
+ 'Enterprise Web 2.0' => 'blog/hinchcliffe',
+ 'Five Nines: The Next Gen Datacenter' => 'blog/datacenter',
+ 'Forrester Research' => 'blog/forrester',
+ 'Full Duplex' => 'blog/full-duplex',
+ 'Gen Why?' => 'blog/gen-why',
+ 'Hardware 2.0' => 'blog/hardware',
+ 'Identity Matters' => 'blog/identity',
+ 'iGeneration' => 'blog/igeneration',
+ 'Internet of Everything' => 'blog/cisco',
+ 'Beyond IT Failure' => 'blog/projectfailures',
+ 'Jamie\'s Mostly Linux Stuff' => 'blog/jamies-mostly-linux-stuff',
+ 'Jack\'s Blog' => 'blog/jacks-blog',
+ 'Laptops & Desktops' => 'blog/computers',
+ 'Linux and Open Source' => 'blog/open-source',
+ 'London Calling' => 'blog/london',
+ 'Mapping Babel' => 'blog/mapping-babel',
+ 'Mixed Signals' => 'blog/mixed-signals',
+ 'Mobile India' => 'blog/mobile-india',
+ 'Mobile News' => 'blog/mobile-news',
+ 'Networking' => 'blog/networking',
+ 'Norse Code' => 'blog/norse-code',
+ 'Null Pointer' => 'blog/null-pointer',
+ 'The Full Tilt' => 'blog/the-full-tilt',
+ 'Pinoy Post' => 'blog/pinoy-post',
+ 'Practically Tech' => 'blog/practically-tech',
+ 'Product Central' => 'blog/product-central',
+ 'Pulp Tech' => 'blog/violetblue',
+ 'Qubits and Pieces' => 'blog/qubits-and-pieces',
+ 'Securify This!' => 'blog/securify-this',
+ 'Service Oriented' => 'blog/service-oriented',
+ 'Small Talk' => 'blog/small-talk',
+ 'Small Business Matters' => 'blog/small-business-matters',
+ 'Smartphones and Cell Phones' => 'blog/cell-phones',
+ 'Social Business' => 'blog/feeds',
+ 'Social CRM: The Conversation' => 'blog/crm',
+ 'Software & Services Safari' => 'blog/sommer',
+ 'Storage Bits' => 'blog/storage',
+ 'Stacking up Open Clouds' => 'blog/apac-redhat',
+ 'Techie Isles' => 'blog/techie-isles',
+ 'Technolatte' => 'blog/technolatte',
+ 'Tech Podium' => 'blog/tech-podium',
+ 'Tel Aviv Tech' => 'blog/tel-aviv',
+ 'Tech Broiler' => 'blog/perlow',
+ 'The SANMAN' => 'blog/the-sanman',
+ 'The open source revolution' => 'blog/the-open-source-revolution',
+ 'The German View' => 'blog/german',
+ 'The Ed Bott Report' => 'blog/bott',
+ 'The Mobile Gadgeteer' => 'blog/mobile-gadgeteer',
+ 'The Apple Core' => 'blog/apple',
+ 'Tom Foremski: IMHO' => 'blog/foremski',
+ 'Twisted Wire' => 'blog/twisted-wire',
+ 'Vive la tech' => 'blog/france',
+ 'Virtually Speaking' => 'blog/virtualization',
+ 'View from China' => 'blog/china',
+ 'Web design & Free Software' => 'blog/web-design-and-free-software',
+ 'ZDNet Government' => 'blog/government',
+ 'ZDNet UK Book Reviews' => 'blog/zdnet-uk-book-reviews',
+ 'ZDNet UK First Take' => 'blog/zdnet-uk-first-take',
+ 'Zero Day' => 'blog/security'
+ ),
+ 'ZDNet Hot Topics RSS:' => array(
+ 'Apple' => 'topic/apple',
+ 'Collaboration' => 'topic/collaboration',
+ 'Enterprise Software' => 'topic/enterprise-software',
+ 'Google' => 'topic/google',
+ 'Great debate' => 'topic/great-debate',
+ 'Hardware' => 'topic/hardware',
+ 'IBM' => 'topic/ibm',
+ 'iOS' => 'topic/ios',
+ 'iPhone' => 'topic/iphone',
+ 'iPad' => 'topic/ipad',
+ 'IT Priorities' => 'topic/it-priorities',
+ 'Laptops' => 'topic/laptops',
+ 'Legal' => 'topic/legal',
+ 'Linux' => 'topic/linux',
+ 'Microsoft' => 'topic/microsoft',
+ 'Mobile OS' => 'topic/mobile-os',
+ 'Mobility' => 'topic/mobility',
+ 'Networking' => 'topic/networking',
+ 'Oracle' => 'topic/oracle',
+ 'Processors' => 'topic/processors',
+ 'Samsung' => 'topic/samsung',
+ 'Security' => 'topic/security',
+ 'Small business: going big on mobility' => 'topic/small-business-going-big-on-mobility'
+ ),
+ 'Product Blogs:' => array(
+ 'Digital Cameras & Camcorders' => 'blog/digitalcameras',
+ 'Home Theater' => 'blog/home-theater',
+ 'Laptops and Desktops' => 'blog/computers',
+ 'The Mobile Gadgeteer' => 'blog/mobile-gadgeteer',
+ 'Smartphones and Cell Phones' => 'blog/cell-phones',
+ 'The ToyBox' => 'blog/gadgetreviews'
+ ),
+ 'Vertical Blogs:' => array(
+ 'ZDNet Education' => 'blog/education',
+ 'ZDNet Healthcare' => 'blog/healthcare',
+ 'ZDNet Government' => 'blog/government'
+ )
+ )
+ )
+ ));
+
+ public function collectData(){
+
+ function stripCdata($string){
+ $string = str_replace('<![CDATA[', '', $string);
+ $string = str_replace(']]>', '', $string);
+ return trim($string);
+ }
+
+ function extractFromDelimiters($string, $start, $end){
+ if(strpos($string, $start) !== false) {
+ $section_retrieved = substr($string, strpos($string, $start) + strlen($start));
+ $section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
+ return $section_retrieved;
+ }
+
+ return false;
+ }
+
+ function stripWithDelimiters($string, $start, $end){
+ while(strpos($string, $start) !== false) {
+ $section_to_remove = substr($string, strpos($string, $start));
+ $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end));
+ $string = str_replace($section_to_remove, '', $string);
+ }
+
+ return $string;
+ }
+
+ function stripRecursiveHtmlSection($string, $tag_name, $tag_start){
+ $open_tag = '<' . $tag_name;
+ $close_tag = '</' . $tag_name . '>';
+ $close_tag_length = strlen($close_tag);
+ if(strpos($tag_start, $open_tag) === 0) {
+ while(strpos($string, $tag_start) !== false) {
+ $max_recursion = 100;
+ $section_to_remove = null;
+ $section_start = strpos($string, $tag_start);
+ $search_offset = $section_start;
+ do {
+ $max_recursion--;
+ $section_end = strpos($string, $close_tag, $search_offset);
+ $search_offset = $section_end + $close_tag_length;
+ $section_to_remove = substr(
+ $string,
+ $section_start,
+ $section_end - $section_start + $close_tag_length
+ );
+
+ $open_tag_count = substr_count($section_to_remove, $open_tag);
+ $close_tag_count = substr_count($section_to_remove, $close_tag);
+ } while ($open_tag_count > $close_tag_count && $max_recursion > 0);
+ $string = str_replace($section_to_remove, '', $string);
+ }
+ }
+ return $string;
+ }
+
+ $baseUri = self::URI;
+ $feed = $this->getInput('feed');
+ if(strpos($feed, 'downloads!') !== false) {
+ $feed = str_replace('downloads!', '', $feed);
+ $baseUri = str_replace('www.', 'downloads.', $baseUri);
+ }
+ $url = $baseUri . trim($feed, '/') . '/rss.xml';
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Could not request ZDNet: ' . $url);
+ $limit = 0;
+
+ foreach($html->find('item') as $element) {
+ if($limit < 10) {
+ $article_url = preg_replace(
+ '/([^#]+)#ftag=.*/',
+ '$1',
+ stripCdata(extractFromDelimiters($element->innertext, '<link>', '</link>'))
+ );
+
+ $article_author = stripCdata(extractFromDelimiters($element->innertext, 'role="author">', '<'));
+ $article_title = stripCdata($element->find('title', 0)->plaintext);
+ $article_subtitle = stripCdata($element->find('description', 0)->plaintext);
+ $article_timestamp = strtotime(stripCdata($element->find('pubDate', 0)->plaintext));
+ $article = getSimpleHTMLDOM($article_url)
+ or returnServerError('Could not request ZDNet: ' . $article_url);
+
+ if(!empty($article_author)) {
+ $author = $article_author;
+ } else {
+ $author = $article->find('meta[name=author]', 0);
+ if(is_object($author)) {
+ $author = $author->content;
+ } else {
+ $author = 'ZDNet';
+ }
+ }
+
+ $thumbnail = $article->find('meta[itemprop=image]', 0);
+ if(is_object($thumbnail)) {
+ $thumbnail = $thumbnail->content;
+ } else {
+ $thumbnail = '';
+ }
+
+ $contents = $article->find('article', 0)->innertext;
+ foreach(array(
+ '<div class="shareBar"',
+ '<div class="shortcodeGalleryWrapper"',
+ '<div class="relatedContent',
+ '<div class="downloadNow',
+ '<div data-shortcode',
+ '<div id="sharethrough',
+ '<div id="inpage-video'
+ ) as $div_start) {
+ $contents = stripRecursiveHtmlSection($contents, 'div', $div_start);
+ }
+ $contents = stripWithDelimiters($contents, '<script', '</script>');
+ $contents = stripWithDelimiters($contents, '<meta itemprop="image"', '>');
+ $contents = trim(stripWithDelimiters($contents, '<section class="sharethrough-top', '</section>'));
+ $content_img = strpos($contents, '<img'); //Look for first image
+ if (($content_img !== false && $content_img < 512) || $thumbnail == '') {
+ $content_img = ''; //Image already present on article beginning or no thumbnail
+ } else {
+ $content_img = '<p><img src="'.$thumbnail.'" /></p>'; //Include thumbnail
+ }
+ $contents = $content_img
+ . '<p><b>'
+ . $article_subtitle
+ . '</b></p>'
+ . $contents;
+
+ $item = array();
+ $item['author'] = $author;
+ $item['uri'] = $article_url;
+ $item['title'] = $article_title;
+ $item['timestamp'] = $article_timestamp;
+ $item['content'] = $contents;
+ $this->items[] = $item;
+ $limit++;
+ }
+ }
+
+ }
+}
diff --git a/bridges/ZenodoBridge.php b/bridges/ZenodoBridge.php
new file mode 100644
index 0000000..18ef91c
--- /dev/null
+++ b/bridges/ZenodoBridge.php
@@ -0,0 +1,55 @@
+<?php
+class ZenodoBridge extends BridgeAbstract {
+ const MAINTAINER = 'theradialactive';
+ const NAME = 'Zenodo';
+ const URI = 'https://zenodo.org';
+ const CACHE_TIMEOUT = 10;
+ const DESCRIPTION = 'Returns the newest content of Zenodo';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('zenodo.org not reachable.');
+
+ foreach($html->find('div.record-elem') as $element) {
+ $item = array();
+ $item['uri'] = self::URI . $element->find('h4', 0)->find('a', 0)->href;
+ $item['title'] = trim(
+ htmlspecialchars_decode($element->find('h4', 0)->find('a', 0)->innertext,
+ ENT_QUOTES
+ )
+ );
+ foreach($element->find('p', 0)->find('span') as $authors) {
+ $item['author'] = $item['author'] . $authors . '; ';
+ }
+ $content = $element->find('p.hidden-xs', 0)->find('a', 0)->innertext . '<br>';
+ $type = '<br>Type: ' . $element->find('span.label-default', 0)->innertext;
+
+ $raw_date = $element->find('small.text-muted', 0)->innertext;
+ $clean_date = date_parse(str_replace('Uploaded on ', '', $raw_date));
+
+ $content = $content . date_parse($clean_date);
+
+ $item['timestamp'] = mktime(
+ $clean_date['hour'],
+ $clean_date['minute'],
+ $clean_date['second'],
+ $clean_date['month'],
+ $clean_date['day'],
+ $clean_date['year']
+ );
+
+ $access = '';
+ if ($element->find('span.label-success', 0)->innertext) {
+ $access = 'Open Access';
+ } elseif ($element->find('span.label-warning', 0)->innertext) {
+ $access = 'Embargoed Access';
+ } else {
+ $access = $element->find('span.label-error', 0)->innertext;
+ }
+ $access = '<br>Access: ' . $access;
+ $publication = '<br>Publication Date: ' . $element->find('span.label-info', 0)->innertext;
+ $item['content'] = $content . $type . $access . $publication;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/cache/.gitkeep b/cache/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cache/.gitkeep
diff --git a/caches/FileCache.php b/caches/FileCache.php
new file mode 100644
index 0000000..de17d52
--- /dev/null
+++ b/caches/FileCache.php
@@ -0,0 +1,121 @@
+<?php
+/**
+* Cache with file system
+*/
+class FileCache implements CacheInterface {
+
+ protected $path;
+ protected $param;
+
+ public function loadData(){
+ if(file_exists($this->getCacheFile())) {
+ return unserialize(file_get_contents($this->getCacheFile()));
+ }
+ }
+
+ public function saveData($datas){
+ // Notice: We use plain serialize() here to reduce memory footprint on
+ // large input data.
+ $writeStream = file_put_contents($this->getCacheFile(), serialize($datas));
+
+ if($writeStream === false) {
+ throw new \Exception('Cannot write the cache... Do you have the right permissions ?');
+ }
+
+ return $this;
+ }
+
+ public function getTime(){
+ $cacheFile = $this->getCacheFile();
+ if(file_exists($cacheFile)) {
+ return filemtime($cacheFile);
+ }
+
+ return false;
+ }
+
+ public function purgeCache($duration){
+ $cachePath = $this->getPath();
+ if(file_exists($cachePath)) {
+ $cacheIterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($cachePath),
+ RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach($cacheIterator as $cacheFile) {
+ if(in_array($cacheFile->getBasename(), array('.', '..', '.gitkeep')))
+ continue;
+ elseif($cacheFile->isFile()) {
+ if(filemtime($cacheFile->getPathname()) < time() - $duration)
+ unlink($cacheFile->getPathname());
+ }
+ }
+ }
+ }
+
+ /**
+ * Set cache path
+ * @return self
+ */
+ public function setPath($path){
+ if(is_null($path) || !is_string($path)) {
+ throw new \Exception('The given path is invalid!');
+ }
+
+ $this->path = $path;
+
+ // Make sure path ends with '/' or '\'
+ $lastchar = substr($this->path, -1, 1);
+ if($lastchar !== '/' && $lastchar !== '\\')
+ $this->path .= '/';
+
+ if(!is_dir($this->path))
+ mkdir($this->path, 0755, true);
+
+ return $this;
+ }
+
+ /**
+ * Set HTTP GET parameters
+ * @return self
+ */
+ public function setParameters(array $param){
+ $this->param = array_map('strtolower', $param);
+
+ return $this;
+ }
+
+ /**
+ * Return cache path (and create if not exist)
+ * @return string Cache path
+ */
+ protected function getPath(){
+ if(is_null($this->path)) {
+ throw new \Exception('Call "setPath" first!');
+ }
+
+ return $this->path;
+ }
+
+ /**
+ * Get the file name use for cache store
+ * @return string Path to the file cache
+ */
+ protected function getCacheFile(){
+ return $this->getPath() . $this->getCacheName();
+ }
+
+ /**
+ * Determines file name for store the cache
+ * return string
+ */
+ protected function getCacheName(){
+ if(is_null($this->param)) {
+ throw new \Exception('Call "setParameters" first!');
+ }
+
+ // Change character when making incompatible changes to prevent loading
+ // errors due to incompatible file contents \|/
+ return hash('md5', http_build_query($this->param) . 'A') . '.cache';
+ }
+}
diff --git a/config.default.ini.php b/config.default.ini.php
new file mode 100644
index 0000000..5909ad8
--- /dev/null
+++ b/config.default.ini.php
@@ -0,0 +1,44 @@
+; <?php exit; ?> DO NOT REMOVE THIS LINE
+
+; This file contains the default settings for RSS-Bridge. Do not change this
+; file, it will be replaced on the next update of RSS-Bridge! You can specify
+; your own configuration in 'config.ini.php' (copy this file).
+
+[cache]
+
+; Allow users to specify custom timeout for specific requests.
+; true = enabled
+; false = disabled (default)
+custom_timeout = false
+
+[proxy]
+
+; Sets the proxy url (i.e. "tcp://192.168.0.0:32")
+; "" = Proxy disabled (default)
+url = ""
+
+; Sets the proxy name that is shown on the bridge instead of the proxy url.
+; "" = Show proxy url
+name = "Hidden proxy name"
+
+; Allow users to disable proxy usage for specific requests.
+; true = enabled
+; false = disabled (default)
+by_bridge = false
+
+[authentication]
+
+; Enables authentication for all requests to this RSS-Bridge instance.
+;
+; Warning: You'll have to upgrade existing feeds after enabling this option!
+;
+; true = enabled
+; false = disabled (default)
+enable = false
+
+; The username for authentication. Insert this name when prompted for login.
+username = ""
+
+; The password for authentication. Insert this password when prompted for login.
+; Use a strong password to prevent others from guessing your login!
+password = ""
diff --git a/formats/AtomFormat.php b/formats/AtomFormat.php
new file mode 100644
index 0000000..9bd08bd
--- /dev/null
+++ b/formats/AtomFormat.php
@@ -0,0 +1,107 @@
+<?php
+/**
+* Atom
+* Documentation Source http://en.wikipedia.org/wiki/Atom_%28standard%29 and
+* http://tools.ietf.org/html/rfc4287
+*/
+class AtomFormat extends FormatAbstract{
+
+ public function stringify(){
+ $https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 's' : '';
+ $httpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
+ $httpInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
+
+ $serverRequestUri = $this->xml_encode($_SERVER['REQUEST_URI']);
+
+ $extraInfos = $this->getExtraInfos();
+ $title = $this->xml_encode($extraInfos['name']);
+ $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : 'https://github.com/RSS-Bridge/rss-bridge';
+
+ $uriparts = parse_url($uri);
+ $icon = $this->xml_encode($uriparts['scheme'] . '://' . $uriparts['host'] .'/favicon.ico');
+
+ $uri = $this->xml_encode($uri);
+
+ $entries = '';
+ foreach($this->getItems() as $item) {
+ $entryAuthor = isset($item['author']) ? $this->xml_encode($item['author']) : '';
+ $entryTitle = isset($item['title']) ? $this->xml_encode($item['title']) : '';
+ $entryUri = isset($item['uri']) ? $this->xml_encode($item['uri']) : '';
+ $entryTimestamp = isset($item['timestamp']) ? $this->xml_encode(date(DATE_ATOM, $item['timestamp'])) : '';
+ $entryContent = isset($item['content']) ? $this->xml_encode($this->sanitizeHtml($item['content'])) : '';
+
+ $entryEnclosures = '';
+ if(isset($item['enclosures'])) {
+ foreach($item['enclosures'] as $enclosure) {
+ $entryEnclosures .= '<link rel="enclosure" href="'
+ . $this->xml_encode($enclosure)
+ . '"/>'
+ . PHP_EOL;
+ }
+ }
+
+ $entryCategories = '';
+ if(isset($item['categories'])) {
+ foreach($item['categories'] as $category) {
+ $entryCategories .= '<category term="'
+ . $this->xml_encode($category)
+ . '"/>'
+ . PHP_EOL;
+ }
+ }
+
+ $entries .= <<<EOD
+
+ <entry>
+ <author>
+ <name>{$entryAuthor}</name>
+ </author>
+ <title type="html">{$entryTitle}</title>
+ <link rel="alternate" type="text/html" href="{$entryUri}" />
+ <id>{$entryUri}</id>
+ <updated>{$entryTimestamp}</updated>
+ <content type="html">{$entryContent}</content>
+ {$entryEnclosures}
+ {$entryCategories}
+ </entry>
+
+EOD;
+ }
+
+ $feedTimestamp = date(DATE_ATOM, time());
+ $charset = $this->getCharset();
+
+ /* Data are prepared, now let's begin the "MAGIE !!!" */
+ $toReturn = <<<EOD
+<?xml version="1.0" encoding="{$charset}"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0">
+
+ <title type="text">{$title}</title>
+ <id>http{$https}://{$httpHost}{$httpInfo}/</id>
+ <icon>{$icon}</icon>
+ <logo>{$icon}</logo>
+ <updated>{$feedTimestamp}</updated>
+ <link rel="alternate" type="text/html" href="{$uri}" />
+ <link rel="self" href="http{$https}://{$httpHost}{$serverRequestUri}" />
+{$entries}
+</feed>
+EOD;
+
+ // Remove invalid characters
+ ini_set('mbstring.substitute_character', 'none');
+ $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
+ return $toReturn;
+ }
+
+ public function display(){
+ $this
+ ->setContentType('application/atom+xml; charset=' . $this->getCharset())
+ ->callContentType();
+
+ return parent::display();
+ }
+
+ private function xml_encode($text){
+ return htmlspecialchars($text, ENT_XML1);
+ }
+}
diff --git a/formats/HtmlFormat.php b/formats/HtmlFormat.php
new file mode 100644
index 0000000..c4c5012
--- /dev/null
+++ b/formats/HtmlFormat.php
@@ -0,0 +1,115 @@
+<?php
+class HtmlFormat extends FormatAbstract {
+
+ public function stringify(){
+ $extraInfos = $this->getExtraInfos();
+ $title = htmlspecialchars($extraInfos['name']);
+ $uri = htmlspecialchars($extraInfos['uri']);
+ $atomquery = str_replace('format=Html', 'format=Atom', htmlentities($_SERVER['QUERY_STRING']));
+ $mrssquery = str_replace('format=Html', 'format=Mrss', htmlentities($_SERVER['QUERY_STRING']));
+
+ $entries = '';
+ foreach($this->getItems() as $item) {
+ $entryAuthor = isset($item['author']) ? '<br /><p class="author">by: ' . $item['author'] . '</p>' : '';
+ $entryTitle = isset($item['title']) ? $this->sanitizeHtml(strip_tags($item['title'])) : '';
+ $entryUri = isset($item['uri']) ? $item['uri'] : $uri;
+
+ $entryTimestamp = '';
+ if(isset($item['timestamp'])) {
+ $entryTimestamp = '<time datetime="'
+ . date(DATE_ATOM, $item['timestamp'])
+ . '">'
+ . date(DATE_ATOM, $item['timestamp'])
+ . '</time>';
+ }
+
+ $entryContent = '';
+ if(isset($item['content'])) {
+ $entryContent = '<div class="content">'
+ . $this->sanitizeHtml($item['content'])
+ . '</div>';
+ }
+
+ $entryEnclosures = '';
+ if(isset($item['enclosures'])) {
+ $entryEnclosures = '<div class="attachments"><p>Attachments:</p>';
+
+ foreach($item['enclosures'] as $enclosure) {
+ $url = $this->sanitizeHtml($enclosure);
+
+ $entryEnclosures .= '<li class="enclosure"><a href="'
+ . $url
+ . '">'
+ . substr($url, strrpos($url, '/') + 1)
+ . '</a></li>';
+ }
+
+ $entryEnclosures .= '</div>';
+ }
+
+ $entryCategories = '';
+ if(isset($item['categories'])) {
+ $entryCategories = '<div class="categories"><p>Categories:</p>';
+
+ foreach($item['categories'] as $category) {
+
+ $entryCategories .= '<li class="category">'
+ . $this->sanitizeHtml($category)
+ . '</li>';
+ }
+
+ $entryCategories .= '</div>';
+ }
+
+ $entries .= <<<EOD
+
+<section class="feeditem">
+ <h2><a class="itemtitle" href="{$entryUri}">{$entryTitle}</a></h2>
+ {$entryTimestamp}
+ {$entryAuthor}
+ {$entryContent}
+ {$entryEnclosures}
+ {$entryCategories}
+</section>
+
+EOD;
+ }
+
+ $charset = $this->getCharset();
+
+ /* Data are prepared, now let's begin the "MAGIE !!!" */
+ $toReturn = <<<EOD
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="{$charset}">
+ <title>{$title}</title>
+ <link href="static/HtmlFormat.css" rel="stylesheet">
+ <meta name="robots" content="noindex, follow">
+</head>
+<body>
+ <h1 class="pagetitle"><a href="{$uri}" target="_blank">{$title}</a></h1>
+ <div class="buttons">
+ <a href="./#bridge-{$_GET['bridge']}"><button class="backbutton">← back to rss-bridge</button></a>
+ <a href="./?{$atomquery}"><button class="rss-feed">RSS feed (ATOM)</button></a>
+ <a href="./?{$mrssquery}"><button class="rss-feed">RSS feed (MRSS)</button></a>
+ </div>
+{$entries}
+</body>
+</html>
+EOD;
+
+ // Remove invalid characters
+ ini_set('mbstring.substitute_character', 'none');
+ $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
+ return $toReturn;
+ }
+
+ public function display() {
+ $this
+ ->setContentType('text/html; charset=' . $this->getCharset())
+ ->callContentType();
+
+ return parent::display();
+ }
+}
diff --git a/formats/JsonFormat.php b/formats/JsonFormat.php
new file mode 100644
index 0000000..a60601a
--- /dev/null
+++ b/formats/JsonFormat.php
@@ -0,0 +1,25 @@
+<?php
+/**
+* Json
+* Builds a JSON string from $this->items and return it to browser.
+*/
+class JsonFormat extends FormatAbstract {
+
+ public function stringify(){
+ $items = $this->getItems();
+ $toReturn = json_encode($items, JSON_PRETTY_PRINT);
+
+ // Remove invalid non-UTF8 characters
+ ini_set('mbstring.substitute_character', 'none');
+ $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
+ return $toReturn;
+ }
+
+ public function display(){
+ $this
+ ->setContentType('application/json; charset=' . $this->getCharset())
+ ->callContentType();
+
+ return parent::display();
+ }
+}
diff --git a/formats/MrssFormat.php b/formats/MrssFormat.php
new file mode 100644
index 0000000..6d07928
--- /dev/null
+++ b/formats/MrssFormat.php
@@ -0,0 +1,118 @@
+<?php
+/**
+* Mrss
+* Documentation Source http://www.rssboard.org/media-rss
+*/
+class MrssFormat extends FormatAbstract {
+
+ public function stringify(){
+ $https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 's' : '';
+ $httpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
+ $httpInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
+
+ $serverRequestUri = $this->xml_encode($_SERVER['REQUEST_URI']);
+
+ $extraInfos = $this->getExtraInfos();
+ $title = $this->xml_encode($extraInfos['name']);
+
+ if(!empty($extraInfos['uri'])) {
+ $uri = $this->xml_encode($extraInfos['uri']);
+ } else {
+ $uri = 'https://github.com/RSS-Bridge/rss-bridge';
+ }
+
+ $uriparts = parse_url($uri);
+ $icon = $this->xml_encode($uriparts['scheme'] . '://' . $uriparts['host'] .'/favicon.ico');
+
+ $items = '';
+ foreach($this->getItems() as $item) {
+ $itemAuthor = isset($item['author']) ? $this->xml_encode($item['author']) : '';
+ $itemTitle = strip_tags(isset($item['title']) ? $this->xml_encode($item['title']) : '');
+ $itemUri = isset($item['uri']) ? $this->xml_encode($item['uri']) : '';
+ $itemTimestamp = isset($item['timestamp']) ? $this->xml_encode(date(DATE_RFC2822, $item['timestamp'])) : '';
+ $itemContent = isset($item['content']) ? $this->xml_encode($this->sanitizeHtml($item['content'])) : '';
+
+ $entryEnclosuresWarning = '';
+ $entryEnclosures = '';
+ if(isset($item['enclosures'])) {
+ $entryEnclosures .= '<enclosure url="'
+ . $this->xml_encode($item['enclosures'][0])
+ . '"/>';
+
+ if(count($item['enclosures']) > 1) {
+ $entryEnclosures .= PHP_EOL;
+ $entryEnclosuresWarning = '&lt;br&gt;Warning:
+Some media files might not be shown to you. Consider using the ATOM format instead!';
+ foreach($item['enclosures'] as $enclosure) {
+ $entryEnclosures .= '<atom:link rel="enclosure" href="'
+ . $enclosure . '" />'
+ . PHP_EOL;
+ }
+ }
+ }
+
+ $entryCategories = '';
+ if(isset($item['categories'])) {
+
+ foreach($item['categories'] as $category) {
+ $entryCategories .= '<category>'
+ . $category . '</category>'
+ . PHP_EOL;
+ }
+ }
+
+ $items .= <<<EOD
+
+ <item>
+ <title>{$itemTitle}</title>
+ <link>{$itemUri}</link>
+ <guid isPermaLink="true">{$itemUri}</guid>
+ <pubDate>{$itemTimestamp}</pubDate>
+ <description>{$itemContent}{$entryEnclosuresWarning}</description>
+ <author>{$itemAuthor}</author>
+ {$entryEnclosures}
+ {$entryCategories}
+ </item>
+
+EOD;
+ }
+
+ $charset = $this->getCharset();
+
+ /* Data are prepared, now let's begin the "MAGIE !!!" */
+ $toReturn = <<<EOD
+<?xml version="1.0" encoding="{$charset}"?>
+<rss version="2.0"
+xmlns:dc="http://purl.org/dc/elements/1.1/"
+xmlns:media="http://search.yahoo.com/mrss/"
+xmlns:atom="http://www.w3.org/2005/Atom">
+ <channel>
+ <title>{$title}</title>
+ <link>http{$https}://{$httpHost}{$httpInfo}/</link>
+ <description>{$title}</description>
+ <image url="{$icon}" title="{$title}" link="{$uri}"/>
+ <atom:link rel="alternate" type="text/html" href="{$uri}" />
+ <atom:link rel="self" href="http{$https}://{$httpHost}{$serverRequestUri}" />
+ {$items}
+ </channel>
+</rss>
+EOD;
+
+ // Remove invalid non-UTF8 characters
+ ini_set('mbstring.substitute_character', 'none');
+ $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
+ return $toReturn;
+ }
+
+ public function display(){
+ $this
+ ->setContentType('application/rss+xml; charset=' . $this->getCharset())
+ ->callContentType();
+
+ return parent::display();
+ }
+
+ private function xml_encode($text){
+ return htmlspecialchars($text, ENT_XML1);
+ }
+}
diff --git a/formats/PlaintextFormat.php b/formats/PlaintextFormat.php
new file mode 100644
index 0000000..c9691d0
--- /dev/null
+++ b/formats/PlaintextFormat.php
@@ -0,0 +1,25 @@
+<?php
+/**
+* Plaintext
+* Returns $this->items as raw php data.
+*/
+class PlaintextFormat extends FormatAbstract {
+
+ public function stringify(){
+ $items = $this->getItems();
+ $toReturn = print_r($items, true);
+
+ // Remove invalid non-UTF8 characters
+ ini_set('mbstring.substitute_character', 'none');
+ $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
+ return $toReturn;
+ }
+
+ public function display(){
+ $this
+ ->setContentType('text/plain; charset=' . $this->getCharset())
+ ->callContentType();
+
+ return parent::display();
+ }
+}
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..4a1e2aa
--- /dev/null
+++ b/index.php
@@ -0,0 +1,270 @@
+<?php
+require_once __DIR__ . '/lib/RssBridge.php';
+
+define('PHP_VERSION_REQUIRED', '5.6.0');
+
+// Specify directory for cached files (using FileCache)
+define('CACHE_DIR', __DIR__ . '/cache');
+
+// Specify path for whitelist file
+define('WHITELIST_FILE', __DIR__ . '/whitelist.txt');
+
+Configuration::verifyInstallation();
+Configuration::loadConfiguration();
+
+Authentication::showPromptIfNeeded();
+
+date_default_timezone_set('UTC');
+error_reporting(0);
+
+/*
+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 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_whitelist = trim(file_get_contents('DEBUG'));
+
+ $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);
+ define('DEBUG', true);
+ }
+}
+
+// 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)';
+$userAgent .= ' Gecko/20121202 Firefox/30.0(rss-bridge/0.1;';
+$userAgent .= '+https://github.com/RSS-Bridge/rss-bridge)';
+
+ini_set('user_agent', $userAgent);
+
+// default whitelist
+$whitelist_default = array(
+ 'BandcampBridge',
+ 'CryptomeBridge',
+ 'DansTonChatBridge',
+ 'DuckDuckGoBridge',
+ 'FacebookBridge',
+ 'FlickrExploreBridge',
+ 'GooglePlusPostBridge',
+ 'GoogleSearchBridge',
+ 'IdenticaBridge',
+ 'InstagramBridge',
+ 'OpenClassroomsBridge',
+ 'PinterestBridge',
+ 'ScmbBridge',
+ 'TwitterBridge',
+ 'WikipediaBridge',
+ 'YoutubeBridge');
+
+try {
+
+ Bridge::setDir(__DIR__ . '/bridges/');
+ Format::setDir(__DIR__ . '/formats/');
+ Cache::setDir(__DIR__ . '/caches/');
+
+ if(!file_exists(WHITELIST_FILE)) {
+ $whitelist_selection = $whitelist_default;
+ $whitelist_write = implode("\n", $whitelist_default);
+ file_put_contents(WHITELIST_FILE, $whitelist_write);
+ } else {
+
+ $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 = 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
+ // this is to keep compatibility until futher complete removal
+ if(($pos = strpos($bridge, 'Bridge')) === (strlen($bridge) - strlen('Bridge'))) {
+ $bridge = substr($bridge, 0, $pos);
+ }
+
+ $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
+ if(($pos = strpos($format, 'Format')) === (strlen($format) - strlen('Format'))) {
+ $format = substr($format, 0, $pos);
+ }
+
+ // whitelist control
+ if(!Bridge::isWhitelisted($whitelist_selection, strtolower($bridge))) {
+ throw new \HttpException('This bridge is not whitelisted', 401);
+ die;
+ }
+
+ // Data retrieval
+ $bridge = Bridge::create($bridge);
+
+ $noproxy = array_key_exists('_noproxy', $params) && filter_var($params['_noproxy'], FILTER_VALIDATE_BOOLEAN);
+ if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
+ define('NOPROXY', true);
+ }
+
+ // Custom cache timeout
+ $cache_timeout = -1;
+ if(array_key_exists('_cache_timeout', $params)) {
+ if(!CUSTOM_CACHE_TIMEOUT) {
+ throw new \HttpException('This server doesn\'t support "_cache_timeout"!');
+ }
+
+ $cache_timeout = filter_var($params['_cache_timeout'], FILTER_VALIDATE_INT);
+ }
+
+ // Initialize cache
+ $cache = Cache::create('FileCache');
+ $cache->setPath(CACHE_DIR);
+ $cache->purgeCache(86400); // 24 hours
+ $cache->setParameters($params);
+
+ unset($params['action']);
+ unset($params['bridge']);
+ unset($params['format']);
+ unset($params['_noproxy']);
+ unset($params['_cache_timeout']);
+
+ // Load cache & data
+ try {
+ $bridge->setCache($cache);
+ $bridge->setCacheTimeout($cache_timeout);
+ $bridge->setDatas($params);
+ } catch(Error $e) {
+ http_response_code($e->getCode());
+ header('Content-Type: text/html');
+ die(buildBridgeException($e, $bridge));
+ } catch(Exception $e) {
+ http_response_code($e->getCode());
+ header('Content-Type: text/html');
+ die(buildBridgeException($e, $bridge));
+ }
+
+ // Data transformation
+ try {
+ $format = Format::create($format);
+ $format->setItems($bridge->getItems());
+ $format->setExtraInfos($bridge->getExtraInfos());
+ $format->display();
+ } catch(Error $e) {
+ http_response_code($e->getCode());
+ header('Content-Type: text/html');
+ die(buildTransformException($e, $bridge));
+ } catch(Exception $e) {
+ http_response_code($e->getCode());
+ header('Content-Type: text/html');
+ die(buildBridgeException($e, $bridge));
+ }
+
+ die;
+ }
+} catch(HttpException $e) {
+ http_response_code($e->getCode());
+ header('Content-Type: text/plain');
+ die($e->getMessage());
+} catch(\Exception $e) {
+ die($e->getMessage());
+}
+
+$formats = Format::searchInformation();
+
+?>
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta name="description" content="Rss-bridge" />
+ <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 {
+ display: none;
+ }
+ </style>
+ </noscript>
+</head>
+
+<body onload="search()">
+ <?php
+ $status = '';
+ if(defined('DEBUG') && DEBUG === true) {
+ $status .= 'debug mode active';
+ }
+
+ $query = filter_input(INPUT_GET, 'q');
+
+ echo <<<EOD
+ <header>
+ <h1>RSS-Bridge</h1>
+ <h2>·Reconnecting the Web·</h2>
+ <p class="status">{$status}</p>
+ </header>
+ <section class="searchbar">
+ <h3>Search</h3>
+ <input type="text" name="searchfield"
+ id="searchfield" placeholder="Enter the bridge you want to search for"
+ onchange="search()" onkeyup="search()" value="{$query}">
+ </section>
+
+EOD;
+
+ $activeFoundBridgeCount = 0;
+ $showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
+ $inactiveBridges = '';
+ $bridgeList = Bridge::listBridges();
+ foreach($bridgeList as $bridgeName) {
+ if(Bridge::isWhitelisted($whitelist_selection, strtolower($bridgeName))) {
+ echo displayBridgeCard($bridgeName, $formats);
+ $activeFoundBridgeCount++;
+ } elseif($showInactive) {
+ // inactive bridges
+ $inactiveBridges .= displayBridgeCard($bridgeName, $formats, false) . PHP_EOL;
+ }
+ }
+ echo $inactiveBridges;
+ ?>
+ <section class="footer">
+ <a href="https://github.com/RSS-Bridge/rss-bridge">RSS-Bridge ~ Public Domain</a><br />
+ <p class="version"> <?= Configuration::getVersion() ?> </p>
+ <?= $activeFoundBridgeCount; ?>/<?= count($bridgeList) ?> active bridges. <br />
+ <?php
+ if($activeFoundBridgeCount !== count($bridgeList)) {
+ // FIXME: This should be done in pure CSS
+ if(!$showInactive)
+ echo '<a href="?show_inactive=1"><button class="small">Show inactive bridges</button></a><br />';
+ else
+ echo '<a href="?show_inactive=0"><button class="small">Hide inactive bridges</button></a><br />';
+ }
+ ?>
+ </section>
+ </body>
+</html>
diff --git a/lib/Authentication.php b/lib/Authentication.php
new file mode 100644
index 0000000..dc75d28
--- /dev/null
+++ b/lib/Authentication.php
@@ -0,0 +1,31 @@
+<?php
+class Authentication {
+
+ public static function showPromptIfNeeded() {
+
+ if(Configuration::getConfig('authentication', 'enable') === true) {
+ if(!Authentication::verifyPrompt()) {
+ header('WWW-Authenticate: Basic realm="RSS-Bridge"');
+ header('HTTP/1.0 401 Unauthorized');
+ die('Please authenticate in order to access this instance !');
+ }
+
+ }
+
+ }
+
+ public static function verifyPrompt() {
+
+ if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
+ if(Configuration::getConfig('authentication', 'username') === $_SERVER['PHP_AUTH_USER']
+ && Configuration::getConfig('authentication', 'password') === $_SERVER['PHP_AUTH_PW']) {
+ return true;
+ } else {
+ error_log('[RSS-Bridge] Failed authentication attempt from ' . $_SERVER['REMOTE_ADDR']);
+ }
+ }
+ return false;
+
+ }
+
+}
diff --git a/lib/Bridge.php b/lib/Bridge.php
new file mode 100644
index 0000000..a2ae927
--- /dev/null
+++ b/lib/Bridge.php
@@ -0,0 +1,88 @@
+<?php
+require_once(__DIR__ . '/BridgeInterface.php');
+class Bridge {
+
+ static protected $dirBridge;
+
+ public function __construct(){
+ throw new \LogicException('Please use ' . __CLASS__ . '::create for new object.');
+ }
+
+ /**
+ * Create a new bridge object
+ * @param string $nameBridge Defined bridge name you want use
+ * @return Bridge object dedicated
+ */
+ static public function create($nameBridge){
+ if(!preg_match('@^[A-Z][a-zA-Z0-9-]*$@', $nameBridge)) {
+ $message = <<<EOD
+'nameBridge' must start with one uppercase character followed or not by
+alphanumeric or dash characters!
+EOD;
+ throw new \InvalidArgumentException($message);
+ }
+
+ $nameBridge = $nameBridge . 'Bridge';
+ $pathBridge = self::getDir() . $nameBridge . '.php';
+
+ if(!file_exists($pathBridge)) {
+ throw new \Exception('The bridge you looking for does not exist. It should be at path '
+ . $pathBridge);
+ }
+
+ require_once $pathBridge;
+
+ if((new ReflectionClass($nameBridge))->isInstantiable()) {
+ return new $nameBridge();
+ }
+
+ return false;
+ }
+
+ static public function setDir($dirBridge){
+ if(!is_string($dirBridge)) {
+ throw new \InvalidArgumentException('Dir bridge must be a string.');
+ }
+
+ if(!file_exists($dirBridge)) {
+ throw new \Exception('Dir bridge does not exist.');
+ }
+
+ self::$dirBridge = $dirBridge;
+ }
+
+ static public function getDir(){
+ if(is_null(self::$dirBridge)) {
+ throw new \LogicException(__CLASS__ . ' class need to know bridge path !');
+ }
+
+ return self::$dirBridge;
+ }
+
+ /**
+ * Lists the available bridges.
+ * @return array List of the bridges
+ */
+ static public function listBridges(){
+ $listBridge = array();
+ $dirFiles = scandir(self::getDir());
+
+ if($dirFiles !== false) {
+ foreach($dirFiles as $fileName) {
+ if(preg_match('@^([^.]+)Bridge\.php$@U', $fileName, $out)) {
+ $listBridge[] = $out[1];
+ }
+ }
+ }
+
+ return $listBridge;
+ }
+
+ static public function isWhitelisted($whitelist, $name){
+ 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]) === '*');
+ }
+}
diff --git a/lib/BridgeAbstract.php b/lib/BridgeAbstract.php
new file mode 100644
index 0000000..0bd1c7c
--- /dev/null
+++ b/lib/BridgeAbstract.php
@@ -0,0 +1,285 @@
+<?php
+require_once(__DIR__ . '/BridgeInterface.php');
+abstract class BridgeAbstract implements BridgeInterface {
+
+ const NAME = 'Unnamed bridge';
+ const URI = '';
+ const DESCRIPTION = 'No description provided';
+ const MAINTAINER = 'No maintainer';
+ const CACHE_TIMEOUT = 3600;
+ const PARAMETERS = array();
+
+ protected $cache;
+ protected $extraInfos;
+ protected $items = array();
+ protected $inputs = array();
+ protected $queriedContext = '';
+ protected $cacheTimeout;
+
+ /**
+ * Return cachable datas (extrainfos and items) stored in the bridge
+ * @return mixed
+ */
+ public function getCachable(){
+ return array(
+ 'items' => $this->getItems(),
+ 'extraInfos' => $this->getExtraInfos()
+ );
+ }
+
+ /**
+ * Return items stored in the bridge
+ * @return mixed
+ */
+ public function getItems(){
+ return $this->items;
+ }
+
+ /**
+ * Sets the input values for a given context. Existing values are
+ * overwritten.
+ *
+ * @param array $inputs Associative array of inputs
+ * @param string $context The context name
+ */
+ protected function setInputs(array $inputs, $queriedContext){
+ // Import and assign all inputs to their context
+ foreach($inputs as $name => $value) {
+ foreach(static::PARAMETERS as $context => $set) {
+ if(array_key_exists($name, static::PARAMETERS[$context])) {
+ $this->inputs[$context][$name]['value'] = $value;
+ }
+ }
+ }
+
+ // Apply default values to missing data
+ $contexts = array($queriedContext);
+ if(array_key_exists('global', static::PARAMETERS)) {
+ $contexts[] = 'global';
+ }
+
+ foreach($contexts as $context) {
+ foreach(static::PARAMETERS[$context] as $name => $properties) {
+ if(isset($this->inputs[$context][$name]['value'])) {
+ continue;
+ }
+
+ $type = isset($properties['type']) ? $properties['type'] : 'text';
+
+ switch($type) {
+ case 'checkbox':
+ if(!isset($properties['defaultValue'])) {
+ $this->inputs[$context][$name]['value'] = false;
+ } else {
+ $this->inputs[$context][$name]['value'] = $properties['defaultValue'];
+ }
+ break;
+ case 'list':
+ if(!isset($properties['defaultValue'])) {
+ $firstItem = reset($properties['values']);
+ if(is_array($firstItem)) {
+ $firstItem = reset($firstItem);
+ }
+ $this->inputs[$context][$name]['value'] = $firstItem;
+ } else {
+ $this->inputs[$context][$name]['value'] = $properties['defaultValue'];
+ }
+ break;
+ default:
+ if(isset($properties['defaultValue'])) {
+ $this->inputs[$context][$name]['value'] = $properties['defaultValue'];
+ }
+ break;
+ }
+ }
+ }
+
+ // Copy global parameter values to the guessed context
+ if(array_key_exists('global', static::PARAMETERS)) {
+ foreach(static::PARAMETERS['global'] as $name => $properties) {
+ if(isset($inputs[$name])) {
+ $value = $inputs[$name];
+ } elseif(isset($properties['value'])) {
+ $value = $properties['value'];
+ } else {
+ continue;
+ }
+ $this->inputs[$queriedContext][$name]['value'] = $value;
+ }
+ }
+
+ // Only keep guessed context parameters values
+ if(isset($this->inputs[$queriedContext])) {
+ $this->inputs = array($queriedContext => $this->inputs[$queriedContext]);
+ } else {
+ $this->inputs = array();
+ }
+ }
+
+ /**
+ * Returns the name of the context matching the provided inputs
+ *
+ * @param array $inputs Associative array of inputs
+ * @return mixed Returns the context name or null if no match was found
+ */
+ protected function getQueriedContext(array $inputs){
+ $queriedContexts = array();
+
+ // Detect matching context
+ foreach(static::PARAMETERS as $context => $set) {
+ $queriedContexts[$context] = null;
+
+ // Check if all parameters of the context are satisfied
+ foreach($set as $id => $properties) {
+ if(isset($inputs[$id]) && !empty($inputs[$id])) {
+ $queriedContexts[$context] = true;
+ } elseif(isset($properties['required'])
+ && $properties['required'] === true) {
+ $queriedContexts[$context] = false;
+ break;
+ }
+ }
+
+ }
+
+ // Abort if one of the globally required parameters is not satisfied
+ if(array_key_exists('global', static::PARAMETERS)
+ && $queriedContexts['global'] === false) {
+ return null;
+ }
+ unset($queriedContexts['global']);
+
+ switch(array_sum($queriedContexts)) {
+ case 0: // Found no match, is there a context without parameters?
+ foreach($queriedContexts as $context => $queried) {
+ if(is_null($queried)) {
+ return $context;
+ }
+ }
+ return null;
+ case 1: // Found unique match
+ return array_search(true, $queriedContexts);
+ default: return false;
+ }
+ }
+
+ /**
+ * Defined datas with parameters depending choose bridge
+ * Note : you can define a cache with "setCache"
+ * @param array array with expected bridge paramters
+ */
+ public function setDatas(array $inputs){
+ if(!is_null($this->cache)) {
+ $time = $this->cache->getTime();
+ if($time !== false
+ && (time() - $this->getCacheTimeout() < $time)
+ && (!defined('DEBUG') || DEBUG !== true)) {
+ $cached = $this->cache->loadData();
+ if(isset($cached['items']) && isset($cached['extraInfos'])) {
+ $this->items = $cached['items'];
+ $this->extraInfos = $cached['extraInfos'];
+ return;
+ }
+ }
+ }
+
+ if(empty(static::PARAMETERS)) {
+ if(!empty($inputs)) {
+ returnClientError('Invalid parameters value(s)');
+ }
+
+ $this->collectData();
+ if(!is_null($this->cache)) {
+ $this->cache->saveData($this->getCachable());
+ }
+ return;
+ }
+
+ if(!validateData($inputs, static::PARAMETERS)) {
+ returnClientError('Invalid parameters value(s)');
+ }
+
+ // Guess the paramter context from input data
+ $this->queriedContext = $this->getQueriedContext($inputs);
+ if(is_null($this->queriedContext)) {
+ returnClientError('Required parameter(s) missing');
+ } elseif($this->queriedContext === false) {
+ returnClientError('Mixed context parameters');
+ }
+
+ $this->setInputs($inputs, $this->queriedContext);
+
+ $this->collectData();
+
+ if(!is_null($this->cache)) {
+ $this->cache->saveData($this->getCachable());
+ }
+ }
+
+ /**
+ * Returns the value for the provided input
+ *
+ * @param string $input The input name
+ * @return mixed Returns the input value or null if the input is not defined
+ */
+ protected function getInput($input){
+ if(!isset($this->inputs[$this->queriedContext][$input]['value'])) {
+ return null;
+ }
+ return $this->inputs[$this->queriedContext][$input]['value'];
+ }
+
+ public function getDescription(){
+ return static::DESCRIPTION;
+ }
+
+ public function getMaintainer(){
+ return static::MAINTAINER;
+ }
+
+ public function getName(){
+ // Return cached name when bridge is using cached data
+ if(isset($this->extraInfos)) {
+ return $this->extraInfos['name'];
+ }
+
+ return static::NAME;
+ }
+
+ public function getParameters(){
+ return static::PARAMETERS;
+ }
+
+ public function getURI(){
+ // Return cached uri when bridge is using cached data
+ if(isset($this->extraInfos)) {
+ return $this->extraInfos['uri'];
+ }
+
+ return static::URI;
+ }
+
+ public function getExtraInfos(){
+ return array(
+ 'name' => $this->getName(),
+ 'uri' => $this->getURI()
+ );
+ }
+
+ public function setCache(\CacheInterface $cache){
+ $this->cache = $cache;
+ }
+
+ public function setCacheTimeout($timeout){
+ if(is_numeric($timeout) && ($timeout < 1 || $timeout > 86400)) {
+ $this->cacheTimeout = static::CACHE_TIMEOUT;
+ return;
+ }
+
+ $this->cacheTimeout = $timeout;
+ }
+
+ public function getCacheTimeout(){
+ return isset($this->cacheTimeout) ? $this->cacheTimeout : static::CACHE_TIMEOUT;
+ }
+}
diff --git a/lib/BridgeInterface.php b/lib/BridgeInterface.php
new file mode 100644
index 0000000..b8f5cf4
--- /dev/null
+++ b/lib/BridgeInterface.php
@@ -0,0 +1,87 @@
+<?php
+interface BridgeInterface {
+
+ /**
+ * Collects data from the site
+ */
+ public function collectData();
+
+ /**
+ * Returns an array of cachable elements
+ *
+ * @return array Associative array of cachable elements
+ */
+ public function getCachable();
+
+ /**
+ * Returns the description
+ *
+ * @return string Description
+ */
+ public function getDescription();
+
+ /**
+ * Return an array of extra information
+ *
+ * @return array Associative array of extra information
+ */
+ public function getExtraInfos();
+
+ /**
+ * Returns an array of collected items
+ *
+ * @return array Associative array of items
+ */
+ public function getItems();
+
+ /**
+ * Returns the bridge maintainer
+ *
+ * @return string Bridge maintainer
+ */
+ public function getMaintainer();
+
+ /**
+ * Returns the bridge name
+ *
+ * @return string Bridge name
+ */
+ public function getName();
+
+ /**
+ * Returns the bridge parameters
+ *
+ * @return array Bridge parameters
+ */
+ public function getParameters();
+
+ /**
+ * Returns the bridge URI
+ *
+ * @return string Bridge URI
+ */
+ public function getURI();
+
+ /**
+ * Sets the cache instance
+ *
+ * @param object CacheInterface The cache instance
+ */
+ public function setCache(\CacheInterface $cache);
+
+ /**
+ * Sets the timeout for clearing the cache files. The timeout must be
+ * specified between 1..86400 seconds (max. 24 hours). The default timeout
+ * (specified by the bridge maintainer) applies for invalid values.
+ *
+ * @param int $timeout The cache timeout in seconds
+ */
+ public function setCacheTimeout($timeout);
+
+ /**
+ * Returns the cache timeout
+ *
+ * @return int Cache timeout
+ */
+ public function getCacheTimeout();
+}
diff --git a/lib/Cache.php b/lib/Cache.php
new file mode 100644
index 0000000..bc17758
--- /dev/null
+++ b/lib/Cache.php
@@ -0,0 +1,53 @@
+<?php
+require_once(__DIR__ . '/CacheInterface.php');
+class Cache {
+
+ static protected $dirCache;
+
+ public function __construct(){
+ throw new \LogicException('Please use ' . __CLASS__ . '::create for new object.');
+ }
+
+ static public function create($nameCache){
+ if(!static::isValidNameCache($nameCache)) {
+ throw new \InvalidArgumentException('Name cache must be at least one
+ uppercase follow or not by alphanumeric or dash characters.');
+ }
+
+ $pathCache = self::getDir() . $nameCache . '.php';
+
+ if(!file_exists($pathCache)) {
+ throw new \Exception('The cache you looking for does not exist.');
+ }
+
+ require_once $pathCache;
+
+ return new $nameCache();
+ }
+
+ static public function setDir($dirCache){
+ if(!is_string($dirCache)) {
+ throw new \InvalidArgumentException('Dir cache must be a string.');
+ }
+
+ if(!file_exists($dirCache)) {
+ throw new \Exception('Dir cache does not exist.');
+ }
+
+ self::$dirCache = $dirCache;
+ }
+
+ static public function getDir(){
+ $dirCache = self::$dirCache;
+
+ if(is_null($dirCache)) {
+ throw new \LogicException(__CLASS__ . ' class need to know cache path !');
+ }
+
+ return $dirCache;
+ }
+
+ static public function isValidNameCache($nameCache){
+ return preg_match('@^[A-Z][a-zA-Z0-9-]*$@', $nameCache);
+ }
+}
diff --git a/lib/CacheInterface.php b/lib/CacheInterface.php
new file mode 100644
index 0000000..5753c0e
--- /dev/null
+++ b/lib/CacheInterface.php
@@ -0,0 +1,7 @@
+<?php
+interface CacheInterface {
+ public function loadData();
+ public function saveData($datas);
+ public function getTime();
+ public function purgeCache($duration);
+}
diff --git a/lib/Configuration.php b/lib/Configuration.php
new file mode 100644
index 0000000..620b0e4
--- /dev/null
+++ b/lib/Configuration.php
@@ -0,0 +1,120 @@
+<?php
+class Configuration {
+
+ public static $VERSION = '2018-07-17';
+
+ public static $config = null;
+
+ public static function verifyInstallation() {
+
+ // Check PHP version
+ if(version_compare(PHP_VERSION, PHP_VERSION_REQUIRED) === -1)
+ die('RSS-Bridge requires at least PHP version ' . PHP_VERSION_REQUIRED . '!');
+
+ // extensions check
+ if(!extension_loaded('openssl'))
+ die('"openssl" extension not loaded. Please check "php.ini"');
+
+ 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"');
+
+ if(!extension_loaded('simplexml'))
+ die('"simplexml" extension not loaded. Please check "php.ini"');
+
+ if(!extension_loaded('curl'))
+ die('"curl" extension not loaded. 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 . '!');
+
+ }
+
+ public static function loadConfiguration() {
+
+ if(!file_exists('config.default.ini.php'))
+ die('The default configuration file "config.default.ini.php" is missing!');
+
+ Configuration::$config = parse_ini_file('config.default.ini.php', true, INI_SCANNER_TYPED);
+ if(!Configuration::$config)
+ die('Error parsing config.default.ini.php');
+
+ if(file_exists('config.ini.php')) {
+ // Replace default configuration with custom settings
+ foreach(parse_ini_file('config.ini.php', true, INI_SCANNER_TYPED) as $header => $section) {
+ foreach($section as $key => $value) {
+ // Skip unknown sections and keys
+ if(array_key_exists($header, Configuration::$config) && array_key_exists($key, Configuration::$config[$header])) {
+ Configuration::$config[$header][$key] = $value;
+ }
+ }
+ }
+ }
+
+ if(!is_string(self::getConfig('proxy', 'url')))
+ die('Parameter [proxy] => "url" is not a valid string! Please check "config.ini.php"!');
+
+ if(!empty(self::getConfig('proxy', 'url')))
+ define('PROXY_URL', self::getConfig('proxy', 'url'));
+
+ if(!is_bool(self::getConfig('proxy', 'by_bridge')))
+ die('Parameter [proxy] => "by_bridge" is not a valid Boolean! Please check "config.ini.php"!');
+
+ define('PROXY_BYBRIDGE', self::getConfig('proxy', 'by_bridge'));
+
+ if(!is_string(self::getConfig('proxy', 'name')))
+ die('Parameter [proxy] => "name" is not a valid string! Please check "config.ini.php"!');
+
+ define('PROXY_NAME', self::getConfig('proxy', 'name'));
+
+ if(!is_bool(self::getConfig('cache', 'custom_timeout')))
+ die('Parameter [cache] => "custom_timeout" is not a valid Boolean! Please check "config.ini.php"!');
+
+ define('CUSTOM_CACHE_TIMEOUT', self::getConfig('cache', 'custom_timeout'));
+
+ if(!is_bool(self::getConfig('authentication', 'enable')))
+ die('Parameter [authentication] => "enable" is not a valid Boolean! Please check "config.ini.php"!');
+
+ if(!is_string(self::getConfig('authentication', 'username')))
+ die('Parameter [authentication] => "username" is not a valid string! Please check "config.ini.php"!');
+
+ if(!is_string(self::getConfig('authentication', 'password')))
+ die('Parameter [authentication] => "password" is not a valid string! Please check "config.ini.php"!');
+
+ }
+
+ public static function getConfig($category, $key) {
+
+ if(array_key_exists($category, self::$config) && array_key_exists($key, self::$config[$category])) {
+ return self::$config[$category][$key];
+ }
+
+ return null;
+
+ }
+
+ public static function getVersion() {
+
+ $headFile = '.git/HEAD';
+
+ if(file_exists($headFile)) {
+
+ $revisionHashFile = '.git/' . substr(file_get_contents($headFile), 5, -1);
+ $branchName = explode('/', $revisionHashFile)[3];
+ if(file_exists($revisionHashFile)) {
+ return 'git.' . $branchName . '.' . substr(file_get_contents($revisionHashFile), 0, 7);
+ }
+ }
+
+ return Configuration::$VERSION;
+
+ }
+
+}
diff --git a/lib/Exceptions.php b/lib/Exceptions.php
new file mode 100644
index 0000000..9b89320
--- /dev/null
+++ b/lib/Exceptions.php
@@ -0,0 +1,154 @@
+<?php
+class HttpException extends \Exception{}
+
+/**
+ * Returns an URL that automatically populates a new issue on GitHub based
+ * on the information provided
+ *
+ * @param $title string Sets the title of the issue
+ * @param $body string Sets the body of the issue (GitHub markdown applies)
+ * @param $labels mixed (optional) Specifies labels to add to the issue
+ * @param $maintainer string (optional) Specifies the maintainer for the issue.
+ * The maintainer only applies if part of the development team!
+ * @return string Returns a qualified URL to a new issue with populated conent.
+ * Returns null if title or body is null or empty
+ */
+function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null){
+ if(!isset($title) || !isset($body) || empty($title) || empty($body)) {
+ return null;
+ }
+
+ // Add title and body
+ $uri = 'https://github.com/rss-bridge/rss-bridge/issues/new?title='
+ . urlencode($title)
+ . '&body='
+ . urlencode($body);
+
+ // Add labels
+ if(!is_null($labels) && is_array($labels) && count($labels) > 0) {
+ if(count($lables) === 1) {
+ $uri .= '&labels=' . urlencode($labels[0]);
+ } else {
+ foreach($labels as $label) {
+ $uri .= '&labels[]=' . urlencode($label);
+ }
+ }
+ } elseif(!is_null($labels) && is_string($labels)) {
+ $uri .= '&labels=' . urlencode($labels);
+ }
+
+ // Add maintainer
+ if(!empty($maintainer)) {
+ $uri .= '&assignee=' . urlencode($maintainer);
+ }
+
+ return $uri;
+}
+
+/**
+ * Returns the exception message as HTML string
+ *
+ * @param $e Exception The exception to show
+ * @param $bridge object The bridge object
+ * @return string Returns the exception as HTML string. Returns null if the
+ * provided parameter are invalid
+ */
+function buildBridgeException($e, $bridge){
+ if(( !($e instanceof \Exception) && !($e instanceof \Error)) || !($bridge instanceof \BridgeInterface)) {
+ return null;
+ }
+
+ $title = $bridge->getName() . ' failed with error ' . $e->getCode();
+
+ // Build a GitHub compatible message
+ $body = 'Error message: `'
+ . $e->getMessage()
+ . "`\nQuery string: `"
+ . $_SERVER['QUERY_STRING']
+ . "`\nVersion: `"
+ . Configuration::getVersion()
+ . '`';
+
+ $link = buildGitHubIssueQuery($title, $body, 'bug report', $bridge->getMaintainer());
+
+ $header = buildHeader($e, $bridge);
+ $message = "<strong>{$bridge->getName()}</strong> was
+unable to receive or process the remote website's content!";
+ $section = buildSection($e, $bridge, $message, $link);
+
+ return buildPage($title, $header, $section);
+}
+
+/**
+ * Returns the exception message as HTML string
+ *
+ * @param $e Exception The exception to show
+ * @param $bridge object The bridge object
+ * @return string Returns the exception as HTML string. Returns null if the
+ * provided parameter are invalid
+ */
+function buildTransformException($e, $bridge){
+ if(( !($e instanceof \Exception) && !($e instanceof \Error)) || !($bridge instanceof \BridgeInterface)) {
+ return null;
+ }
+
+ $title = $bridge->getName() . ' failed with error ' . $e->getCode();
+
+ // Build a GitHub compatible message
+ $body = 'Error message: `'
+ . $e->getMessage()
+ . "`\nQuery string: `"
+ . $_SERVER['QUERY_STRING'] . '`';
+
+ $link = buildGitHubIssueQuery($title, $body, 'bug report', $bridge->getMaintainer());
+ $header = buildHeader($e, $bridge);
+ $message = "RSS-Bridge was unable to transform the contents returned by
+<strong>{$bridge->getName()}</strong>!";
+ $section = buildSection($e, $bridge, $message, $link);
+
+ return buildPage($title, $header, $section);
+}
+
+function buildHeader($e, $bridge){
+ return <<<EOD
+<header>
+ <h1>Error {$e->getCode()}</h1>
+ <h2>{$e->getMessage()}</h2>
+ <p class="status">{$bridge->getName()}</p>
+</header>
+EOD;
+}
+
+function buildSection($e, $bridge, $message, $link){
+ return <<<EOD
+<section>
+ <p class="exception-message">{$message}</p>
+ <div class="advice">
+ <ul class="advice">
+ <li>Press Return to check your input parameters</li>
+ <li>Press F5 to retry</li>
+ <li>Open a GitHub Issue if this error persists</li>
+ </ul>
+ </div>
+ <a href="{$link}" title="After clicking this button you can review
+ the issue before submitting it"><button>Open GitHub Issue</button></a>
+ <p class="maintainer">{$bridge->getMaintainer()}</p>
+</section>
+EOD;
+}
+
+function buildPage($title, $header, $section){
+ return <<<EOD
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <title>{$title}</title>
+ <link href="static/style.css" rel="stylesheet">
+</head>
+<body>
+ {$header}
+ {$section}
+</body>
+</html>
+EOD;
+}
diff --git a/lib/FeedExpander.php b/lib/FeedExpander.php
new file mode 100644
index 0000000..6e1f16f
--- /dev/null
+++ b/lib/FeedExpander.php
@@ -0,0 +1,208 @@
+<?php
+require_once(__DIR__ . '/BridgeInterface.php');
+abstract class FeedExpander extends BridgeAbstract {
+
+ private $name;
+ private $uri;
+ private $feedType;
+
+ public function collectExpandableDatas($url, $maxItems = -1){
+ if(empty($url)) {
+ returnServerError('There is no $url for this RSS expander');
+ }
+
+ debugMessage('Loading from ' . $url);
+
+ /* Notice we do not use cache here on purpose:
+ * we want a fresh view of the RSS stream each time
+ */
+ $content = getContents($url)
+ or returnServerError('Could not request ' . $url);
+ $rssContent = simplexml_load_string(trim($content));
+
+ debugMessage('Detecting feed format/version');
+ switch(true) {
+ case isset($rssContent->item[0]):
+ debugMessage('Detected RSS 1.0 format');
+ $this->feedType = 'RSS_1_0';
+ break;
+ case isset($rssContent->channel[0]):
+ debugMessage('Detected RSS 0.9x or 2.0 format');
+ $this->feedType = 'RSS_2_0';
+ break;
+ case isset($rssContent->entry[0]):
+ debugMessage('Detected ATOM format');
+ $this->feedType = 'ATOM_1_0';
+ break;
+ default:
+ debugMessage('Unknown feed format/version');
+ returnServerError('The feed format is unknown!');
+ break;
+ }
+
+ debugMessage('Calling function "collect_' . $this->feedType . '_data"');
+ $this->{'collect_' . $this->feedType . '_data'}($rssContent, $maxItems);
+ }
+
+ protected function collect_RSS_1_0_data($rssContent, $maxItems){
+ $this->load_RSS_2_0_feed_data($rssContent->channel[0]);
+ foreach($rssContent->item as $item) {
+ debugMessage('parsing item ' . var_export($item, true));
+ $tmp_item = $this->parseItem($item);
+ if (!empty($tmp_item)) {
+ $this->items[] = $tmp_item;
+ }
+ if($maxItems !== -1 && count($this->items) >= $maxItems) break;
+ }
+ }
+
+ protected function collect_RSS_2_0_data($rssContent, $maxItems){
+ $rssContent = $rssContent->channel[0];
+ debugMessage('RSS content is ===========\n'
+ . var_export($rssContent, true)
+ . '===========');
+
+ $this->load_RSS_2_0_feed_data($rssContent);
+ foreach($rssContent->item as $item) {
+ debugMessage('parsing item ' . var_export($item, true));
+ $tmp_item = $this->parseItem($item);
+ if (!empty($tmp_item)) {
+ $this->items[] = $tmp_item;
+ }
+ if($maxItems !== -1 && count($this->items) >= $maxItems) break;
+ }
+ }
+
+ protected function collect_ATOM_1_0_data($content, $maxItems){
+ $this->load_ATOM_feed_data($content);
+ foreach($content->entry as $item) {
+ debugMessage('parsing item ' . var_export($item, true));
+ $tmp_item = $this->parseItem($item);
+ if (!empty($tmp_item)) {
+ $this->items[] = $tmp_item;
+ }
+ if($maxItems !== -1 && count($this->items) >= $maxItems) break;
+ }
+ }
+
+ protected function RSS_2_0_time_to_timestamp($item){
+ return DateTime::createFromFormat('D, d M Y H:i:s e', $item->pubDate)->getTimestamp();
+ }
+
+ // TODO set title, link, description, language, and so on
+ protected function load_RSS_2_0_feed_data($rssContent){
+ $this->name = trim((string)$rssContent->title);
+ $this->uri = trim((string)$rssContent->link);
+ }
+
+ protected function load_ATOM_feed_data($content){
+ $this->name = (string)$content->title;
+
+ // Find best link (only one, or first of 'alternate')
+ if(!isset($content->link)) {
+ $this->uri = '';
+ } elseif (count($content->link) === 1) {
+ $this->uri = (string)$content->link[0]['href'];
+ } else {
+ $this->uri = '';
+ foreach($content->link as $link) {
+ if(strtolower($link['rel']) === 'alternate') {
+ $this->uri = (string)$link['href'];
+ break;
+ }
+ }
+ }
+ }
+
+ protected function parseATOMItem($feedItem){
+ $item = array();
+ if(isset($feedItem->id)) $item['uri'] = (string)$feedItem->id;
+ if(isset($feedItem->title)) $item['title'] = (string)$feedItem->title;
+ if(isset($feedItem->updated)) $item['timestamp'] = strtotime((string)$feedItem->updated);
+ if(isset($feedItem->author)) $item['author'] = (string)$feedItem->author->name;
+ if(isset($feedItem->content)) $item['content'] = (string)$feedItem->content;
+ return $item;
+ }
+
+ protected function parseRSS_0_9_1_Item($feedItem){
+ $item = array();
+ if(isset($feedItem->link)) $item['uri'] = (string)$feedItem->link;
+ if(isset($feedItem->title)) $item['title'] = (string)$feedItem->title;
+ // rss 0.91 doesn't support timestamps
+ // rss 0.91 doesn't support authors
+ if(isset($feedItem->description)) $item['content'] = (string)$feedItem->description;
+ return $item;
+ }
+
+ protected function parseRSS_1_0_Item($feedItem){
+ // 1.0 adds optional elements around the 0.91 standard
+ $item = $this->parseRSS_0_9_1_Item($feedItem);
+
+ $namespaces = $feedItem->getNamespaces(true);
+ if(isset($namespaces['dc'])) {
+ $dc = $feedItem->children($namespaces['dc']);
+ if(isset($dc->date)) $item['timestamp'] = strtotime((string)$dc->date);
+ if(isset($dc->creator)) $item['author'] = (string)$dc->creator;
+ }
+
+ return $item;
+ }
+
+ protected function parseRSS_2_0_Item($feedItem){
+ // Primary data is compatible to 0.91 with some additional data
+ $item = $this->parseRSS_0_9_1_Item($feedItem);
+
+ $namespaces = $feedItem->getNamespaces(true);
+ if(isset($namespaces['dc'])) $dc = $feedItem->children($namespaces['dc']);
+
+ if(isset($feedItem->guid)) {
+ foreach($feedItem->guid->attributes() as $attribute => $value) {
+ if($attribute === 'isPermaLink'
+ && ($value === 'true' || filter_var($feedItem->guid, FILTER_VALIDATE_URL))) {
+ $item['uri'] = (string)$feedItem->guid;
+ break;
+ }
+ }
+ }
+
+ if(isset($feedItem->pubDate)) {
+ $item['timestamp'] = strtotime((string)$feedItem->pubDate);
+ } elseif(isset($dc->date)) {
+ $item['timestamp'] = strtotime((string)$dc->date);
+ }
+ if(isset($feedItem->author)) {
+ $item['author'] = (string)$feedItem->author;
+ } elseif(isset($dc->creator)) {
+ $item['author'] = (string)$dc->creator;
+ }
+ return $item;
+ }
+
+ /**
+ * Method should return, from a source RSS item given by lastRSS, one of our Items objects
+ * @param $item the input rss item
+ * @return a RSS-Bridge Item, with (hopefully) the whole content)
+ */
+ protected function parseItem($item){
+ switch($this->feedType) {
+ case 'RSS_1_0':
+ return $this->parseRSS_1_0_Item($item);
+ break;
+ case 'RSS_2_0':
+ return $this->parseRSS_2_0_Item($item);
+ break;
+ case 'ATOM_1_0':
+ return $this->parseATOMItem($item);
+ break;
+ default: returnClientError('Unknown version ' . $this->getInput('version') . '!');
+ }
+ }
+
+ public function getURI(){
+ return $this->uri ?: parent::getURI();
+ }
+
+ public function getName(){
+ return $this->name ?: parent::getName();
+ }
+}
diff --git a/lib/Format.php b/lib/Format.php
new file mode 100644
index 0000000..ab932cc
--- /dev/null
+++ b/lib/Format.php
@@ -0,0 +1,73 @@
+<?php
+require_once(__DIR__ . '/FormatInterface.php');
+class Format {
+
+ static protected $dirFormat;
+
+ public function __construct(){
+ throw new \LogicException('Please use ' . __CLASS__ . '::create for new object.');
+ }
+
+ static public function create($nameFormat){
+ if(!preg_match('@^[A-Z][a-zA-Z]*$@', $nameFormat)) {
+ throw new \InvalidArgumentException('Name format must be at least
+ one uppercase follow or not by alphabetic characters.');
+ }
+
+ $nameFormat = $nameFormat . 'Format';
+ $pathFormat = self::getDir() . $nameFormat . '.php';
+
+ if(!file_exists($pathFormat)) {
+ throw new \Exception('The format you looking for does not exist.');
+ }
+
+ require_once $pathFormat;
+
+ return new $nameFormat();
+ }
+
+ static public function setDir($dirFormat){
+ if(!is_string($dirFormat)) {
+ throw new \InvalidArgumentException('Dir format must be a string.');
+ }
+
+ if(!file_exists($dirFormat)) {
+ throw new \Exception('Dir format does not exist.');
+ }
+
+ self::$dirFormat = $dirFormat;
+ }
+
+ static public function getDir(){
+ $dirFormat = self::$dirFormat;
+
+ if(is_null($dirFormat)) {
+ throw new \LogicException(__CLASS__ . ' class need to know format path !');
+ }
+
+ return $dirFormat;
+ }
+
+ /**
+ * Read format dir and catch informations about each format depending annotation
+ * @return array Informations about each format
+ */
+ static public function searchInformation(){
+ $pathDirFormat = self::getDir();
+
+ $listFormat = array();
+
+ $searchCommonPattern = array('name');
+
+ $dirFiles = scandir($pathDirFormat);
+ if($dirFiles !== false) {
+ foreach($dirFiles as $fileName) {
+ if(preg_match('@^([^.]+)Format\.php$@U', $fileName, $out)) { // Is PHP file ?
+ $listFormat[] = $out[1];
+ }
+ }
+ }
+
+ return $listFormat;
+ }
+}
diff --git a/lib/FormatAbstract.php b/lib/FormatAbstract.php
new file mode 100644
index 0000000..46d5e4a
--- /dev/null
+++ b/lib/FormatAbstract.php
@@ -0,0 +1,106 @@
+<?php
+require_once(__DIR__ . '/FormatInterface.php');
+abstract class FormatAbstract implements FormatInterface {
+ const DEFAULT_CHARSET = 'UTF-8';
+
+ protected
+ $contentType,
+ $charset,
+ $items,
+ $extraInfos;
+
+ public function setCharset($charset){
+ $this->charset = $charset;
+
+ return $this;
+ }
+
+ public function getCharset(){
+ $charset = $this->charset;
+
+ return is_null($charset) ? static::DEFAULT_CHARSET : $charset;
+ }
+
+ protected function setContentType($contentType){
+ $this->contentType = $contentType;
+
+ return $this;
+ }
+
+ protected function callContentType(){
+ header('Content-Type: ' . $this->contentType);
+ }
+
+ public function display(){
+ echo $this->stringify();
+
+ return $this;
+ }
+
+ public function setItems(array $items){
+ $this->items = array_map(array($this, 'array_trim'), $items);
+
+ return $this;
+ }
+
+ public function getItems(){
+ if(!is_array($this->items))
+ throw new \LogicException('Feed the ' . get_class($this) . ' with "setItems" method before !');
+
+ return $this->items;
+ }
+
+ /**
+ * Define common informations can be required by formats and set default value for unknow values
+ * @param array $extraInfos array with know informations (there isn't merge !!!)
+ * @return this
+ */
+ public function setExtraInfos(array $extraInfos = array()){
+ foreach(array('name', 'uri') as $infoName) {
+ if(!isset($extraInfos[$infoName])) {
+ $extraInfos[$infoName] = '';
+ }
+ }
+
+ $this->extraInfos = $extraInfos;
+
+ return $this;
+ }
+
+ /**
+ * Return extra infos
+ * @return array See "setExtraInfos" detail method to know what extra are disponibles
+ */
+ public function getExtraInfos(){
+ if(is_null($this->extraInfos)) { // No extra info ?
+ $this->setExtraInfos(); // Define with default value
+ }
+
+ return $this->extraInfos;
+ }
+
+ /**
+ * Sanitized html while leaving it functionnal.
+ * The aim is to keep html as-is (with clickable hyperlinks)
+ * while reducing annoying and potentially dangerous things.
+ * Yes, I know sanitizing HTML 100% is an impossible task.
+ * Maybe we'll switch to http://htmlpurifier.org/
+ * or http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/index.php
+ */
+ protected function sanitizeHtml($html)
+ {
+ $html = str_replace('<script', '<&zwnj;script', $html); // Disable scripts, but leave them visible.
+ $html = str_replace('<iframe', '<&zwnj;iframe', $html);
+ $html = str_replace('<link', '<&zwnj;link', $html);
+ // We leave alone object and embed so that videos can play in RSS readers.
+ return $html;
+ }
+
+ protected function array_trim($elements){
+ foreach($elements as $key => $value) {
+ if(is_string($value))
+ $elements[$key] = trim($value);
+ }
+ return $elements;
+ }
+}
diff --git a/lib/FormatInterface.php b/lib/FormatInterface.php
new file mode 100644
index 0000000..f99d214
--- /dev/null
+++ b/lib/FormatInterface.php
@@ -0,0 +1,11 @@
+<?php
+interface FormatInterface {
+ public function stringify();
+ public function display();
+ public function setItems(array $bridges);
+ public function getItems();
+ public function setExtraInfos(array $infos);
+ public function getExtraInfos();
+ public function setCharset($charset);
+ public function getCharset();
+}
diff --git a/lib/RssBridge.php b/lib/RssBridge.php
new file mode 100644
index 0000000..8d0ef90
--- /dev/null
+++ b/lib/RssBridge.php
@@ -0,0 +1,55 @@
+<?php
+/* rss-bridge library.
+Foundation functions for rss-bridge project.
+See https://github.com/sebsauvage/rss-bridge
+Licence: Public domain.
+*/
+
+define('PATH_VENDOR', '/../vendor');
+
+require __DIR__ . '/Exceptions.php';
+require __DIR__ . '/Format.php';
+require __DIR__ . '/FormatAbstract.php';
+require __DIR__ . '/Bridge.php';
+require __DIR__ . '/BridgeAbstract.php';
+require __DIR__ . '/FeedExpander.php';
+require __DIR__ . '/Cache.php';
+require __DIR__ . '/Authentication.php';
+require __DIR__ . '/Configuration.php';
+
+require __DIR__ . '/validation.php';
+require __DIR__ . '/html.php';
+require __DIR__ . '/error.php';
+require __DIR__ . '/contents.php';
+
+$vendorLibSimpleHtmlDom = __DIR__ . PATH_VENDOR . '/simplehtmldom/simple_html_dom.php';
+if(!file_exists($vendorLibSimpleHtmlDom)) {
+ throw new \HttpException('"PHP Simple HTML DOM Parser" library is missing.
+ Get it from http://simplehtmldom.sourceforge.net and place the script "simple_html_dom.php" in '
+ . substr(PATH_VENDOR, 4)
+ . '/simplehtmldom/',
+ 500);
+}
+require_once $vendorLibSimpleHtmlDom;
+
+/* Example use
+
+ require_once __DIR__ . '/lib/RssBridge.php';
+
+ // Data retrieval
+ Bridge::setDir(__DIR__ . '/bridges/');
+ $bridge = Bridge::create('GoogleSearch');
+ $bridge->collectData($_REQUEST);
+
+ // Data transformation
+ Format::setDir(__DIR__ . '/formats/');
+ $format = Format::create('Atom');
+ $format
+ ->setItems($bridge->getItems())
+ ->setExtraInfos(array(
+ 'name' => $bridge->getName(),
+ 'uri' => $bridge->getURI(),
+ ))
+ ->display();
+
+*/
diff --git a/lib/contents.php b/lib/contents.php
new file mode 100644
index 0000000..416fb7d
--- /dev/null
+++ b/lib/contents.php
@@ -0,0 +1,100 @@
+<?php
+function getContents($url, $header = array(), $opts = array()){
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+
+ if(is_array($header) && count($header) !== 0)
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
+
+ curl_setopt($ch, CURLOPT_USERAGENT, ini_get('user_agent'));
+ curl_setopt($ch, CURLOPT_ENCODING, '');
+ curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
+
+ if(is_array($opts)) {
+ foreach($opts as $key => $value) {
+ curl_setopt($ch, $key, $value);
+ }
+ }
+
+ if(defined('PROXY_URL') && !defined('NOPROXY')) {
+ curl_setopt($ch, CURLOPT_PROXY, PROXY_URL);
+ }
+
+ $content = curl_exec($ch);
+ $curlError = curl_error($ch);
+ $curlErrno = curl_errno($ch);
+ curl_close($ch);
+
+ if($content === false)
+ debugMessage('Cant\'t download ' . $url . ' cUrl error: ' . $curlError . ' (' . $curlErrno . ')');
+
+ return $content;
+}
+
+function getSimpleHTMLDOM($url,
+$header = array(),
+$opts = array(),
+$lowercase = true,
+$forceTagsClosed = true,
+$target_charset = DEFAULT_TARGET_CHARSET,
+$stripRN = true,
+$defaultBRText = DEFAULT_BR_TEXT,
+$defaultSpanText = DEFAULT_SPAN_TEXT){
+ $content = getContents($url, $header, $opts);
+ return str_get_html($content,
+ $lowercase,
+ $forceTagsClosed,
+ $target_charset,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText);
+}
+
+/**
+ * Maintain locally cached versions of pages to avoid multiple downloads.
+ * @param url url to cache
+ * @param duration duration of the cache file in seconds (default: 24h/86400s)
+ * @return content of the file as string
+ */
+function getSimpleHTMLDOMCached($url,
+$duration = 86400,
+$header = array(),
+$opts = array(),
+$lowercase = true,
+$forceTagsClosed = true,
+$target_charset = DEFAULT_TARGET_CHARSET,
+$stripRN = true,
+$defaultBRText = DEFAULT_BR_TEXT,
+$defaultSpanText = DEFAULT_SPAN_TEXT){
+ debugMessage('Caching url ' . $url . ', duration ' . $duration);
+
+ // Initialize cache
+ $cache = Cache::create('FileCache');
+ $cache->setPath(CACHE_DIR . '/pages');
+ $cache->purgeCache(86400); // 24 hours (forced)
+
+ $params = [$url];
+ $cache->setParameters($params);
+
+ // Determine if cached file is within duration
+ $time = $cache->getTime();
+ if($time !== false
+ && (time() - $duration < $time)
+ && (!defined('DEBUG') || DEBUG !== true)) { // Contents within duration
+ $content = $cache->loadData();
+ } else { // Content not within duration
+ $content = getContents($url, $header, $opts);
+ if($content !== false) {
+ $cache->saveData($content);
+ }
+ }
+
+ return str_get_html($content,
+ $lowercase,
+ $forceTagsClosed,
+ $target_charset,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText);
+}
diff --git a/lib/error.php b/lib/error.php
new file mode 100644
index 0000000..ae18f6f
--- /dev/null
+++ b/lib/error.php
@@ -0,0 +1,28 @@
+<?php
+function returnError($message, $code){
+ throw new \HttpException($message, $code);
+}
+
+function returnClientError($message){
+ returnError($message, 400);
+}
+
+function returnServerError($message){
+ returnError($message, 500);
+}
+
+function debugMessage($text){
+ if(!file_exists('DEBUG')) {
+ return;
+ }
+
+ $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
+ $calling = $backtrace[2];
+ $message = $calling['file'] . ':'
+ . $calling['line'] . ' class '
+ . $calling['class'] . '->'
+ . $calling['function'] . ' - '
+ . $text;
+
+ error_log($message);
+}
diff --git a/lib/html.php b/lib/html.php
new file mode 100644
index 0000000..297ab80
--- /dev/null
+++ b/lib/html.php
@@ -0,0 +1,360 @@
+<?php
+function displayBridgeCard($bridgeName, $formats, $isActive = true){
+
+ $getHelperButtonsFormat = function($formats){
+ $buttons = '';
+ foreach($formats as $name) {
+ $buttons .= '<button type="submit" name="format" value="'
+ . $name
+ . '">'
+ . $name
+ . '</button>'
+ . PHP_EOL;
+ }
+
+ return $buttons;
+ };
+
+ $getFormHeader = function($bridgeName){
+ return <<<EOD
+ <form method="GET" action="?">
+ <input type="hidden" name="action" value="display" />
+ <input type="hidden" name="bridge" value="{$bridgeName}" />
+EOD;
+ };
+
+ $bridge = Bridge::create($bridgeName);
+
+ if($bridge == false)
+ return '';
+
+ $HTTPSWarning = '';
+ if(strpos($bridge->getURI(), 'https') !== 0) {
+
+ $HTTPSWarning = '<div class="secure-warning">Warning :
+ This bridge is not fetching its content through a secure connection</div>';
+
+ }
+
+ $name = '<a href="' . $bridge->getURI() . '">' . $bridge->getName() . '</a>';
+ $description = $bridge->getDescription();
+
+ $card = <<<CARD
+ <section id="bridge-{$bridgeName}" data-ref="{$bridgeName}">
+ <h2>{$name}</h2>
+ <p class="description">
+ {$description}
+ </p>
+ <input type="checkbox" class="showmore-box" id="showmore-{$bridgeName}" />
+ <label class="showmore" for="showmore-{$bridgeName}">Show more</label>
+CARD;
+
+ // If we don't have any parameter for the bridge, we print a generic form to load it.
+ if(count($bridge->getParameters()) == 0) {
+
+ $card .= $getFormHeader($bridgeName);
+ $card .= $HTTPSWarning;
+
+ if($isActive) {
+ if(defined('PROXY_URL') && PROXY_BYBRIDGE) {
+ $idArg = 'arg-'
+ . urlencode($bridgeName)
+ . '-'
+ . urlencode('proxyoff')
+ . '-'
+ . urlencode('_noproxy');
+
+ $card .= '<input id="'
+ . $idArg
+ . '" type="checkbox" name="_noproxy" />'
+ . PHP_EOL;
+
+ $card .= '<label for="'
+ . $idArg
+ . '">Disable proxy ('
+ . ((defined('PROXY_NAME') && PROXY_NAME) ? PROXY_NAME : PROXY_URL)
+ . ')</label><br />'
+ . PHP_EOL;
+ } if(CUSTOM_CACHE_TIMEOUT) {
+ $idArg = 'arg-'
+ . urlencode($bridgeName)
+ . '-'
+ . urlencode('_cache_timeout');
+
+ $card .= '<label for="'
+ . $idArg
+ . '">Cache timeout in seconds : </label>'
+ . PHP_EOL;
+
+ $card .= '<input id="'
+ . $idArg
+ . '" type="number" value="'
+ . $bridge->getCacheTimeout()
+ . '" name="_cache_timeout" /><br />'
+ . PHP_EOL;
+ }
+ $card .= $getHelperButtonsFormat($formats);
+ } else {
+ $card .= '<span style="font-weight: bold;">Inactive</span>';
+ }
+
+ $card .= '</form>' . PHP_EOL;
+ }
+
+ $hasGlobalParameter = array_key_exists('global', $bridge->getParameters());
+
+ if($hasGlobalParameter) {
+ $globalParameters = $bridge->getParameters()['global'];
+ }
+
+ foreach($bridge->getParameters() as $parameterName => $parameter) {
+ if(!is_numeric($parameterName) && $parameterName == 'global')
+ continue;
+
+ if($hasGlobalParameter)
+ $parameter = array_merge($parameter, $globalParameters);
+
+ if(!is_numeric($parameterName))
+ $card .= '<h5>' . $parameterName . '</h5>' . PHP_EOL;
+
+ $card .= $getFormHeader($bridgeName);
+ $card .= $HTTPSWarning;
+
+ foreach($parameter as $id => $inputEntry) {
+ $additionalInfoString = '';
+
+ if(isset($inputEntry['required']) && $inputEntry['required'] === true)
+ $additionalInfoString .= ' required';
+
+ if(isset($inputEntry['pattern']))
+ $additionalInfoString .= ' pattern="' . $inputEntry['pattern'] . '"';
+
+ if(isset($inputEntry['title']))
+ $additionalInfoString .= ' title="' . $inputEntry['title'] . '"';
+
+ if(!isset($inputEntry['exampleValue']))
+ $inputEntry['exampleValue'] = '';
+
+ if(!isset($inputEntry['defaultValue']))
+ $inputEntry['defaultValue'] = '';
+
+ $idArg = 'arg-'
+ . urlencode($bridgeName)
+ . '-'
+ . urlencode($parameterName)
+ . '-'
+ . urlencode($id);
+
+ $card .= '<label for="'
+ . $idArg
+ . '">'
+ . $inputEntry['name']
+ . ' : </label>'
+ . PHP_EOL;
+
+ if(!isset($inputEntry['type']) || $inputEntry['type'] == 'text') {
+ $card .= '<input '
+ . $additionalInfoString
+ . ' id="'
+ . $idArg
+ . '" type="text" value="'
+ . $inputEntry['defaultValue']
+ . '" placeholder="'
+ . $inputEntry['exampleValue']
+ . '" name="'
+ . $id
+ . '" /><br />'
+ . PHP_EOL;
+ } elseif($inputEntry['type'] == 'number') {
+ $card .= '<input '
+ . $additionalInfoString
+ . ' id="'
+ . $idArg
+ . '" type="number" value="'
+ . $inputEntry['defaultValue']
+ . '" placeholder="'
+ . $inputEntry['exampleValue']
+ . '" name="'
+ . $id
+ . '" /><br />'
+ . PHP_EOL;
+ } else if($inputEntry['type'] == 'list') {
+ $card .= '<select '
+ . $additionalInfoString
+ . ' id="'
+ . $idArg
+ . '" name="'
+ . $id
+ . '" >';
+
+ foreach($inputEntry['values'] as $name => $value) {
+ if(is_array($value)) {
+ $card .= '<optgroup label="' . htmlentities($name) . '">';
+ foreach($value as $subname => $subvalue) {
+ if($inputEntry['defaultValue'] === $subname
+ || $inputEntry['defaultValue'] === $subvalue) {
+ $card .= '<option value="'
+ . $subvalue
+ . '" selected>'
+ . $subname
+ . '</option>';
+ } else {
+ $card .= '<option value="'
+ . $subvalue
+ . '">'
+ . $subname
+ . '</option>';
+ }
+ }
+ $card .= '</optgroup>';
+ } else {
+ if($inputEntry['defaultValue'] === $name
+ || $inputEntry['defaultValue'] === $value) {
+ $card .= '<option value="'
+ . $value
+ . '" selected>'
+ . $name
+ . '</option>';
+ } else {
+ $card .= '<option value="'
+ . $value
+ . '">'
+ . $name
+ . '</option>';
+ }
+ }
+ }
+ $card .= '</select><br >';
+ } elseif($inputEntry['type'] == 'checkbox') {
+ if($inputEntry['defaultValue'] === 'checked')
+ $card .= '<input '
+ . $additionalInfoString
+ . ' id="'
+ . $idArg
+ . '" type="checkbox" name="'
+ . $id
+ . '" checked /><br />'
+ . PHP_EOL;
+ else
+ $card .= '<input '
+ . $additionalInfoString
+ . ' id="'
+ . $idArg
+ . '" type="checkbox" name="'
+ . $id
+ . '" /><br />'
+ . PHP_EOL;
+ }
+ }
+
+ if($isActive) {
+ if(defined('PROXY_URL') && PROXY_BYBRIDGE) {
+ $idArg = 'arg-'
+ . urlencode($bridgeName)
+ . '-'
+ . urlencode('proxyoff')
+ . '-'
+ . urlencode('_noproxy');
+
+ $card .= '<input id="'
+ . $idArg
+ . '" type="checkbox" name="_noproxy" />'
+ . PHP_EOL;
+
+ $card .= '<label for="'
+ . $idArg
+ . '">Disable proxy ('
+ . ((defined('PROXY_NAME') && PROXY_NAME) ? PROXY_NAME : PROXY_URL)
+ . ')</label><br />'
+ . PHP_EOL;
+ } if(CUSTOM_CACHE_TIMEOUT) {
+ $idArg = 'arg-'
+ . urlencode($bridgeName)
+ . '-'
+ . urlencode('_cache_timeout');
+
+ $card .= '<label for="'
+ . $idArg
+ . '">Cache timeout in seconds : </label>'
+ . PHP_EOL;
+
+ $card .= '<input id="'
+ . $idArg
+ . '" type="number" value="'
+ . $bridge->getCacheTimeout()
+ . '" name="_cache_timeout" /><br />'
+ . PHP_EOL;
+ }
+ $card .= $getHelperButtonsFormat($formats);
+ } else {
+ $card .= '<span style="font-weight: bold;">Inactive</span>';
+ }
+ $card .= '</form>' . PHP_EOL;
+ }
+
+ $card .= '<label class="showless" for="showmore-' . $bridgeName . '">Show less</label>';
+ $card .= '<p class="maintainer">' . $bridge->getMaintainer() . '</p>';
+ $card .= '</section>';
+
+ return $card;
+}
+
+function sanitize($textToSanitize,
+$removedTags = array('script', 'iframe', 'input', 'form'),
+$keptAttributes = array('title', 'href', 'src'),
+$keptText = array()){
+ $htmlContent = str_get_html($textToSanitize);
+
+ foreach($htmlContent->find('*[!b38fd2b1fe7f4747d6b1c1254ccd055e]') as $element) {
+ if(in_array($element->tag, $keptText)) {
+ $element->outertext = $element->plaintext;
+ } elseif(in_array($element->tag, $removedTags)) {
+ $element->outertext = '';
+ } else {
+ foreach($element->getAllAttributes() as $attributeName => $attribute) {
+ if(!in_array($attributeName, $keptAttributes))
+ $element->removeAttribute($attributeName);
+ }
+ }
+ }
+
+ return $htmlContent;
+}
+
+function backgroundToImg($htmlContent) {
+
+ $regex = '/background-image[ ]{0,}:[ ]{0,}url\([\'"]{0,}(.*?)[\'"]{0,}\)/';
+ $htmlContent = str_get_html($htmlContent);
+
+ foreach($htmlContent->find('*[!b38fd2b1fe7f4747d6b1c1254ccd055e]') as $element) {
+
+ if(preg_match($regex, $element->style, $matches) > 0) {
+
+ $element->outertext = '<img style="display:block;" src="' . $matches[1] . '" />';
+
+ }
+
+ }
+
+ return $htmlContent;
+
+}
+
+function defaultLinkTo($content, $server){
+ foreach($content->find('img') as $image) {
+ if(strpos($image->src, 'http') === false
+ && strpos($image->src, '//') === false
+ && strpos($image->src, 'data:') === false)
+ $image->src = $server . $image->src;
+ }
+
+ foreach($content->find('a') as $anchor) {
+ if(strpos($anchor->href, 'http') === false
+ && strpos($anchor->href, '//') === false
+ && strpos($anchor->href, '#') !== 0
+ && strpos($anchor->href, '?') !== 0)
+ $anchor->href = $server . $anchor->href;
+ }
+
+ return $content;
+}
diff --git a/lib/validation.php b/lib/validation.php
new file mode 100644
index 0000000..fdcb51c
--- /dev/null
+++ b/lib/validation.php
@@ -0,0 +1,95 @@
+<?php
+function validateData(&$data, $parameters){
+ $validateTextValue = function($value, $pattern = null){
+ if(!is_null($pattern)) {
+ $filteredValue = filter_var($value,
+ FILTER_VALIDATE_REGEXP,
+ array('options' => array(
+ 'regexp' => '/^' . $pattern . '$/'
+ )
+ ));
+ } else {
+ $filteredValue = filter_var($value);
+ }
+
+ if($filteredValue === false)
+ return null;
+
+ return $filteredValue;
+ };
+
+ $validateNumberValue = function($value){
+ $filteredValue = filter_var($value, FILTER_VALIDATE_INT);
+
+ if($filteredValue === false)
+ return null;
+
+ return $filteredValue;
+ };
+
+ $validateCheckboxValue = function($value){
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
+ };
+
+ $validateListValue = function($value, $expectedValues){
+ $filteredValue = filter_var($value);
+
+ if($filteredValue === false)
+ return null;
+
+ if(!in_array($filteredValue, $expectedValues)) { // Check sub-values?
+ foreach($expectedValues as $subName => $subValue) {
+ if(is_array($subValue) && in_array($filteredValue, $subValue))
+ return $filteredValue;
+ }
+ return null;
+ }
+
+ return $filteredValue;
+ };
+
+ if(!is_array($data))
+ return false;
+
+ foreach($data as $name => $value) {
+ $registered = false;
+ foreach($parameters as $context => $set) {
+ if(array_key_exists($name, $set)) {
+ $registered = true;
+ if(!isset($set[$name]['type'])) {
+ $set[$name]['type'] = 'text';
+ }
+
+ switch($set[$name]['type']) {
+ case 'number':
+ $data[$name] = $validateNumberValue($value);
+ break;
+ case 'checkbox':
+ $data[$name] = $validateCheckboxValue($value);
+ break;
+ case 'list':
+ $data[$name] = $validateListValue($value, $set[$name]['values']);
+ break;
+ default:
+ case 'text':
+ if(isset($set[$name]['pattern'])) {
+ $data[$name] = $validateTextValue($value, $set[$name]['pattern']);
+ } else {
+ $data[$name] = $validateTextValue($value);
+ }
+ break;
+ }
+
+ if(is_null($data[$name]) && isset($set[$name]['required']) && $set[$name]['required']) {
+ echo 'Parameter \'' . $name . '\' is invalid!' . PHP_EOL;
+ return false;
+ }
+ }
+ }
+
+ if(!$registered)
+ return false;
+ }
+
+ return true;
+}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..a67262c
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ruleset name="RSS-Bridge Ruleset">
+ <description>Created with the PHP Coding Standard Generator. http://edorian.github.com/php-coding-standard-generator/</description>
+ <exclude-pattern>./static</exclude-pattern>
+ <exclude-pattern>./vendor</exclude-pattern>
+ <!-- Duplicate class names are not allowed -->
+ <rule ref="Generic.Classes.DuplicateClassName"/>
+ <!-- Statements must not be empty -->
+ <rule ref="Generic.CodeAnalysis.EmptyStatement"/>
+ <!-- Unconditional if-statements are not allowed -->
+ <rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
+ <!-- Do not use final statements inside final classes -->
+ <rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
+ <!-- Do not override methods to call their parent -->
+ <rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
+ <!-- One line should not have more than 80 characters -->
+ <!-- One line must never exceed 120 characters -->
+ <rule ref="Generic.Files.LineLength">
+ <properties>
+ <property name="lineLimit" value="80"/>
+ <property name="absoluteLineLimit" value="120"/>
+ </properties>
+ </rule>
+ <!-- When calling a function: -->
+ <!-- Do not add a space before the opening parenthesis -->
+ <!-- Do not add a space after the opening parenthesis -->
+ <!-- Do not add a space before the closing parenthesis -->
+ <!-- Do not add a space before a comma -->
+ <!-- Add a space after a comma -->
+ <rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
+ <!-- Use UPPERCARE for constants -->
+ <rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
+ <!-- Use lowercase for 'true', 'false' and 'null' -->
+ <rule ref="Generic.PHP.LowerCaseConstant"/>
+ <!-- Use a single string instead of concating -->
+ <rule ref="Generic.Strings.UnnecessaryStringConcat"/>
+ <!-- Use tabs for indentation -->
+ <rule ref="Generic.WhiteSpace.DisallowSpaceIndent"/>
+ <!-- Parameters with default values must appear last in functions -->
+ <rule ref="PEAR.Functions.ValidDefaultValue"/>
+ <!-- Use PascalCase for class names -->
+ <rule ref="PEAR.NamingConventions.ValidClassName"/>
+ <!-- Use 'elseif' instead of 'else if' -->
+ <rule ref="PSR2.ControlStructures.ElseIfDeclaration"/>
+ <!-- Do not add spaces after opening or before closing bracket -->
+ <rule ref="PSR2.ControlStructures.ControlStructureSpacing"/>
+ <!-- Add a new line at the end of a file -->
+ <rule ref="PSR2.Files.EndFileNewline"/>
+ <!-- Add space after closing parenthesis -->
+ <!-- Add body into new line -->
+ <!-- Close body in new line -->
+ <rule ref="Squiz.ControlStructures.ControlSignature">
+ <!-- No space after keyword (before opening parenthesis) -->
+ <exclude name="Squiz.ControlStructures.ControlSignature.SpaceAfterKeyword"/>
+ </rule>
+ <!-- When declaring a function: -->
+ <!-- Do not add a space before a comma -->
+ <!-- Add a space after a comma -->
+ <!-- Add a space before and after an equal sign -->
+ <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing">
+ <properties>
+ <property name="equalsSpacing" value="1"/>
+ </properties>
+ </rule>
+ <!-- Do not add spaces when casting -->
+ <rule ref="Squiz.WhiteSpace.CastSpacing"/>
+ <!-- Operators must have a space around them -->
+ <rule ref="Squiz.WhiteSpace.OperatorSpacing"/>
+ <!-- Do not add a whitespace before a semicolon -->
+ <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
+ <!-- Do not add whitespace at start or end of a file or end of a line -->
+ <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>
+ <!-- Whenever possible use single quote strings -->
+ <rule ref="Squiz.Strings.DoubleQuoteUsage">
+ <exclude name="Squiz.Strings.DoubleQuoteUsage.ContainsVar" />
+ </rule>
+</ruleset>
diff --git a/scalingo.json b/scalingo.json
new file mode 100644
index 0000000..9b1d51e
--- /dev/null
+++ b/scalingo.json
@@ -0,0 +1,6 @@
+{
+ "name": "RSS Bridge",
+ "description": "rss-bridge is a PHP project capable of generating ATOM feeds for websites which don't have one.",
+ "repository": "https://github.com/sebsauvage/rss-bridge",
+ "website": "https://github.com/sebsauvage/rss-bridge"
+}
diff --git a/static/HtmlFormat.css b/static/HtmlFormat.css
new file mode 100644
index 0000000..195a9b0
--- /dev/null
+++ b/static/HtmlFormat.css
@@ -0,0 +1,119 @@
+html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary, time, mark, audio, video {
+
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ font-size: 100%;
+ vertical-align: baseline;
+
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
+
+ display: block;
+
+}
+
+/* Let's go for the actual style */
+
+body {
+
+ background-color: #EEEEEE;
+ font-family: 'Noto Sans';
+
+}
+
+section {
+
+ background-color: #FFFFFF;
+ width: 90%;
+ margin: 30px auto;
+ padding: 10px 15px;
+
+ box-shadow: 0px 1px 2px rgba(0,0,0, 0.25);
+
+}
+
+section > h2 {
+
+ font-size: 200%;
+ font-weight: bold;
+ text-align: center;
+
+}
+
+h1.pagetitle {
+
+ font-size: 300%;
+ font-weight: bold;
+
+ text-align: center;
+ color: #2196F3;
+
+}
+
+h1.pagetitle > a {
+ color: #2196F3;
+}
+
+a.backlink, a.backlink:link, a.backlink:visited, a.itemtitle, a.itemtitle:link, a.itemtitle:visited {
+
+ color: #2196F3;
+
+}
+
+.buttons {
+
+ text-align: center;
+
+}
+
+section > div.content, section > div.categories,
+section > div.content, section > div.attachments {
+
+ padding: 10px;
+
+}
+
+section > div.categories > li.category,
+section > div.attachments > li.enclosure {
+
+ list-style-type: circle;
+ list-style-position: inside;
+
+}
+
+section > time, section > p.author {
+
+ color: #888;
+ font-size: 80%;
+ padding: 10px;
+
+}
+
+button.backbutton, button.rss-feed {
+
+ line-height: 1em;
+ color: #FFF;
+ font-weight: bold;
+ vertical-align: middle;
+ padding: 6px 12px;
+ margin: 12px auto 0px;
+ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3);
+ border-radius: 2px;
+ border: 1px solid transparent;
+ width: 200px;
+ background: #2196F3 none repeat scroll 0% 0%;
+ cursor: pointer;
+
+ margin: 10px;
+
+
+}
+
+img {
+
+ max-width: 100%;
+
+}
diff --git a/static/search.js b/static/search.js
new file mode 100644
index 0000000..3ddd99b
--- /dev/null
+++ b/static/search.js
@@ -0,0 +1,27 @@
+function search() {
+
+ var searchTerm = document.getElementById('searchfield').value;
+ var searchableElements = document.getElementsByTagName('section');
+
+ var regexMatch = new RegExp(searchTerm, "i");
+
+ for(var i = 0; i < searchableElements.length; i++) {
+
+ var textValue = searchableElements[i].getAttribute('data-ref');
+ if(textValue != null) {
+
+ if(textValue.match(regexMatch) == null && searchableElements[i].style.display != "none") {
+
+ searchableElements[i].style.display = "none";
+
+ } else if(textValue.match(regexMatch) != null) {
+
+ searchableElements[i].style.display = "block";
+
+ }
+
+ }
+
+ }
+
+}
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
new file mode 100644
index 0000000..ac2f469
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,310 @@
+html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary, time, mark, audio, video {
+
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
+
+ display: block;
+
+}
+
+/* Let's go for the actual style */
+
+body {
+
+ background-color: #EEEEEE;
+ font-family: 'Noto Sans';
+
+}
+
+header {
+
+ text-shadow:0 5px 6px rgba(150,150,150,0.69);
+ text-align: center;
+ color: #1182DB;
+
+}
+
+header > h1 {
+
+ font-size: 300%;
+
+}
+
+header > h2 {
+
+ margin-left: 1em;
+ font-size: 120%;
+
+}
+
+header > p.status {
+ font-weight: bold;
+ margin: 1em;
+ 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%;
+ margin: auto;
+
+}
+
+.searchbar input[type="text"] {
+
+ width: 100%;
+ margin: auto;
+ font-size: 1.4em;
+ text-align: center;
+
+}
+
+.searchbar input[type="text"]::placeholder {
+
+ text-align: center;
+
+}
+
+.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%;
+ font-weight: bold;
+ color: #1182DB;
+
+}
+
+section {
+
+ background-color: #FFFFFF;
+ width: 80%;
+ margin: 30px auto;
+ padding: 10px 15px;
+ text-align: center;
+
+ box-shadow: 0px 1px 2px rgba(0,0,0, 0.25);
+
+}
+
+
+section.footer {
+
+ opacity: 0.5;
+
+}
+
+section.footer:hover {
+
+ opacity: 1;
+
+}
+
+section.footer .version {
+
+ font-size: 80%;
+
+}
+
+section > h2 {
+
+ font-size: 200%;
+ font-weight: bold;
+
+}
+
+a, a:link, a:visited {
+
+ color: #2196F3;
+
+}
+
+button {
+
+ line-height: 1.9em;
+ color: #FFF;
+ font-weight: bold;
+ vertical-align: middle;
+ padding: 6px 12px;
+ margin: 12px auto 0px;
+ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3);
+ border-radius: 2px;
+ border: 1px solid transparent;
+ min-width: 140px;
+ background: #2196F3 none repeat scroll 0% 0%;
+ cursor: pointer;
+
+ width: calc(20% - 4px);
+
+}
+
+button.small {
+
+ width: auto;
+ line-height: 1.2em;
+
+}
+
+.description {
+
+ margin: 10px;
+ text-decoration: underline;
+
+}
+
+h5 {
+
+ margin: 20px;
+ font-weight: bold;
+
+}
+
+form {
+
+ margin-bottom: 6px;
+
+}
+
+.maintainer {
+
+ font-size: 60%;
+ text-align: right;
+
+}
+
+.secure-warning {
+
+ background-color: #ffc600;
+ color: #5f5f5f;
+
+ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3);
+ border-radius: 2px;
+ border: 1px solid transparent;
+
+ width: 80%;
+ margin: auto;
+ margin-bottom: 6px;
+
+}
+
+form {
+
+ display: none;
+
+}
+
+h5 {
+
+ display: none;
+
+}
+
+.showmore-box {
+
+ display: none;
+
+}
+
+.showmore, .showless {
+
+ color: #888888;
+ cursor: pointer;
+
+}
+
+.showmore-box:checked ~ .showmore {
+
+ display: none;
+
+}
+
+.showmore-box:not(:checked) ~ .showless {
+
+ display: none;
+
+}
+
+
+
+.showmore-box:checked ~ form, .showmore-box:checked ~ h5 {
+
+ display: block;
+
+}
+
+/* Additional styles for error pages */
+
+.exception-message {
+
+ background-color: #c00000;
+ color: #FFFFFF;
+
+ font-weight: bold;
+
+ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3);
+ border-radius: 2px;
+ border: 1px solid transparent;
+
+ width: 80%;
+ margin: auto;
+ margin-bottom: 6px;
+
+}
+
+.advice {
+
+ margin-left: auto;
+ margin-right: auto;
+
+ display: table;
+
+}
+
+.advice > li {
+
+ text-align: left;
+
+}
diff --git a/vendor/simplehtmldom/simple_html_dom.php b/vendor/simplehtmldom/simple_html_dom.php
new file mode 100644
index 0000000..b5d3089
--- /dev/null
+++ b/vendor/simplehtmldom/simple_html_dom.php
@@ -0,0 +1,1742 @@
+<?php
+/**
+ * Website: http://sourceforge.net/projects/simplehtmldom/
+ * Additional projects that may be used: http://sourceforge.net/projects/debugobject/
+ * Acknowledge: Jose Solorzano (https://sourceforge.net/projects/php-html/)
+ * Contributions by:
+ * Yousuke Kumakura (Attribute filters)
+ * Vadim Voituk (Negative indexes supports of "find" method)
+ * Antcs (Constructor with automatically load contents either text or file/url)
+ *
+ * all affected sections have comments starting with "PaperG"
+ *
+ * Paperg - Added case insensitive testing of the value of the selector.
+ * Paperg - Added tag_start for the starting index of tags - NOTE: This works but not accurately.
+ * This tag_start gets counted AFTER \r\n have been crushed out, and after the remove_noice calls so it will not reflect the REAL position of the tag in the source,
+ * it will almost always be smaller by some amount.
+ * We use this to determine how far into the file the tag in question is. This "percentage will never be accurate as the $dom->size is the "real" number of bytes the dom was created from.
+ * but for most purposes, it's a really good estimation.
+ * Paperg - Added the forceTagsClosed to the dom constructor. Forcing tags closed is great for malformed html, but it CAN lead to parsing errors.
+ * Allow the user to tell us how much they trust the html.
+ * Paperg add the text and plaintext to the selectors for the find syntax. plaintext implies text in the innertext of a node. text implies that the tag is a text node.
+ * This allows for us to find tags based on the text they contain.
+ * Create find_ancestor_tag to see if a tag is - at any level - inside of another specific tag.
+ * Paperg: added parse_charset so that we know about the character set of the source document.
+ * NOTE: If the user's system has a routine called get_last_retrieve_url_contents_content_type availalbe, we will assume it's returning the content-type header from the
+ * last transfer or curl_exec, and we will parse that and use it in preference to any other method of charset detection.
+ *
+ * Found infinite loop in the case of broken html in restore_noise. Rewrote to protect from that.
+ * PaperG (John Schlick) Added get_display_size for "IMG" tags.
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author S.C. Chen <me578022@gmail.com>
+ * @author John Schlick
+ * @author Rus Carroll
+ * @version 1.5 ($Rev: 208 $)
+ * @package PlaceLocalInclude
+ * @subpackage simple_html_dom
+ */
+
+/**
+ * All of the Defines for the classes below.
+ * @author S.C. Chen <me578022@gmail.com>
+ */
+define('HDOM_TYPE_ELEMENT', 1);
+define('HDOM_TYPE_COMMENT', 2);
+define('HDOM_TYPE_TEXT', 3);
+define('HDOM_TYPE_ENDTAG', 4);
+define('HDOM_TYPE_ROOT', 5);
+define('HDOM_TYPE_UNKNOWN', 6);
+define('HDOM_QUOTE_DOUBLE', 0);
+define('HDOM_QUOTE_SINGLE', 1);
+define('HDOM_QUOTE_NO', 3);
+define('HDOM_INFO_BEGIN', 0);
+define('HDOM_INFO_END', 1);
+define('HDOM_INFO_QUOTE', 2);
+define('HDOM_INFO_SPACE', 3);
+define('HDOM_INFO_TEXT', 4);
+define('HDOM_INFO_INNER', 5);
+define('HDOM_INFO_OUTER', 6);
+define('HDOM_INFO_ENDSPACE',7);
+define('DEFAULT_TARGET_CHARSET', 'UTF-8');
+define('DEFAULT_BR_TEXT', "\r\n");
+define('DEFAULT_SPAN_TEXT', " ");
+define('MAX_FILE_SIZE', 10000000);
+// helper functions
+// -----------------------------------------------------------------------------
+// get html dom from file
+// $maxlen is defined in the code as PHP_STREAM_COPY_ALL which is defined as -1.
+function file_get_html($url, $use_include_path = false, $context=null, $offset = -1, $maxLen=-1, $lowercase = true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
+{
+ // We DO force the tags to be terminated.
+ $dom = new simple_html_dom(null, $lowercase, $forceTagsClosed, $target_charset, $stripRN, $defaultBRText, $defaultSpanText);
+ // For sourceforge users: uncomment the next line and comment the retreive_url_contents line 2 lines down if it is not already done.
+ $contents = file_get_contents($url, $use_include_path, $context, $offset);
+ // Paperg - use our own mechanism for getting the contents as we want to control the timeout.
+ //$contents = retrieve_url_contents($url);
+ if (empty($contents) || strlen($contents) > MAX_FILE_SIZE)
+ {
+ return false;
+ }
+ // The second parameter can force the selectors to all be lowercase.
+ $dom->load($contents, $lowercase, $stripRN);
+ return $dom;
+}
+
+// get html dom from string
+function str_get_html($str, $lowercase=true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
+{
+ $dom = new simple_html_dom(null, $lowercase, $forceTagsClosed, $target_charset, $stripRN, $defaultBRText, $defaultSpanText);
+ if (empty($str) || strlen($str) > MAX_FILE_SIZE)
+ {
+ $dom->clear();
+ return false;
+ }
+ $dom->load($str, $lowercase, $stripRN);
+ return $dom;
+}
+
+// dump html dom tree
+function dump_html_tree($node, $show_attr=true, $deep=0)
+{
+ $node->dump($node);
+}
+
+
+/**
+ * simple html dom node
+ * PaperG - added ability for "find" routine to lowercase the value of the selector.
+ * PaperG - added $tag_start to track the start position of the tag in the total byte index
+ *
+ * @package PlaceLocalInclude
+ */
+class simple_html_dom_node
+{
+ public $nodetype = HDOM_TYPE_TEXT;
+ public $tag = 'text';
+ public $attr = array();
+ public $children = array();
+ public $nodes = array();
+ public $parent = null;
+ // The "info" array - see HDOM_INFO_... for what each element contains.
+ public $_ = array();
+ public $tag_start = 0;
+ private $dom = null;
+
+ function __construct($dom)
+ {
+ $this->dom = $dom;
+ $dom->nodes[] = $this;
+ }
+
+ function __destruct()
+ {
+ $this->clear();
+ }
+
+ function __toString()
+ {
+ return $this->outertext();
+ }
+
+ // clean up memory due to php5 circular references memory leak...
+ function clear()
+ {
+ $this->dom = null;
+ $this->nodes = null;
+ $this->parent = null;
+ $this->children = null;
+ }
+
+ // dump node's tree
+ function dump($show_attr=true, $deep=0)
+ {
+ $lead = str_repeat(' ', $deep);
+
+ echo $lead.$this->tag;
+ if ($show_attr && count($this->attr)>0)
+ {
+ echo '(';
+ foreach ($this->attr as $k=>$v)
+ echo "[$k]=>\"".$this->$k.'", ';
+ echo ')';
+ }
+ echo "\n";
+
+ if ($this->nodes)
+ {
+ foreach ($this->nodes as $c)
+ {
+ $c->dump($show_attr, $deep+1);
+ }
+ }
+ }
+
+
+ // Debugging function to dump a single dom node with a bunch of information about it.
+ function dump_node($echo=true)
+ {
+
+ $string = $this->tag;
+ if (count($this->attr)>0)
+ {
+ $string .= '(';
+ foreach ($this->attr as $k=>$v)
+ {
+ $string .= "[$k]=>\"".$this->$k.'", ';
+ }
+ $string .= ')';
+ }
+ if (count($this->_)>0)
+ {
+ $string .= ' $_ (';
+ foreach ($this->_ as $k=>$v)
+ {
+ if (is_array($v))
+ {
+ $string .= "[$k]=>(";
+ foreach ($v as $k2=>$v2)
+ {
+ $string .= "[$k2]=>\"".$v2.'", ';
+ }
+ $string .= ")";
+ } else {
+ $string .= "[$k]=>\"".$v.'", ';
+ }
+ }
+ $string .= ")";
+ }
+
+ if (isset($this->text))
+ {
+ $string .= " text: (" . $this->text . ")";
+ }
+
+ $string .= " HDOM_INNER_INFO: '";
+ if (isset($node->_[HDOM_INFO_INNER]))
+ {
+ $string .= $node->_[HDOM_INFO_INNER] . "'";
+ }
+ else
+ {
+ $string .= ' NULL ';
+ }
+
+ $string .= " children: " . count($this->children);
+ $string .= " nodes: " . count($this->nodes);
+ $string .= " tag_start: " . $this->tag_start;
+ $string .= "\n";
+
+ if ($echo)
+ {
+ echo $string;
+ return;
+ }
+ else
+ {
+ return $string;
+ }
+ }
+
+ // returns the parent of node
+ // If a node is passed in, it will reset the parent of the current node to that one.
+ function parent($parent=null)
+ {
+ // I am SURE that this doesn't work properly.
+ // It fails to unset the current node from it's current parents nodes or children list first.
+ if ($parent !== null)
+ {
+ $this->parent = $parent;
+ $this->parent->nodes[] = $this;
+ $this->parent->children[] = $this;
+ }
+
+ return $this->parent;
+ }
+
+ // verify that node has children
+ function has_child()
+ {
+ return !empty($this->children);
+ }
+
+ // returns children of node
+ function children($idx=-1)
+ {
+ if ($idx===-1)
+ {
+ return $this->children;
+ }
+ if (isset($this->children[$idx]))
+ {
+ return $this->children[$idx];
+ }
+ return null;
+ }
+
+ // returns the first child of node
+ function first_child()
+ {
+ if (count($this->children)>0)
+ {
+ return $this->children[0];
+ }
+ return null;
+ }
+
+ // returns the last child of node
+ function last_child()
+ {
+ if (($count=count($this->children))>0)
+ {
+ return $this->children[$count-1];
+ }
+ return null;
+ }
+
+ // returns the next sibling of node
+ function next_sibling()
+ {
+ if ($this->parent===null)
+ {
+ return null;
+ }
+
+ $idx = 0;
+ $count = count($this->parent->children);
+ while ($idx<$count && $this!==$this->parent->children[$idx])
+ {
+ ++$idx;
+ }
+ if (++$idx>=$count)
+ {
+ return null;
+ }
+ return $this->parent->children[$idx];
+ }
+
+ // returns the previous sibling of node
+ function prev_sibling()
+ {
+ if ($this->parent===null) return null;
+ $idx = 0;
+ $count = count($this->parent->children);
+ while ($idx<$count && $this!==$this->parent->children[$idx])
+ ++$idx;
+ if (--$idx<0) return null;
+ return $this->parent->children[$idx];
+ }
+
+ // function to locate a specific ancestor tag in the path to the root.
+ function find_ancestor_tag($tag)
+ {
+ global $debug_object;
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
+
+ // Start by including ourselves in the comparison.
+ $returnDom = $this;
+
+ while (!is_null($returnDom))
+ {
+ if (is_object($debug_object)) { $debug_object->debug_log(2, "Current tag is: " . $returnDom->tag); }
+
+ if ($returnDom->tag == $tag)
+ {
+ break;
+ }
+ $returnDom = $returnDom->parent;
+ }
+ return $returnDom;
+ }
+
+ // get dom node's inner html
+ function innertext()
+ {
+ if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER];
+ if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
+
+ $ret = '';
+ foreach ($this->nodes as $n)
+ $ret .= $n->outertext();
+ return $ret;
+ }
+
+ // get dom node's outer text (with tag)
+ function outertext()
+ {
+ global $debug_object;
+ if (is_object($debug_object))
+ {
+ $text = '';
+ if ($this->tag == 'text')
+ {
+ if (!empty($this->text))
+ {
+ $text = " with text: " . $this->text;
+ }
+ }
+ $debug_object->debug_log(1, 'Innertext of tag: ' . $this->tag . $text);
+ }
+
+ if ($this->tag==='root') return $this->innertext();
+
+ // trigger callback
+ if ($this->dom && $this->dom->callback!==null)
+ {
+ call_user_func_array($this->dom->callback, array($this));
+ }
+
+ if (isset($this->_[HDOM_INFO_OUTER])) return $this->_[HDOM_INFO_OUTER];
+ if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
+
+ // render begin tag
+ if ($this->dom && $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]])
+ {
+ $ret = $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]->makeup();
+ } else {
+ $ret = "";
+ }
+
+ // render inner text
+ if (isset($this->_[HDOM_INFO_INNER]))
+ {
+ // If it's a br tag... don't return the HDOM_INNER_INFO that we may or may not have added.
+ if ($this->tag != "br")
+ {
+ $ret .= $this->_[HDOM_INFO_INNER];
+ }
+ } else {
+ if ($this->nodes)
+ {
+ foreach ($this->nodes as $n)
+ {
+ $ret .= $this->convert_text($n->outertext());
+ }
+ }
+ }
+
+ // render end tag
+ if (isset($this->_[HDOM_INFO_END]) && $this->_[HDOM_INFO_END]!=0)
+ $ret .= '</'.$this->tag.'>';
+ return $ret;
+ }
+
+ // get dom node's plain text
+ function text()
+ {
+ if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER];
+ switch ($this->nodetype)
+ {
+ case HDOM_TYPE_TEXT: return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
+ case HDOM_TYPE_COMMENT: return '';
+ case HDOM_TYPE_UNKNOWN: return '';
+ }
+ if (strcasecmp($this->tag, 'script')===0) return '';
+ if (strcasecmp($this->tag, 'style')===0) return '';
+
+ $ret = '';
+ // In rare cases, (always node type 1 or HDOM_TYPE_ELEMENT - observed for some span tags, and some p tags) $this->nodes is set to NULL.
+ // NOTE: This indicates that there is a problem where it's set to NULL without a clear happening.
+ // WHY is this happening?
+ if (!is_null($this->nodes))
+ {
+ foreach ($this->nodes as $n)
+ {
+ $ret .= $this->convert_text($n->text());
+ }
+
+ // If this node is a span... add a space at the end of it so multiple spans don't run into each other. This is plaintext after all.
+ if ($this->tag == "span")
+ {
+ $ret .= $this->dom->default_span_text;
+ }
+
+
+ }
+ return $ret;
+ }
+
+ function xmltext()
+ {
+ $ret = $this->innertext();
+ $ret = str_ireplace('<![CDATA[', '', $ret);
+ $ret = str_replace(']]>', '', $ret);
+ return $ret;
+ }
+
+ // build node's text with tag
+ function makeup()
+ {
+ // text, comment, unknown
+ if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
+
+ $ret = '<'.$this->tag;
+ $i = -1;
+
+ foreach ($this->attr as $key=>$val)
+ {
+ ++$i;
+
+ // skip removed attribute
+ if ($val===null || $val===false)
+ continue;
+
+ $ret .= $this->_[HDOM_INFO_SPACE][$i][0];
+ //no value attr: nowrap, checked selected...
+ if ($val===true)
+ $ret .= $key;
+ else {
+ switch ($this->_[HDOM_INFO_QUOTE][$i])
+ {
+ case HDOM_QUOTE_DOUBLE: $quote = '"'; break;
+ case HDOM_QUOTE_SINGLE: $quote = '\''; break;
+ default: $quote = '';
+ }
+ $ret .= $key.$this->_[HDOM_INFO_SPACE][$i][1].'='.$this->_[HDOM_INFO_SPACE][$i][2].$quote.$val.$quote;
+ }
+ }
+ $ret = $this->dom->restore_noise($ret);
+ return $ret . $this->_[HDOM_INFO_ENDSPACE] . '>';
+ }
+
+ // find elements by css selector
+ //PaperG - added ability for find to lowercase the value of the selector.
+ function find($selector, $idx=null, $lowercase=false)
+ {
+ $selectors = $this->parse_selector($selector);
+ if (($count=count($selectors))===0) return array();
+ $found_keys = array();
+
+ // find each selector
+ for ($c=0; $c<$count; ++$c)
+ {
+ // The change on the below line was documented on the sourceforge code tracker id 2788009
+ // used to be: if (($levle=count($selectors[0]))===0) return array();
+ if (($levle=count($selectors[$c]))===0) return array();
+ if (!isset($this->_[HDOM_INFO_BEGIN])) return array();
+
+ $head = array($this->_[HDOM_INFO_BEGIN]=>1);
+
+ // handle descendant selectors, no recursive!
+ for ($l=0; $l<$levle; ++$l)
+ {
+ $ret = array();
+ foreach ($head as $k=>$v)
+ {
+ $n = ($k===-1) ? $this->dom->root : $this->dom->nodes[$k];
+ //PaperG - Pass this optional parameter on to the seek function.
+ $n->seek($selectors[$c][$l], $ret, $lowercase);
+ }
+ $head = $ret;
+ }
+
+ foreach ($head as $k=>$v)
+ {
+ if (!isset($found_keys[$k]))
+ {
+ $found_keys[$k] = 1;
+ }
+ }
+ }
+
+ // sort keys
+ ksort($found_keys);
+
+ $found = array();
+ foreach ($found_keys as $k=>$v)
+ $found[] = $this->dom->nodes[$k];
+
+ // return nth-element or array
+ if (is_null($idx)) return $found;
+ else if ($idx<0) $idx = count($found) + $idx;
+ return (isset($found[$idx])) ? $found[$idx] : null;
+ }
+
+ // seek for given conditions
+ // PaperG - added parameter to allow for case insensitive testing of the value of a selector.
+ protected function seek($selector, &$ret, $lowercase=false)
+ {
+ global $debug_object;
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
+
+ list($tag, $key, $val, $exp, $no_key) = $selector;
+
+ // xpath index
+ if ($tag && $key && is_numeric($key))
+ {
+ $count = 0;
+ foreach ($this->children as $c)
+ {
+ if ($tag==='*' || $tag===$c->tag) {
+ if (++$count==$key) {
+ $ret[$c->_[HDOM_INFO_BEGIN]] = 1;
+ return;
+ }
+ }
+ }
+ return;
+ }
+
+ $end = (!empty($this->_[HDOM_INFO_END])) ? $this->_[HDOM_INFO_END] : 0;
+ if ($end==0) {
+ $parent = $this->parent;
+ while (!isset($parent->_[HDOM_INFO_END]) && $parent!==null) {
+ $end -= 1;
+ $parent = $parent->parent;
+ }
+ $end += $parent->_[HDOM_INFO_END];
+ }
+
+ for ($i=$this->_[HDOM_INFO_BEGIN]+1; $i<$end; ++$i) {
+ $node = $this->dom->nodes[$i];
+
+ $pass = true;
+
+ if ($tag==='*' && !$key) {
+ if (in_array($node, $this->children, true))
+ $ret[$i] = 1;
+ continue;
+ }
+
+ // compare tag
+ if ($tag && $tag!=$node->tag && $tag!=='*') {$pass=false;}
+ // compare key
+ if ($pass && $key) {
+ if ($no_key) {
+ if (isset($node->attr[$key])) $pass=false;
+ } else {
+ if (($key != "plaintext") && !isset($node->attr[$key])) $pass=false;
+ }
+ }
+ // compare value
+ if ($pass && $key && $val && $val!=='*') {
+ // If they have told us that this is a "plaintext" search then we want the plaintext of the node - right?
+ if ($key == "plaintext") {
+ // $node->plaintext actually returns $node->text();
+ $nodeKeyValue = $node->text();
+ } else {
+ // this is a normal search, we want the value of that attribute of the tag.
+ $nodeKeyValue = $node->attr[$key];
+ }
+ if (is_object($debug_object)) {$debug_object->debug_log(2, "testing node: " . $node->tag . " for attribute: " . $key . $exp . $val . " where nodes value is: " . $nodeKeyValue);}
+
+ //PaperG - If lowercase is set, do a case insensitive test of the value of the selector.
+ if ($lowercase) {
+ $check = $this->match($exp, strtolower($val), strtolower($nodeKeyValue));
+ } else {
+ $check = $this->match($exp, $val, $nodeKeyValue);
+ }
+ if (is_object($debug_object)) {$debug_object->debug_log(2, "after match: " . ($check ? "true" : "false"));}
+
+ // handle multiple class
+ if (!$check && strcasecmp($key, 'class')===0) {
+ foreach (explode(' ',$node->attr[$key]) as $k) {
+ // Without this, there were cases where leading, trailing, or double spaces lead to our comparing blanks - bad form.
+ if (!empty($k)) {
+ if ($lowercase) {
+ $check = $this->match($exp, strtolower($val), strtolower($k));
+ } else {
+ $check = $this->match($exp, $val, $k);
+ }
+ if ($check) break;
+ }
+ }
+ }
+ if (!$check) $pass = false;
+ }
+ if ($pass) $ret[$i] = 1;
+ unset($node);
+ }
+ // It's passed by reference so this is actually what this function returns.
+ if (is_object($debug_object)) {$debug_object->debug_log(1, "EXIT - ret: ", $ret);}
+ }
+
+ protected function match($exp, $pattern, $value) {
+ global $debug_object;
+ if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
+
+ switch ($exp) {
+ case '=':
+ return ($value===$pattern);
+ case '!=':
+ return ($value!==$pattern);
+ case '^=':
+ return preg_match("/^".preg_quote($pattern,'/')."/", $value);
+ case '$=':
+ return preg_match("/".preg_quote($pattern,'/')."$/", $value);
+ case '*=':
+ if ($pattern[0]=='/') {
+ return preg_match($pattern, $value);
+ }
+ return preg_match("/".$pattern."/i", $value);
+ }
+ return false;
+ }
+
+ protected function parse_selector($selector_string) {
+ global $debug_object;
+ if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
+
+ // pattern of CSS selectors, modified from mootools
+ // Paperg: Add the colon to the attrbute, so that it properly finds <tag attr:ibute="something" > like google does.
+ // Note: if you try to look at this attribute, yo MUST use getAttribute since $dom->x:y will fail the php syntax check.
+// Notice the \[ starting the attbute? and the @? following? This implies that an attribute can begin with an @ sign that is not captured.
+// This implies that an html attribute specifier may start with an @ sign that is NOT captured by the expression.
+// farther study is required to determine of this should be documented or removed.
+// $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
+ $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
+ preg_match_all($pattern, trim($selector_string).' ', $matches, PREG_SET_ORDER);
+ if (is_object($debug_object)) {$debug_object->debug_log(2, "Matches Array: ", $matches);}
+
+ $selectors = array();
+ $result = array();
+ //print_r($matches);
+
+ foreach ($matches as $m) {
+ $m[0] = trim($m[0]);
+ if ($m[0]==='' || $m[0]==='/' || $m[0]==='//') continue;
+ // for browser generated xpath
+ if ($m[1]==='tbody') continue;
+
+ list($tag, $key, $val, $exp, $no_key) = array($m[1], null, null, '=', false);
+ if (!empty($m[2])) {$key='id'; $val=$m[2];}
+ if (!empty($m[3])) {$key='class'; $val=$m[3];}
+ if (!empty($m[4])) {$key=$m[4];}
+ if (!empty($m[5])) {$exp=$m[5];}
+ if (!empty($m[6])) {$val=$m[6];}
+
+ // convert to lowercase
+ if ($this->dom->lowercase) {$tag=strtolower($tag); $key=strtolower($key);}
+ //elements that do NOT have the specified attribute
+ if (isset($key[0]) && $key[0]==='!') {$key=substr($key, 1); $no_key=true;}
+
+ $result[] = array($tag, $key, $val, $exp, $no_key);
+ if (trim($m[7])===',') {
+ $selectors[] = $result;
+ $result = array();
+ }
+ }
+ if (count($result)>0)
+ $selectors[] = $result;
+ return $selectors;
+ }
+
+ function __get($name)
+ {
+ if (isset($this->attr[$name]))
+ {
+ return $this->convert_text($this->attr[$name]);
+ }
+ switch ($name)
+ {
+ case 'outertext': return $this->outertext();
+ case 'innertext': return $this->innertext();
+ case 'plaintext': return $this->text();
+ case 'xmltext': return $this->xmltext();
+ default: return array_key_exists($name, $this->attr);
+ }
+ }
+
+ function __set($name, $value)
+ {
+ global $debug_object;
+ if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
+
+ switch ($name)
+ {
+ case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value;
+ case 'innertext':
+ if (isset($this->_[HDOM_INFO_TEXT])) return $this->_[HDOM_INFO_TEXT] = $value;
+ return $this->_[HDOM_INFO_INNER] = $value;
+ }
+ if (!isset($this->attr[$name]))
+ {
+ $this->_[HDOM_INFO_SPACE][] = array(' ', '', '');
+ $this->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE;
+ }
+ $this->attr[$name] = $value;
+ }
+
+ function __isset($name)
+ {
+ switch ($name)
+ {
+ case 'outertext': return true;
+ case 'innertext': return true;
+ case 'plaintext': return true;
+ }
+ //no value attr: nowrap, checked selected...
+ return (array_key_exists($name, $this->attr)) ? true : isset($this->attr[$name]);
+ }
+
+ function __unset($name) {
+ if (isset($this->attr[$name]))
+ unset($this->attr[$name]);
+ }
+
+ // PaperG - Function to convert the text from one character set to another if the two sets are not the same.
+ function convert_text($text)
+ {
+ global $debug_object;
+ if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
+
+ $converted_text = $text;
+
+ $sourceCharset = "";
+ $targetCharset = "";
+
+ if ($this->dom)
+ {
+ $sourceCharset = strtoupper($this->dom->_charset);
+ $targetCharset = strtoupper($this->dom->_target_charset);
+ }
+ if (is_object($debug_object)) {$debug_object->debug_log(3, "source charset: " . $sourceCharset . " target charaset: " . $targetCharset);}
+
+ if (!empty($sourceCharset) && !empty($targetCharset) && (strcasecmp($sourceCharset, $targetCharset) != 0))
+ {
+ // Check if the reported encoding could have been incorrect and the text is actually already UTF-8
+ if ((strcasecmp($targetCharset, 'UTF-8') == 0) && ($this->is_utf8($text)))
+ {
+ $converted_text = $text;
+ }
+ else
+ {
+ $converted_text = iconv($sourceCharset, $targetCharset, $text);
+ }
+ }
+
+ // Lets make sure that we don't have that silly BOM issue with any of the utf-8 text we output.
+ if ($targetCharset == 'UTF-8')
+ {
+ if (substr($converted_text, 0, 3) == "\xef\xbb\xbf")
+ {
+ $converted_text = substr($converted_text, 3);
+ }
+ if (substr($converted_text, -3) == "\xef\xbb\xbf")
+ {
+ $converted_text = substr($converted_text, 0, -3);
+ }
+ }
+
+ return $converted_text;
+ }
+
+ /**
+ * Returns true if $string is valid UTF-8 and false otherwise.
+ *
+ * @param mixed $str String to be tested
+ * @return boolean
+ */
+ static function is_utf8($str)
+ {
+ $c=0; $b=0;
+ $bits=0;
+ $len=strlen($str);
+ for($i=0; $i<$len; $i++)
+ {
+ $c=ord($str[$i]);
+ if($c > 128)
+ {
+ if(($c >= 254)) return false;
+ elseif($c >= 252) $bits=6;
+ elseif($c >= 248) $bits=5;
+ elseif($c >= 240) $bits=4;
+ elseif($c >= 224) $bits=3;
+ elseif($c >= 192) $bits=2;
+ else return false;
+ if(($i+$bits) > $len) return false;
+ while($bits > 1)
+ {
+ $i++;
+ $b=ord($str[$i]);
+ if($b < 128 || $b > 191) return false;
+ $bits--;
+ }
+ }
+ }
+ return true;
+ }
+ /*
+ function is_utf8($string)
+ {
+ //this is buggy
+ return (utf8_encode(utf8_decode($string)) == $string);
+ }
+ */
+
+ /**
+ * Function to try a few tricks to determine the displayed size of an img on the page.
+ * NOTE: This will ONLY work on an IMG tag. Returns FALSE on all other tag types.
+ *
+ * @author John Schlick
+ * @version April 19 2012
+ * @return array an array containing the 'height' and 'width' of the image on the page or -1 if we can't figure it out.
+ */
+ function get_display_size()
+ {
+ global $debug_object;
+
+ $width = -1;
+ $height = -1;
+
+ if ($this->tag !== 'img')
+ {
+ return false;
+ }
+
+ // See if there is aheight or width attribute in the tag itself.
+ if (isset($this->attr['width']))
+ {
+ $width = $this->attr['width'];
+ }
+
+ if (isset($this->attr['height']))
+ {
+ $height = $this->attr['height'];
+ }
+
+ // Now look for an inline style.
+ if (isset($this->attr['style']))
+ {
+ // Thanks to user gnarf from stackoverflow for this regular expression.
+ $attributes = array();
+ preg_match_all("/([\w-]+)\s*:\s*([^;]+)\s*;?/", $this->attr['style'], $matches, PREG_SET_ORDER);
+ foreach ($matches as $match) {
+ $attributes[$match[1]] = $match[2];
+ }
+
+ // If there is a width in the style attributes:
+ if (isset($attributes['width']) && $width == -1)
+ {
+ // check that the last two characters are px (pixels)
+ if (strtolower(substr($attributes['width'], -2)) == 'px')
+ {
+ $proposed_width = substr($attributes['width'], 0, -2);
+ // Now make sure that it's an integer and not something stupid.
+ if (filter_var($proposed_width, FILTER_VALIDATE_INT))
+ {
+ $width = $proposed_width;
+ }
+ }
+ }
+
+ // If there is a width in the style attributes:
+ if (isset($attributes['height']) && $height == -1)
+ {
+ // check that the last two characters are px (pixels)
+ if (strtolower(substr($attributes['height'], -2)) == 'px')
+ {
+ $proposed_height = substr($attributes['height'], 0, -2);
+ // Now make sure that it's an integer and not something stupid.
+ if (filter_var($proposed_height, FILTER_VALIDATE_INT))
+ {
+ $height = $proposed_height;
+ }
+ }
+ }
+
+ }
+
+ // Future enhancement:
+ // Look in the tag to see if there is a class or id specified that has a height or width attribute to it.
+
+ // Far future enhancement
+ // Look at all the parent tags of this image to see if they specify a class or id that has an img selector that specifies a height or width
+ // Note that in this case, the class or id will have the img subselector for it to apply to the image.
+
+ // ridiculously far future development
+ // If the class or id is specified in a SEPARATE css file thats not on the page, go get it and do what we were just doing for the ones on the page.
+
+ $result = array('height' => $height,
+ 'width' => $width);
+ return $result;
+ }
+
+ // camel naming conventions
+ function getAllAttributes() {return $this->attr;}
+ function getAttribute($name) {return $this->__get($name);}
+ function setAttribute($name, $value) {$this->__set($name, $value);}
+ function hasAttribute($name) {return $this->__isset($name);}
+ function removeAttribute($name) {$this->__set($name, null);}
+ function getElementById($id) {return $this->find("#$id", 0);}
+ function getElementsById($id, $idx=null) {return $this->find("#$id", $idx);}
+ function getElementByTagName($name) {return $this->find($name, 0);}
+ function getElementsByTagName($name, $idx=null) {return $this->find($name, $idx);}
+ function parentNode() {return $this->parent();}
+ function childNodes($idx=-1) {return $this->children($idx);}
+ function firstChild() {return $this->first_child();}
+ function lastChild() {return $this->last_child();}
+ function nextSibling() {return $this->next_sibling();}
+ function previousSibling() {return $this->prev_sibling();}
+ function hasChildNodes() {return $this->has_child();}
+ function nodeName() {return $this->tag;}
+ function appendChild($node) {$node->parent($this); return $node;}
+
+}
+
+/**
+ * simple html dom parser
+ * Paperg - in the find routine: allow us to specify that we want case insensitive testing of the value of the selector.
+ * Paperg - change $size from protected to public so we can easily access it
+ * Paperg - added ForceTagsClosed in the constructor which tells us whether we trust the html or not. Default is to NOT trust it.
+ *
+ * @package PlaceLocalInclude
+ */
+class simple_html_dom
+{
+ public $root = null;
+ public $nodes = array();
+ public $callback = null;
+ public $lowercase = false;
+ // Used to keep track of how large the text was when we started.
+ public $original_size;
+ public $size;
+ protected $pos;
+ protected $doc;
+ protected $char;
+ protected $cursor;
+ protected $parent;
+ protected $noise = array();
+ protected $token_blank = " \t\r\n";
+ protected $token_equal = ' =/>';
+ protected $token_slash = " />\r\n\t";
+ protected $token_attr = ' >';
+ // Note that this is referenced by a child node, and so it needs to be public for that node to see this information.
+ public $_charset = '';
+ public $_target_charset = '';
+ protected $default_br_text = "";
+ public $default_span_text = "";
+
+ // use isset instead of in_array, performance boost about 30%...
+ protected $self_closing_tags = array('img'=>1, 'br'=>1, 'input'=>1, 'meta'=>1, 'link'=>1, 'hr'=>1, 'base'=>1, 'embed'=>1, 'spacer'=>1);
+ protected $block_tags = array('root'=>1, 'body'=>1, 'form'=>1, 'div'=>1, 'span'=>1, 'table'=>1);
+ // Known sourceforge issue #2977341
+ // B tags that are not closed cause us to return everything to the end of the document.
+ protected $optional_closing_tags = array(
+ 'tr'=>array('tr'=>1, 'td'=>1, 'th'=>1),
+ 'th'=>array('th'=>1),
+ 'td'=>array('td'=>1),
+ 'li'=>array('li'=>1),
+ 'dt'=>array('dt'=>1, 'dd'=>1),
+ 'dd'=>array('dd'=>1, 'dt'=>1),
+ 'dl'=>array('dd'=>1, 'dt'=>1),
+ 'p'=>array('p'=>1),
+ 'nobr'=>array('nobr'=>1),
+ 'b'=>array('b'=>1),
+ 'option'=>array('option'=>1),
+ );
+
+ function __construct($str=null, $lowercase=true, $forceTagsClosed=true, $target_charset=DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
+ {
+ if ($str)
+ {
+ if (preg_match("/^http:\/\//i",$str) || is_file($str))
+ {
+ $this->load_file($str);
+ }
+ else
+ {
+ $this->load($str, $lowercase, $stripRN, $defaultBRText, $defaultSpanText);
+ }
+ }
+ // Forcing tags to be closed implies that we don't trust the html, but it can lead to parsing errors if we SHOULD trust the html.
+ if (!$forceTagsClosed) {
+ $this->optional_closing_array=array();
+ }
+ $this->_target_charset = $target_charset;
+ }
+
+ function __destruct()
+ {
+ $this->clear();
+ }
+
+ // load html from string
+ function load($str, $lowercase=true, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
+ {
+ global $debug_object;
+
+ // prepare
+ $this->prepare($str, $lowercase, $stripRN, $defaultBRText, $defaultSpanText);
+ // strip out cdata
+ $this->remove_noise("'<!\[CDATA\[(.*?)\]\]>'is", true);
+ // strip out comments
+ $this->remove_noise("'<!--(.*?)-->'is");
+ // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037
+ // Script tags removal now preceeds style tag removal.
+ // strip out <script> tags
+ $this->remove_noise("'<\s*script[^>]*[^/]>(.*?)<\s*/\s*script\s*>'is");
+ $this->remove_noise("'<\s*script\s*>(.*?)<\s*/\s*script\s*>'is");
+ // strip out <style> tags
+ $this->remove_noise("'<\s*style[^>]*[^/]>(.*?)<\s*/\s*style\s*>'is");
+ $this->remove_noise("'<\s*style\s*>(.*?)<\s*/\s*style\s*>'is");
+ // strip out preformatted tags
+ $this->remove_noise("'<\s*(?:code)[^>]*>(.*?)<\s*/\s*(?:code)\s*>'is");
+ // strip out server side scripts
+ $this->remove_noise("'(<\?)(.*?)(\?>)'s", true);
+ // strip smarty scripts
+ $this->remove_noise("'(\{\w)(.*?)(\})'s", true);
+
+ // parsing
+ while ($this->parse());
+ // end
+ $this->root->_[HDOM_INFO_END] = $this->cursor;
+ $this->parse_charset();
+
+ // make load function chainable
+ return $this;
+
+ }
+
+ // load html from file
+ function load_file()
+ {
+ $args = func_get_args();
+ $this->load(call_user_func_array('file_get_contents', $args), true);
+ // Throw an error if we can't properly load the dom.
+ if (($error=error_get_last())!==null) {
+ $this->clear();
+ return false;
+ }
+ }
+
+ // set callback function
+ function set_callback($function_name)
+ {
+ $this->callback = $function_name;
+ }
+
+ // remove callback function
+ function remove_callback()
+ {
+ $this->callback = null;
+ }
+
+ // save dom as string
+ function save($filepath='')
+ {
+ $ret = $this->root->innertext();
+ if ($filepath!=='') file_put_contents($filepath, $ret, LOCK_EX);
+ return $ret;
+ }
+
+ // find dom node by css selector
+ // Paperg - allow us to specify that we want case insensitive testing of the value of the selector.
+ function find($selector, $idx=null, $lowercase=false)
+ {
+ return $this->root->find($selector, $idx, $lowercase);
+ }
+
+ // clean up memory due to php5 circular references memory leak...
+ function clear()
+ {
+ foreach ($this->nodes as $n) {$n->clear(); $n = null;}
+ // This add next line is documented in the sourceforge repository. 2977248 as a fix for ongoing memory leaks that occur even with the use of clear.
+ if (isset($this->children)) foreach ($this->children as $n) {$n->clear(); $n = null;}
+ if (isset($this->parent)) {$this->parent->clear(); unset($this->parent);}
+ if (isset($this->root)) {$this->root->clear(); unset($this->root);}
+ unset($this->doc);
+ unset($this->noise);
+ }
+
+ function dump($show_attr=true)
+ {
+ $this->root->dump($show_attr);
+ }
+
+ // prepare HTML data and init everything
+ protected function prepare($str, $lowercase=true, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
+ {
+ $this->clear();
+
+ // set the length of content before we do anything to it.
+ $this->size = strlen($str);
+ // Save the original size of the html that we got in. It might be useful to someone.
+ $this->original_size = $this->size;
+
+ //before we save the string as the doc... strip out the \r \n's if we are told to.
+ if ($stripRN) {
+ $str = str_replace("\r", " ", $str);
+ $str = str_replace("\n", " ", $str);
+
+ // set the length of content since we have changed it.
+ $this->size = strlen($str);
+ }
+
+ $this->doc = $str;
+ $this->pos = 0;
+ $this->cursor = 1;
+ $this->noise = array();
+ $this->nodes = array();
+ $this->lowercase = $lowercase;
+ $this->default_br_text = $defaultBRText;
+ $this->default_span_text = $defaultSpanText;
+ $this->root = new simple_html_dom_node($this);
+ $this->root->tag = 'root';
+ $this->root->_[HDOM_INFO_BEGIN] = -1;
+ $this->root->nodetype = HDOM_TYPE_ROOT;
+ $this->parent = $this->root;
+ if ($this->size>0) $this->char = $this->doc[0];
+ }
+
+ // parse html content
+ protected function parse()
+ {
+ if (($s = $this->copy_until_char('<'))==='')
+ {
+ return $this->read_tag();
+ }
+
+ // text
+ $node = new simple_html_dom_node($this);
+ ++$this->cursor;
+ $node->_[HDOM_INFO_TEXT] = $s;
+ $this->link_nodes($node, false);
+ return true;
+ }
+
+ // PAPERG - dkchou - added this to try to identify the character set of the page we have just parsed so we know better how to spit it out later.
+ // NOTE: IF you provide a routine called get_last_retrieve_url_contents_content_type which returns the CURLINFO_CONTENT_TYPE from the last curl_exec
+ // (or the content_type header from the last transfer), we will parse THAT, and if a charset is specified, we will use it over any other mechanism.
+ protected function parse_charset()
+ {
+ global $debug_object;
+
+ $charset = null;
+
+ if (function_exists('get_last_retrieve_url_contents_content_type'))
+ {
+ $contentTypeHeader = get_last_retrieve_url_contents_content_type();
+ $success = preg_match('/charset=(.+)/', $contentTypeHeader, $matches);
+ if ($success)
+ {
+ $charset = $matches[1];
+ if (is_object($debug_object)) {$debug_object->debug_log(2, 'header content-type found charset of: ' . $charset);}
+ }
+
+ }
+
+ if (empty($charset))
+ {
+ $el = $this->root->find('meta[http-equiv=Content-Type]',0);
+ if (!empty($el))
+ {
+ $fullvalue = $el->content;
+ if (is_object($debug_object)) {$debug_object->debug_log(2, 'meta content-type tag found' . $fullvalue);}
+
+ if (!empty($fullvalue))
+ {
+ $success = preg_match('/charset=(.+)/', $fullvalue, $matches);
+ if ($success)
+ {
+ $charset = $matches[1];
+ }
+ else
+ {
+ // If there is a meta tag, and they don't specify the character set, research says that it's typically ISO-8859-1
+ if (is_object($debug_object)) {$debug_object->debug_log(2, 'meta content-type tag couldn\'t be parsed. using iso-8859 default.');}
+ $charset = 'ISO-8859-1';
+ }
+ }
+ }
+ }
+
+ // If we couldn't find a charset above, then lets try to detect one based on the text we got...
+ if (empty($charset))
+ {
+ // Use this in case mb_detect_charset isn't installed/loaded on this machine.
+ $charset = false;
+ if (function_exists('mb_detect_encoding'))
+ {
+ // Have php try to detect the encoding from the text given to us.
+ $charset = mb_detect_encoding($this->root->plaintext . "ascii", $encoding_list = array( "UTF-8", "CP1252" ) );
+ if (is_object($debug_object)) {$debug_object->debug_log(2, 'mb_detect found: ' . $charset);}
+ }
+
+ // and if this doesn't work... then we need to just wrongheadedly assume it's UTF-8 so that we can move on - cause this will usually give us most of what we need...
+ if ($charset === false)
+ {
+ if (is_object($debug_object)) {$debug_object->debug_log(2, 'since mb_detect failed - using default of utf-8');}
+ $charset = 'UTF-8';
+ }
+ }
+
+ // Since CP1252 is a superset, if we get one of it's subsets, we want it instead.
+ if ((strtolower($charset) == strtolower('ISO-8859-1')) || (strtolower($charset) == strtolower('Latin1')) || (strtolower($charset) == strtolower('Latin-1')))
+ {
+ if (is_object($debug_object)) {$debug_object->debug_log(2, 'replacing ' . $charset . ' with CP1252 as its a superset');}
+ $charset = 'CP1252';
+ }
+
+ if (is_object($debug_object)) {$debug_object->debug_log(1, 'EXIT - ' . $charset);}
+
+ return $this->_charset = $charset;
+ }
+
+ // read tag info
+ protected function read_tag()
+ {
+ if ($this->char!=='<')
+ {
+ $this->root->_[HDOM_INFO_END] = $this->cursor;
+ return false;
+ }
+ $begin_tag_pos = $this->pos;
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+
+ // end tag
+ if ($this->char==='/')
+ {
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ // This represents the change in the simple_html_dom trunk from revision 180 to 181.
+ // $this->skip($this->token_blank_t);
+ $this->skip($this->token_blank);
+ $tag = $this->copy_until_char('>');
+
+ // skip attributes in end tag
+ if (($pos = strpos($tag, ' '))!==false)
+ $tag = substr($tag, 0, $pos);
+
+ $parent_lower = strtolower($this->parent->tag);
+ $tag_lower = strtolower($tag);
+
+ if ($parent_lower!==$tag_lower)
+ {
+ if (isset($this->optional_closing_tags[$parent_lower]) && isset($this->block_tags[$tag_lower]))
+ {
+ $this->parent->_[HDOM_INFO_END] = 0;
+ $org_parent = $this->parent;
+
+ while (($this->parent->parent) && strtolower($this->parent->tag)!==$tag_lower)
+ $this->parent = $this->parent->parent;
+
+ if (strtolower($this->parent->tag)!==$tag_lower) {
+ $this->parent = $org_parent; // restore origonal parent
+ if ($this->parent->parent) $this->parent = $this->parent->parent;
+ $this->parent->_[HDOM_INFO_END] = $this->cursor;
+ return $this->as_text_node($tag);
+ }
+ }
+ else if (($this->parent->parent) && isset($this->block_tags[$tag_lower]))
+ {
+ $this->parent->_[HDOM_INFO_END] = 0;
+ $org_parent = $this->parent;
+
+ while (($this->parent->parent) && strtolower($this->parent->tag)!==$tag_lower)
+ $this->parent = $this->parent->parent;
+
+ if (strtolower($this->parent->tag)!==$tag_lower)
+ {
+ $this->parent = $org_parent; // restore origonal parent
+ $this->parent->_[HDOM_INFO_END] = $this->cursor;
+ return $this->as_text_node($tag);
+ }
+ }
+ else if (($this->parent->parent) && strtolower($this->parent->parent->tag)===$tag_lower)
+ {
+ $this->parent->_[HDOM_INFO_END] = 0;
+ $this->parent = $this->parent->parent;
+ }
+ else
+ return $this->as_text_node($tag);
+ }
+
+ $this->parent->_[HDOM_INFO_END] = $this->cursor;
+ if ($this->parent->parent) $this->parent = $this->parent->parent;
+
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ return true;
+ }
+
+ $node = new simple_html_dom_node($this);
+ $node->_[HDOM_INFO_BEGIN] = $this->cursor;
+ ++$this->cursor;
+ $tag = $this->copy_until($this->token_slash);
+ $node->tag_start = $begin_tag_pos;
+
+ // doctype, cdata & comments...
+ if (isset($tag[0]) && $tag[0]==='!') {
+ $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until_char('>');
+
+ if (isset($tag[2]) && $tag[1]==='-' && $tag[2]==='-') {
+ $node->nodetype = HDOM_TYPE_COMMENT;
+ $node->tag = 'comment';
+ } else {
+ $node->nodetype = HDOM_TYPE_UNKNOWN;
+ $node->tag = 'unknown';
+ }
+ if ($this->char==='>') $node->_[HDOM_INFO_TEXT].='>';
+ $this->link_nodes($node, true);
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ return true;
+ }
+
+ // text
+ if ($pos=strpos($tag, '<')!==false) {
+ $tag = '<' . substr($tag, 0, -1);
+ $node->_[HDOM_INFO_TEXT] = $tag;
+ $this->link_nodes($node, false);
+ $this->char = $this->doc[--$this->pos]; // prev
+ return true;
+ }
+
+ if (!preg_match("/^[\w-:]+$/", $tag)) {
+ $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until('<>');
+ if ($this->char==='<') {
+ $this->link_nodes($node, false);
+ return true;
+ }
+
+ if ($this->char==='>') $node->_[HDOM_INFO_TEXT].='>';
+ $this->link_nodes($node, false);
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ return true;
+ }
+
+ // begin tag
+ $node->nodetype = HDOM_TYPE_ELEMENT;
+ $tag_lower = strtolower($tag);
+ $node->tag = ($this->lowercase) ? $tag_lower : $tag;
+
+ // handle optional closing tags
+ if (isset($this->optional_closing_tags[$tag_lower]) )
+ {
+ while (isset($this->optional_closing_tags[$tag_lower][strtolower($this->parent->tag)]))
+ {
+ $this->parent->_[HDOM_INFO_END] = 0;
+ $this->parent = $this->parent->parent;
+ }
+ $node->parent = $this->parent;
+ }
+
+ $guard = 0; // prevent infinity loop
+ $space = array($this->copy_skip($this->token_blank), '', '');
+
+ // attributes
+ do
+ {
+ if ($this->char!==null && $space[0]==='')
+ {
+ break;
+ }
+ $name = $this->copy_until($this->token_equal);
+ if ($guard===$this->pos)
+ {
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ continue;
+ }
+ $guard = $this->pos;
+
+ // handle endless '<'
+ if ($this->pos>=$this->size-1 && $this->char!=='>') {
+ $node->nodetype = HDOM_TYPE_TEXT;
+ $node->_[HDOM_INFO_END] = 0;
+ $node->_[HDOM_INFO_TEXT] = '<'.$tag . $space[0] . $name;
+ $node->tag = 'text';
+ $this->link_nodes($node, false);
+ return true;
+ }
+
+ // handle mismatch '<'
+ if ($this->doc[$this->pos-1]=='<') {
+ $node->nodetype = HDOM_TYPE_TEXT;
+ $node->tag = 'text';
+ $node->attr = array();
+ $node->_[HDOM_INFO_END] = 0;
+ $node->_[HDOM_INFO_TEXT] = substr($this->doc, $begin_tag_pos, $this->pos-$begin_tag_pos-1);
+ $this->pos -= 2;
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ $this->link_nodes($node, false);
+ return true;
+ }
+
+ if ($name!=='/' && $name!=='') {
+ $space[1] = $this->copy_skip($this->token_blank);
+ $name = $this->restore_noise($name);
+ if ($this->lowercase) $name = strtolower($name);
+ if ($this->char==='=') {
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ $this->parse_attr($node, $name, $space);
+ }
+ else {
+ //no value attr: nowrap, checked selected...
+ $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_NO;
+ $node->attr[$name] = true;
+ if ($this->char!='>') $this->char = $this->doc[--$this->pos]; // prev
+ }
+ $node->_[HDOM_INFO_SPACE][] = $space;
+ $space = array($this->copy_skip($this->token_blank), '', '');
+ }
+ else
+ break;
+ } while ($this->char!=='>' && $this->char!=='/');
+
+ $this->link_nodes($node, true);
+ $node->_[HDOM_INFO_ENDSPACE] = $space[0];
+
+ // check self closing
+ if ($this->copy_until_char_escape('>')==='/')
+ {
+ $node->_[HDOM_INFO_ENDSPACE] .= '/';
+ $node->_[HDOM_INFO_END] = 0;
+ }
+ else
+ {
+ // reset parent
+ if (!isset($this->self_closing_tags[strtolower($node->tag)])) $this->parent = $node;
+ }
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+
+ // If it's a BR tag, we need to set it's text to the default text.
+ // This way when we see it in plaintext, we can generate formatting that the user wants.
+ // since a br tag never has sub nodes, this works well.
+ if ($node->tag == "br")
+ {
+ $node->_[HDOM_INFO_INNER] = $this->default_br_text;
+ }
+
+ return true;
+ }
+
+ // parse attributes
+ protected function parse_attr($node, $name, &$space)
+ {
+ // Per sourceforge: http://sourceforge.net/tracker/?func=detail&aid=3061408&group_id=218559&atid=1044037
+ // If the attribute is already defined inside a tag, only pay atetntion to the first one as opposed to the last one.
+ if (isset($node->attr[$name]))
+ {
+ return;
+ }
+
+ $space[2] = $this->copy_skip($this->token_blank);
+ switch ($this->char) {
+ case '"':
+ $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE;
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ $node->attr[$name] = $this->restore_noise($this->copy_until_char_escape('"'));
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ break;
+ case '\'':
+ $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_SINGLE;
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ $node->attr[$name] = $this->restore_noise($this->copy_until_char_escape('\''));
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ break;
+ default:
+ $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_NO;
+ $node->attr[$name] = $this->restore_noise($this->copy_until($this->token_attr));
+ }
+ // PaperG: Attributes should not have \r or \n in them, that counts as html whitespace.
+ $node->attr[$name] = str_replace("\r", "", $node->attr[$name]);
+ $node->attr[$name] = str_replace("\n", "", $node->attr[$name]);
+ // PaperG: If this is a "class" selector, lets get rid of the preceeding and trailing space since some people leave it in the multi class case.
+ if ($name == "class") {
+ $node->attr[$name] = trim($node->attr[$name]);
+ }
+ }
+
+ // link node's parent
+ protected function link_nodes(&$node, $is_child)
+ {
+ $node->parent = $this->parent;
+ $this->parent->nodes[] = $node;
+ if ($is_child)
+ {
+ $this->parent->children[] = $node;
+ }
+ }
+
+ // as a text node
+ protected function as_text_node($tag)
+ {
+ $node = new simple_html_dom_node($this);
+ ++$this->cursor;
+ $node->_[HDOM_INFO_TEXT] = '</' . $tag . '>';
+ $this->link_nodes($node, false);
+ $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ return true;
+ }
+
+ protected function skip($chars)
+ {
+ $this->pos += strspn($this->doc, $chars, $this->pos);
+ $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ }
+
+ protected function copy_skip($chars)
+ {
+ $pos = $this->pos;
+ $len = strspn($this->doc, $chars, $pos);
+ $this->pos += $len;
+ $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ if ($len===0) return '';
+ return substr($this->doc, $pos, $len);
+ }
+
+ protected function copy_until($chars)
+ {
+ $pos = $this->pos;
+ $len = strcspn($this->doc, $chars, $pos);
+ $this->pos += $len;
+ $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ return substr($this->doc, $pos, $len);
+ }
+
+ protected function copy_until_char($char)
+ {
+ if ($this->char===null) return '';
+
+ if (($pos = strpos($this->doc, $char, $this->pos))===false) {
+ $ret = substr($this->doc, $this->pos, $this->size-$this->pos);
+ $this->char = null;
+ $this->pos = $this->size;
+ return $ret;
+ }
+
+ if ($pos===$this->pos) return '';
+ $pos_old = $this->pos;
+ $this->char = $this->doc[$pos];
+ $this->pos = $pos;
+ return substr($this->doc, $pos_old, $pos-$pos_old);
+ }
+
+ protected function copy_until_char_escape($char)
+ {
+ if ($this->char===null) return '';
+
+ $start = $this->pos;
+ while (1)
+ {
+ if (($pos = strpos($this->doc, $char, $start))===false)
+ {
+ $ret = substr($this->doc, $this->pos, $this->size-$this->pos);
+ $this->char = null;
+ $this->pos = $this->size;
+ return $ret;
+ }
+
+ if ($pos===$this->pos) return '';
+
+ if ($this->doc[$pos-1]==='\\') {
+ $start = $pos+1;
+ continue;
+ }
+
+ $pos_old = $this->pos;
+ $this->char = $this->doc[$pos];
+ $this->pos = $pos;
+ return substr($this->doc, $pos_old, $pos-$pos_old);
+ }
+ }
+
+ // remove noise from html content
+ // save the noise in the $this->noise array.
+ protected function remove_noise($pattern, $remove_tag=false)
+ {
+ global $debug_object;
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
+
+ $count = preg_match_all($pattern, $this->doc, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE);
+
+ for ($i=$count-1; $i>-1; --$i)
+ {
+ $key = '___noise___'.sprintf('% 5d', count($this->noise)+1000);
+ if (is_object($debug_object)) { $debug_object->debug_log(2, 'key is: ' . $key); }
+ $idx = ($remove_tag) ? 0 : 1;
+ $this->noise[$key] = $matches[$i][$idx][0];
+ $this->doc = substr_replace($this->doc, $key, $matches[$i][$idx][1], strlen($matches[$i][$idx][0]));
+ }
+
+ // reset the length of content
+ $this->size = strlen($this->doc);
+ if ($this->size>0)
+ {
+ $this->char = $this->doc[0];
+ }
+ }
+
+ // restore noise to html content
+ function restore_noise($text)
+ {
+ global $debug_object;
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
+
+ while (($pos=strpos($text, '___noise___'))!==false)
+ {
+ // Sometimes there is a broken piece of markup, and we don't GET the pos+11 etc... token which indicates a problem outside of us...
+ if (strlen($text) > $pos+15)
+ {
+ $key = '___noise___'.$text[$pos+11].$text[$pos+12].$text[$pos+13].$text[$pos+14].$text[$pos+15];
+ if (is_object($debug_object)) { $debug_object->debug_log(2, 'located key of: ' . $key); }
+
+ if (isset($this->noise[$key]))
+ {
+ $text = substr($text, 0, $pos).$this->noise[$key].substr($text, $pos+16);
+ }
+ else
+ {
+ // do this to prevent an infinite loop.
+ $text = substr($text, 0, $pos).'UNDEFINED NOISE FOR KEY: '.$key . substr($text, $pos+16);
+ }
+ }
+ else
+ {
+ // There is no valid key being given back to us... We must get rid of the ___noise___ or we will have a problem.
+ $text = substr($text, 0, $pos).'NO NUMERIC NOISE KEY' . substr($text, $pos+11);
+ }
+ }
+ return $text;
+ }
+
+ // Sometimes we NEED one of the noise elements.
+ function search_noise($text)
+ {
+ global $debug_object;
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
+
+ foreach($this->noise as $noiseElement)
+ {
+ if (strpos($noiseElement, $text)!==false)
+ {
+ return $noiseElement;
+ }
+ }
+ }
+ function __toString()
+ {
+ return $this->root->innertext();
+ }
+
+ function __get($name)
+ {
+ switch ($name)
+ {
+ case 'outertext':
+ return $this->root->innertext();
+ case 'innertext':
+ return $this->root->innertext();
+ case 'plaintext':
+ return $this->root->text();
+ case 'charset':
+ return $this->_charset;
+ case 'target_charset':
+ return $this->_target_charset;
+ }
+ }
+
+ // camel naming conventions
+ function childNodes($idx=-1) {return $this->root->childNodes($idx);}
+ function firstChild() {return $this->root->first_child();}
+ function lastChild() {return $this->root->last_child();}
+ function createElement($name, $value=null) {return @str_get_html("<$name>$value</$name>")->first_child();}
+ function createTextNode($value) {return @end(str_get_html($value)->nodes);}
+ function getElementById($id) {return $this->find("#$id", 0);}
+ function getElementsById($id, $idx=null) {return $this->find("#$id", $idx);}
+ function getElementByTagName($name) {return $this->find($name, 0);}
+ function getElementsByTagName($name, $idx=-1) {return $this->find($name, $idx);}
+ function loadFile() {$args = func_get_args();$this->load_file($args);}
+}
+
+?>