summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md211
-rw-r--r--actions/DetectAction.php53
-rw-r--r--actions/DisplayAction.php235
-rw-r--r--actions/ListAction.php56
-rw-r--r--app.json8
-rw-r--r--bridges/AO3Bridge.php121
-rw-r--r--bridges/AllocineFRBridge.php1
-rw-r--r--bridges/AmazonBridge.php2
-rw-r--r--bridges/AmazonPriceTrackerBridge.php1
-rw-r--r--bridges/AppleMusicBridge.php62
-rw-r--r--bridges/ArtStationBridge.php93
-rw-r--r--bridges/Arte7Bridge.php3
-rw-r--r--bridges/AsahiShimbunAJWBridge.php72
-rw-r--r--bridges/AtmoNouvelleAquitaineBridge.php4638
-rw-r--r--bridges/AutoJMBridge.php195
-rw-r--r--bridges/BAEBridge.php4
-rw-r--r--bridges/BadDragonBridge.php435
-rw-r--r--bridges/BakaUpdatesMangaReleasesBridge.php103
-rw-r--r--bridges/BandcampBridge.php76
-rw-r--r--bridges/BinanceBridge.php103
-rw-r--r--bridges/BingSearchBridge.php119
-rw-r--r--bridges/BrutBridge.php157
-rw-r--r--bridges/BundesbankBridge.php1
-rw-r--r--bridges/CNETFranceBridge.php63
-rw-r--r--bridges/CachetBridge.php134
-rw-r--r--bridges/CastorusBridge.php2
-rw-r--r--bridges/ComboiosDePortugalBridge.php22
-rw-r--r--bridges/ContainerLinuxReleasesBridge.php1
-rw-r--r--bridges/CourrierInternationalBridge.php2
-rw-r--r--bridges/CuriousCatBridge.php109
-rw-r--r--bridges/DailymotionBridge.php152
-rw-r--r--bridges/DanbooruBridge.php2
-rw-r--r--bridges/DavesTrailerPageBridge.php27
-rw-r--r--bridges/DealabsBridge.php11
-rw-r--r--bridges/DemoBridge.php46
-rw-r--r--bridges/DemonoidBridge.php169
-rw-r--r--bridges/DesoutterBridge.php9
-rw-r--r--bridges/DollbooruBridge.php9
-rw-r--r--bridges/EconomistBridge.php63
-rw-r--r--bridges/EliteDangerousGalnetBridge.php3
-rw-r--r--bridges/ElloBridge.php8
-rw-r--r--bridges/EngadgetBridge.php26
-rw-r--r--bridges/ExtremeDownloadBridge.php1
-rw-r--r--bridges/FB2Bridge.php14
-rw-r--r--bridges/FDroidBridge.php7
-rw-r--r--bridges/FabriceBellardBridge.php36
-rw-r--r--bridges/FacebookBridge.php17
-rw-r--r--bridges/FeedExpanderExampleBridge.php62
-rw-r--r--bridges/FicbookBridge.php164
-rw-r--r--bridges/FindACrewBridge.php15
-rw-r--r--bridges/FurAffinityBridge.php918
-rw-r--r--bridges/GBAtempBridge.php1
-rw-r--r--bridges/GOGBridge.php8
-rw-r--r--bridges/GQMagazineBridge.php74
-rw-r--r--bridges/GiteaBridge.php27
-rw-r--r--bridges/GithubIssueBridge.php100
-rw-r--r--bridges/GithubSearchBridge.php2
-rw-r--r--bridges/GlassdoorBridge.php18
-rw-r--r--bridges/GlowficBridge.php88
-rw-r--r--bridges/GogsBridge.php206
-rw-r--r--bridges/GooglePlusPostBridge.php208
-rw-r--r--bridges/HDWallpapersBridge.php10
-rw-r--r--bridges/HaveIBeenPwnedBridge.php138
-rw-r--r--bridges/HeiseBridge.php75
-rw-r--r--bridges/HentaiHavenBridge.php2
-rw-r--r--bridges/HotUKDealsBridge.php4
-rw-r--r--bridges/IGNBridge.php55
-rw-r--r--bridges/IndeedBridge.php245
-rw-r--r--bridges/InstagramBridge.php90
-rw-r--r--bridges/InstructablesBridge.php494
-rw-r--r--bridges/InternetArchiveBridge.php293
-rw-r--r--bridges/IvooxBridge.php128
-rw-r--r--bridges/JustETFBridge.php1
-rw-r--r--bridges/KununuBridge.php41
-rw-r--r--bridges/LaCentraleBridge.php477
-rw-r--r--bridges/LeBonCoinBridge.php1
-rw-r--r--bridges/LeMondeInformatiqueBridge.php5
-rw-r--r--bridges/MangareaderBridge.php1
-rw-r--r--bridges/MastodonBridge.php89
-rw-r--r--bridges/MediapartBridge.php60
-rw-r--r--bridges/MozillaBugTrackerBridge.php153
-rw-r--r--bridges/MozillaSecurityBridge.php3
-rw-r--r--bridges/MydealsBridge.php4
-rw-r--r--bridges/NYTBridge.php26
-rw-r--r--bridges/NationalGeographicBridge.php194
-rw-r--r--bridges/NineGagBridge.php3
-rw-r--r--bridges/NotAlwaysBridge.php3
-rw-r--r--bridges/NovelUpdatesBridge.php2
-rw-r--r--bridges/OnVaSortirBridge.php1
-rw-r--r--bridges/OneFortuneADayBridge.php12
-rw-r--r--bridges/OpenClassroomsBridge.php1
-rw-r--r--bridges/PatreonBridge.php203
-rw-r--r--bridges/PikabuBridge.php56
-rw-r--r--bridges/PinterestBridge.php90
-rw-r--r--bridges/PirateCommunityBridge.php88
-rw-r--r--bridges/QPlayBridge.php132
-rw-r--r--bridges/RadioMelodieBridge.php91
-rw-r--r--bridges/RoadAndTrackBridge.php68
-rw-r--r--bridges/Rue89Bridge.php5
-rw-r--r--bridges/Rule34pahealBridge.php17
-rw-r--r--bridges/SIMARBridge.php63
-rw-r--r--bridges/SakugabooruBridge.php11
-rw-r--r--bridges/ShanaprojectBridge.php161
-rw-r--r--bridges/SkimfeedBridge.php2
-rw-r--r--bridges/SoundcloudBridge.php14
-rw-r--r--bridges/SplCenterBridge.php64
-rw-r--r--bridges/SteamBridge.php91
-rw-r--r--bridges/SteamCommunityBridge.php191
-rw-r--r--bridges/StockFilingsBridge.php80
-rw-r--r--bridges/StoriesIGBridge.php47
-rw-r--r--bridges/TelegramBridge.php301
-rw-r--r--bridges/TheGuardianBridge.php96
-rw-r--r--bridges/ThePirateBayBridge.php9
-rw-r--r--bridges/TwitchBridge.php202
-rw-r--r--bridges/TwitterBridge.php130
-rw-r--r--bridges/UnsplashBridge.php59
-rw-r--r--bridges/VMwareSecurityBridge.php31
-rw-r--r--bridges/VimeoBridge.php175
-rw-r--r--bridges/VkBridge.php9
-rw-r--r--bridges/WikiLeaksBridge.php2
-rw-r--r--bridges/WikipediaBridge.php2
-rw-r--r--bridges/WiredBridge.php102
-rw-r--r--bridges/WordPressPluginUpdateBridge.php12
-rw-r--r--bridges/WorldOfTanksBridge.php20
-rw-r--r--bridges/XenForoBridge.php32
-rw-r--r--bridges/YoutubeBridge.php109
-rw-r--r--cache/pages/.gitkeep0
-rw-r--r--cache/server/.gitkeep0
-rw-r--r--caches/FileCache.php72
-rw-r--r--caches/MemcachedCache.php115
-rw-r--r--caches/SQLiteCache.php121
-rw-r--r--composer.json12
-rw-r--r--composer.lock26
-rw-r--r--config.default.ini.php21
-rw-r--r--debian/changelog15
-rw-r--r--debian/compat1
-rw-r--r--debian/control13
-rw-r--r--debian/copyright4
-rw-r--r--debian/rss-bridge.install5
-rwxr-xr-xdebian/rules3
-rw-r--r--debian/tests/control4
-rw-r--r--debian/tests/general2
-rwxr-xr-xdebian/tests/whitelist15
-rw-r--r--formats/AtomFormat.php97
-rw-r--r--formats/HtmlFormat.php30
-rw-r--r--formats/JsonFormat.php41
-rw-r--r--formats/MrssFormat.php134
-rw-r--r--index.php308
-rw-r--r--lib/ActionAbstract.php33
-rw-r--r--lib/ActionFactory.php65
-rw-r--r--lib/ActionInterface.php34
-rw-r--r--lib/BridgeAbstract.php12
-rw-r--r--lib/BridgeCard.php25
-rw-r--r--lib/BridgeFactory.php (renamed from lib/Bridge.php)132
-rw-r--r--lib/BridgeList.php17
-rw-r--r--lib/CacheFactory.php (renamed from lib/Cache.php)135
-rw-r--r--lib/CacheInterface.php40
-rw-r--r--lib/Configuration.php115
-rw-r--r--lib/Exceptions.php26
-rw-r--r--lib/FactoryAbstract.php70
-rw-r--r--lib/FeedExpander.php2
-rw-r--r--lib/FeedItem.php44
-rw-r--r--lib/FormatFactory.php (renamed from lib/Format.php)131
-rw-r--r--lib/ParameterValidator.php8
-rw-r--r--lib/contents.php71
-rw-r--r--lib/html.php33
-rw-r--r--lib/rssbridge.php48
-rw-r--r--static/HtmlFormat.css32
-rw-r--r--static/favicon.pngbin0 -> 3007 bytes
-rw-r--r--static/favicon.svg122
-rw-r--r--static/logo.svg162
-rw-r--r--static/logo_300px.pngbin0 -> 11546 bytes
-rw-r--r--static/logo_600px.pngbin0 -> 24072 bytes
-rw-r--r--static/style.css84
-rw-r--r--vendor/simplehtmldom/LICENSE21
-rw-r--r--vendor/simplehtmldom/simple_html_dom.php2586
-rw-r--r--whitelist.default.txt (renamed from debian/whitelist.txt)3
177 files changed, 16516 insertions, 3523 deletions
diff --git a/README.md b/README.md
index 4e2c7b1..66632f9 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-rss-bridge
+![RSS-Bridge](static/logo_600px.png)
===
-[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![Debian Release](https://img.shields.io/badge/dynamic/json.svg?label=debian%20release&url=https%3A%2F%2Fsources.debian.org%2Fapi%2Fsrc%2Frss-bridge%2F&query=%24.versions%5B0%5D.version&colorB=blue)](https://tracker.debian.org/pkg/rss-bridge) [![Guix Release](https://img.shields.io/badge/guix%20release-unknown-light--gray.svg)](https://www.gnu.org/software/guix/packages/R/) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge) [![Docker Build Status](https://img.shields.io/docker/build/rssbridge/rss-bridge.svg)](https://hub.docker.com/r/rssbridge/rss-bridge/)
+[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg?logo=github)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![Debian Release](https://img.shields.io/badge/dynamic/json.svg?logo=debian&label=debian%20release&url=https%3A%2F%2Fsources.debian.org%2Fapi%2Fsrc%2Frss-bridge%2F&query=%24.versions%5B0%5D.version&colorB=blue)](https://tracker.debian.org/pkg/rss-bridge) [![Guix Release](https://img.shields.io/badge/guix%20release-unknown-blue.svg)](https://www.gnu.org/software/guix/packages/R/) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge) [![Docker Build Status](https://img.shields.io/docker/build/rssbridge/rss-bridge.svg?logo=docker)](https://hub.docker.com/r/rssbridge/rss-bridge/)
RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites which don't have one. It can be used on webservers or as stand alone application in CLI mode.
@@ -15,7 +15,6 @@ Supported sites/pages (examples)
* `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
@@ -66,6 +65,7 @@ RSS-Bridge requires PHP 5.6 or higher with following extensions enabled:
- [`simplexml`](https://secure.php.net/manual/en/book.simplexml.php)
- [`curl`](https://secure.php.net/manual/en/book.curl.php)
- [`json`](https://secure.php.net/manual/en/book.json.php)
+ - [`sqlite3`](http://php.net/manual/en/book.sqlite3.php) (only when using SQLiteCache)
Find more information on our [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
@@ -84,7 +84,7 @@ Deploy
Thanks to the community, hosting your own instance of RSS-Bridge is as easy as clicking a button!
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/sebsauvage/rss-bridge)
-[![Deploy to Docker Cloud](https://files.cloud.docker.com/images/deploy-to-dockercloud.svg)](https://cloud.docker.com/stack/deploy/?repo=https://github.com/rss-bridge/rss-bridge)
+[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
Getting involved
===
@@ -110,98 +110,117 @@ Use this script to generate the list automatically (using the GitHub API):
https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
-->
- * [16mhz](https://github.com/16mhz)
- * [Ahiles3005](https://github.com/Ahiles3005)
- * [Albirew](https://github.com/Albirew)
- * [AmauryCarrade](https://github.com/AmauryCarrade)
- * [AntoineTurmel](https://github.com/AntoineTurmel)
- * [ArthurHoaro](https://github.com/ArthurHoaro)
- * [Astalaseven](https://github.com/Astalaseven)
- * [Astyan-42](https://github.com/Astyan-42)
- * [Daiyousei](https://github.com/Daiyousei)
- * [Djuuu](https://github.com/Djuuu)
- * [Draeli](https://github.com/Draeli)
- * [EtienneM](https://github.com/EtienneM)
- * [Frenzie](https://github.com/Frenzie)
- * [Ginko-Aloe](https://github.com/Ginko-Aloe)
- * [Glandos](https://github.com/Glandos)
- * [GregThib](https://github.com/GregThib)
- * [Grummfy](https://github.com/Grummfy)
- * [JackNUMBER](https://github.com/JackNUMBER)
- * [JeremyRand](https://github.com/JeremyRand)
- * [Jocker666z](https://github.com/Jocker666z)
- * [LogMANOriginal](https://github.com/LogMANOriginal)
- * [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
- * [Nono-m0le](https://github.com/Nono-m0le)
- * [ORelio](https://github.com/ORelio)
- * [PaulVayssiere](https://github.com/PaulVayssiere)
- * [Piranhaplant](https://github.com/Piranhaplant)
- * [Riduidel](https://github.com/Riduidel)
- * [Roliga](https://github.com/Roliga)
- * [Strubbl](https://github.com/Strubbl)
- * [TheRadialActive](https://github.com/TheRadialActive)
- * [TwizzyDizzy](https://github.com/TwizzyDizzy)
- * [WalterBarrett](https://github.com/WalterBarrett)
- * [ZeNairolf](https://github.com/ZeNairolf)
- * [adamchainz](https://github.com/adamchainz)
- * [aledeg](https://github.com/aledeg)
- * [alexAubin](https://github.com/alexAubin)
- * [az5he6ch](https://github.com/az5he6ch)
- * [b1nj](https://github.com/b1nj)
- * [benasse](https://github.com/benasse)
- * [captn3m0](https://github.com/captn3m0)
- * [chemel](https://github.com/chemel)
- * [ckiw](https://github.com/ckiw)
- * [cnlpete](https://github.com/cnlpete)
- * [corenting](https://github.com/corenting)
- * [couraudt](https://github.com/couraudt)
- * [da2x](https://github.com/da2x)
- * [disk0x](https://github.com/disk0x)
- * [eMerzh](https://github.com/eMerzh)
- * [em92](https://github.com/em92)
- * [fluffy-critter](https://github.com/fluffy-critter)
- * [fulmeek](https://github.com/fulmeek)
- * [griffaurel](https://github.com/griffaurel)
- * [hunhejj](https://github.com/hunhejj)
- * [j0k3r](https://github.com/j0k3r)
- * [jdigilio](https://github.com/jdigilio)
- * [kranack](https://github.com/kranack)
- * [kraoc](https://github.com/kraoc)
- * [laBecasse](https://github.com/laBecasse)
- * [lagaisse](https://github.com/lagaisse)
- * [lalannev](https://github.com/lalannev)
- * [ldidry](https://github.com/ldidry)
- * [lorenzos](https://github.com/lorenzos)
- * [m0zes](https://github.com/m0zes)
- * [matthewseal](https://github.com/matthewseal)
- * [mcbyte-it](https://github.com/mcbyte-it)
- * [mdemoss](https://github.com/mdemoss)
- * [melangue](https://github.com/melangue)
- * [metaMMA](https://github.com/metaMMA)
- * [mickael-bertrand](https://github.com/mickael-bertrand)
- * [mitsukarenai](https://github.com/mitsukarenai)
- * [mr-flibble](https://github.com/mr-flibble)
- * [mro](https://github.com/mro)
- * [mxmehl](https://github.com/mxmehl)
- * [nel50n](https://github.com/nel50n)
- * [niawag](https://github.com/niawag)
- * [pellaeon](https://github.com/pellaeon)
- * [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
- * [pitchoule](https://github.com/pitchoule)
- * [pmaziere](https://github.com/pmaziere)
- * [prysme01](https://github.com/prysme01)
- * [quentinus95](https://github.com/quentinus95)
- * [qwertygc](https://github.com/qwertygc)
- * [regisenguehard](https://github.com/regisenguehard)
- * [rogerdc](https://github.com/rogerdc)
- * [sebsauvage](https://github.com/sebsauvage)
- * [sublimz](https://github.com/sublimz)
- * [sysadminstory](https://github.com/sysadminstory)
- * [tameroski](https://github.com/tameroski)
- * [teromene](https://github.com/teromene)
- * [triatic](https://github.com/triatic)
- * [wtuuju](https://github.com/wtuuju)
- * [yardenac](https://github.com/yardenac)
+* [16mhz](https://github.com/16mhz)
+* [adamchainz](https://github.com/adamchainz)
+* [Ahiles3005](https://github.com/Ahiles3005)
+* [Albirew](https://github.com/Albirew)
+* [aledeg](https://github.com/aledeg)
+* [alex73](https://github.com/alex73)
+* [alexAubin](https://github.com/alexAubin)
+* [AmauryCarrade](https://github.com/AmauryCarrade)
+* [ArthurHoaro](https://github.com/ArthurHoaro)
+* [Astalaseven](https://github.com/Astalaseven)
+* [Astyan-42](https://github.com/Astyan-42)
+* [az5he6ch](https://github.com/az5he6ch)
+* [azdkj532](https://github.com/azdkj532)
+* [b1nj](https://github.com/b1nj)
+* [benasse](https://github.com/benasse)
+* [captn3m0](https://github.com/captn3m0)
+* [chemel](https://github.com/chemel)
+* [ckiw](https://github.com/ckiw)
+* [cnlpete](https://github.com/cnlpete)
+* [corenting](https://github.com/corenting)
+* [couraudt](https://github.com/couraudt)
+* [da2x](https://github.com/da2x)
+* [Daiyousei](https://github.com/Daiyousei)
+* [dawidsowa](https://github.com/dawidsowa)
+* [disk0x](https://github.com/disk0x)
+* [DJCrashdummy](https://github.com/DJCrashdummy)
+* [Djuuu](https://github.com/Djuuu)
+* [DnAp](https://github.com/DnAp)
+* [Draeli](https://github.com/Draeli)
+* [Dreckiger-Dan](https://github.com/Dreckiger-Dan)
+* [em92](https://github.com/em92)
+* [eMerzh](https://github.com/eMerzh)
+* [EtienneM](https://github.com/EtienneM)
+* [floviolleau](https://github.com/floviolleau)
+* [fluffy-critter](https://github.com/fluffy-critter)
+* [Frenzie](https://github.com/Frenzie)
+* [fulmeek](https://github.com/fulmeek)
+* [Ginko-Aloe](https://github.com/Ginko-Aloe)
+* [Glandos](https://github.com/Glandos)
+* [GregThib](https://github.com/GregThib)
+* [griffaurel](https://github.com/griffaurel)
+* [Grummfy](https://github.com/Grummfy)
+* [hunhejj](https://github.com/hunhejj)
+* [husim0](https://github.com/husim0)
+* [IceWreck](https://github.com/IceWreck)
+* [j0k3r](https://github.com/j0k3r)
+* [JackNUMBER](https://github.com/JackNUMBER)
+* [jdigilio](https://github.com/jdigilio)
+* [JeremyRand](https://github.com/JeremyRand)
+* [Jocker666z](https://github.com/Jocker666z)
+* [johnnygroovy](https://github.com/johnnygroovy)
+* [killruana](https://github.com/killruana)
+* [klimplant](https://github.com/klimplant)
+* [kranack](https://github.com/kranack)
+* [kraoc](https://github.com/kraoc)
+* [l1n](https://github.com/l1n)
+* [laBecasse](https://github.com/laBecasse)
+* [lagaisse](https://github.com/lagaisse)
+* [lalannev](https://github.com/lalannev)
+* [Leomaradan](https://github.com/Leomaradan)
+* [ldidry](https://github.com/ldidry)
+* [Limero](https://github.com/Limero)
+* [LogMANOriginal](https://github.com/LogMANOriginal)
+* [lorenzos](https://github.com/lorenzos)
+* [m0zes](https://github.com/m0zes)
+* [matthewseal](https://github.com/matthewseal)
+* [mcbyte-it](https://github.com/mcbyte-it)
+* [mdemoss](https://github.com/mdemoss)
+* [melangue](https://github.com/melangue)
+* [metaMMA](https://github.com/metaMMA)
+* [mitsukarenai](https://github.com/mitsukarenai)
+* [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
+* [mr-flibble](https://github.com/mr-flibble)
+* [mro](https://github.com/mro)
+* [mxmehl](https://github.com/mxmehl)
+* [nel50n](https://github.com/nel50n)
+* [niawag](https://github.com/niawag)
+* [Nono-m0le](https://github.com/Nono-m0le)
+* [ObsidianWitch](https://github.com/ObsidianWitch)
+* [ORelio](https://github.com/ORelio)
+* [PaulVayssiere](https://github.com/PaulVayssiere)
+* [pellaeon](https://github.com/pellaeon)
+* [Piranhaplant](https://github.com/Piranhaplant)
+* [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
+* [pitchoule](https://github.com/pitchoule)
+* [pmaziere](https://github.com/pmaziere)
+* [Pofilo](https://github.com/Pofilo)
+* [prysme01](https://github.com/prysme01)
+* [quentinus95](https://github.com/quentinus95)
+* [regisenguehard](https://github.com/regisenguehard)
+* [Riduidel](https://github.com/Riduidel)
+* [rogerdc](https://github.com/rogerdc)
+* [Roliga](https://github.com/Roliga)
+* [sebsauvage](https://github.com/sebsauvage)
+* [somini](https://github.com/somini)
+* [squeek502](https://github.com/squeek502)
+* [Strubbl](https://github.com/Strubbl)
+* [sublimz](https://github.com/sublimz)
+* [sysadminstory](https://github.com/sysadminstory)
+* [tameroski](https://github.com/tameroski)
+* [teromene](https://github.com/teromene)
+* [thefranke](https://github.com/thefranke)
+* [ThePadawan](https://github.com/ThePadawan)
+* [TheRadialActive](https://github.com/TheRadialActive)
+* [triatic](https://github.com/triatic)
+* [VerifiedJoseph](https://github.com/VerifiedJoseph)
+* [WalterBarrett](https://github.com/WalterBarrett)
+* [wtuuju](https://github.com/wtuuju)
+* [xurxof](https://github.com/xurxof)
+* [yardenac](https://github.com/yardenac)
+* [ZeNairolf](https://github.com/ZeNairolf)
Licenses
===
diff --git a/actions/DetectAction.php b/actions/DetectAction.php
new file mode 100644
index 0000000..86605de
--- /dev/null
+++ b/actions/DetectAction.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+class DetectAction extends ActionAbstract {
+ public function execute() {
+ $targetURL = $this->userData['url']
+ or returnClientError('You must specify a url!');
+
+ $format = $this->userData['format']
+ or returnClientError('You must specify a format!');
+
+ $bridgeFac = new \BridgeFactory();
+ $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
+
+ foreach($bridgeFac->getBridgeNames() as $bridgeName) {
+
+ if(!$bridgeFac->isWhitelisted($bridgeName)) {
+ continue;
+ }
+
+ $bridge = $bridgeFac->create($bridgeName);
+
+ if($bridge === false) {
+ continue;
+ }
+
+ $bridgeParams = $bridge->detectParameters($targetURL);
+
+ if(is_null($bridgeParams)) {
+ continue;
+ }
+
+ $bridgeParams['bridge'] = $bridgeName;
+ $bridgeParams['format'] = $format;
+
+ header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
+ die();
+
+ }
+
+ returnClientError('No bridge found for given URL: ' . $targetURL);
+ }
+}
diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php
new file mode 100644
index 0000000..9b4d363
--- /dev/null
+++ b/actions/DisplayAction.php
@@ -0,0 +1,235 @@
+<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+class DisplayAction extends ActionAbstract {
+ public function execute() {
+ $bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null;
+
+ $format = $this->userData['format']
+ or returnClientError('You must specify a format!');
+
+ $bridgeFac = new \BridgeFactory();
+ $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
+
+ // whitelist control
+ if(!$bridgeFac->isWhitelisted($bridge)) {
+ throw new \Exception('This bridge is not whitelisted', 401);
+ die;
+ }
+
+ // Data retrieval
+ $bridge = $bridgeFac->create($bridge);
+
+ $noproxy = array_key_exists('_noproxy', $this->userData)
+ && filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN);
+
+ if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
+ define('NOPROXY', true);
+ }
+
+ // Cache timeout
+ $cache_timeout = -1;
+ if(array_key_exists('_cache_timeout', $this->userData)) {
+
+ if(!CUSTOM_CACHE_TIMEOUT) {
+ unset($this->userData['_cache_timeout']);
+ $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($this->userData);
+ header('Location: ' . $uri, true, 301);
+ die();
+ }
+
+ $cache_timeout = filter_var($this->userData['_cache_timeout'], FILTER_VALIDATE_INT);
+
+ } else {
+ $cache_timeout = $bridge->getCacheTimeout();
+ }
+
+ // Remove parameters that don't concern bridges
+ $bridge_params = array_diff_key(
+ $this->userData,
+ array_fill_keys(
+ array(
+ 'action',
+ 'bridge',
+ 'format',
+ '_noproxy',
+ '_cache_timeout',
+ '_error_time'
+ ), '')
+ );
+
+ // Remove parameters that don't concern caches
+ $cache_params = array_diff_key(
+ $this->userData,
+ array_fill_keys(
+ array(
+ 'action',
+ 'format',
+ '_noproxy',
+ '_cache_timeout',
+ '_error_time'
+ ), '')
+ );
+
+ // Initialize cache
+ $cacheFac = new CacheFactory();
+ $cacheFac->setWorkingDir(PATH_LIB_CACHES);
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope('');
+ $cache->purgeCache(86400); // 24 hours
+ $cache->setKey($cache_params);
+
+ $items = array();
+ $infos = array();
+ $mtime = $cache->getTime();
+
+ if($mtime !== false
+ && (time() - $cache_timeout < $mtime)
+ && !Debug::isEnabled()) { // Load cached data
+
+ // Send "Not Modified" response if client supports it
+ // Implementation based on https://stackoverflow.com/a/10847262
+ if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
+ $stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
+
+ if($mtime <= $stime) { // Cached data is older or same
+ header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304);
+ die();
+ }
+ }
+
+ $cached = $cache->loadData();
+
+ if(isset($cached['items']) && isset($cached['extraInfos'])) {
+ foreach($cached['items'] as $item) {
+ $items[] = new \FeedItem($item);
+ }
+
+ $infos = $cached['extraInfos'];
+ }
+
+ } else { // Collect new data
+
+ try {
+ $bridge->setDatas($bridge_params);
+ $bridge->collectData();
+
+ $items = $bridge->getItems();
+
+ // Transform "legacy" items to FeedItems if necessary.
+ // Remove this code when support for "legacy" items ends!
+ if(isset($items[0]) && is_array($items[0])) {
+ $feedItems = array();
+
+ foreach($items as $item) {
+ $feedItems[] = new \FeedItem($item);
+ }
+
+ $items = $feedItems;
+ }
+
+ $infos = array(
+ 'name' => $bridge->getName(),
+ 'uri' => $bridge->getURI(),
+ 'icon' => $bridge->getIcon()
+ );
+ } catch(Error $e) {
+ error_log($e);
+
+ $item = new \FeedItem();
+
+ // Create "new" error message every 24 hours
+ $this->userData['_error_time'] = urlencode((int)(time() / 86400));
+
+ // Error 0 is a special case (i.e. "trying to get property of non-object")
+ if($e->getCode() === 0) {
+ $item->setTitle(
+ 'Bridge encountered an unexpected situation! ('
+ . $this->userData['_error_time']
+ . ')'
+ );
+ } else {
+ $item->setTitle(
+ 'Bridge returned error '
+ . $e->getCode()
+ . '! ('
+ . $this->userData['_error_time']
+ . ')'
+ );
+ }
+
+ $item->setURI(
+ (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
+ . '?'
+ . http_build_query($this->userData)
+ );
+
+ $item->setTimestamp(time());
+ $item->setContent(buildBridgeException($e, $bridge));
+
+ $items[] = $item;
+ } catch(Exception $e) {
+ error_log($e);
+
+ $item = new \FeedItem();
+
+ // Create "new" error message every 24 hours
+ $this->userData['_error_time'] = urlencode((int)(time() / 86400));
+
+ $item->setURI(
+ (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
+ . '?'
+ . http_build_query($this->userData)
+ );
+
+ $item->setTitle(
+ 'Bridge returned error '
+ . $e->getCode()
+ . '! ('
+ . $this->userData['_error_time']
+ . ')'
+ );
+ $item->setTimestamp(time());
+ $item->setContent(buildBridgeException($e, $bridge));
+
+ $items[] = $item;
+ }
+
+ // Store data in cache
+ $cache->saveData(array(
+ 'items' => array_map(function($i){ return $i->toArray(); }, $items),
+ 'extraInfos' => $infos
+ ));
+
+ }
+
+ // Data transformation
+ try {
+ $formatFac = new FormatFactory();
+ $formatFac->setWorkingDir(PATH_LIB_FORMATS);
+ $format = $formatFac->create($format);
+ $format->setItems($items);
+ $format->setExtraInfos($infos);
+ $format->setLastModified($cache->getTime());
+ $format->display();
+ } catch(Error $e) {
+ error_log($e);
+ header('Content-Type: text/html', true, $e->getCode());
+ die(buildTransformException($e, $bridge));
+ } catch(Exception $e) {
+ error_log($e);
+ header('Content-Type: text/html', true, $e->getCode());
+ die(buildTransformException($e, $bridge));
+ }
+ }
+}
diff --git a/actions/ListAction.php b/actions/ListAction.php
new file mode 100644
index 0000000..92aef0e
--- /dev/null
+++ b/actions/ListAction.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+class ListAction extends ActionAbstract {
+ public function execute() {
+ $list = new StdClass();
+ $list->bridges = array();
+ $list->total = 0;
+
+ $bridgeFac = new \BridgeFactory();
+ $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
+
+ foreach($bridgeFac->getBridgeNames() as $bridgeName) {
+
+ $bridge = $bridgeFac->create($bridgeName);
+
+ if($bridge === false) { // Broken bridge, show as inactive
+
+ $list->bridges[$bridgeName] = array(
+ 'status' => 'inactive'
+ );
+
+ continue;
+
+ }
+
+ $status = $bridgeFac->isWhitelisted($bridgeName) ? 'active' : 'inactive';
+
+ $list->bridges[$bridgeName] = array(
+ 'status' => $status,
+ 'uri' => $bridge->getURI(),
+ 'name' => $bridge->getName(),
+ 'icon' => $bridge->getIcon(),
+ 'parameters' => $bridge->getParameters(),
+ 'maintainer' => $bridge->getMaintainer(),
+ 'description' => $bridge->getDescription()
+ );
+
+ }
+
+ $list->total = count($list->bridges);
+
+ header('Content-Type: application/json');
+ echo json_encode($list, JSON_PRETTY_PRINT);
+ }
+}
diff --git a/app.json b/app.json
new file mode 100644
index 0000000..f184799
--- /dev/null
+++ b/app.json
@@ -0,0 +1,8 @@
+{
+ "service": "Heroku",
+ "name": "RSS-Bridge",
+ "description": "RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites which don't have one.",
+ "repository": "https://github.com/RSS-Bridge/rss-bridge",
+ "keywords": ["php", "rss-bridge", "rss"]
+}
+
diff --git a/bridges/AO3Bridge.php b/bridges/AO3Bridge.php
new file mode 100644
index 0000000..9a3b5c8
--- /dev/null
+++ b/bridges/AO3Bridge.php
@@ -0,0 +1,121 @@
+<?php
+
+class AO3Bridge extends BridgeAbstract {
+ const NAME = 'AO3';
+ const URI = 'https://archiveofourown.org/';
+ const CACHE_TIMEOUT = 1800;
+ const DESCRIPTION = 'Returns works or chapters from Archive of Our Own';
+ const MAINTAINER = 'Obsidienne';
+ const PARAMETERS = array(
+ 'List' => array(
+ 'url' => array(
+ 'name' => 'url',
+ 'required' => true,
+ // Example: F/F tag, complete works only
+ 'exampleValue' => self::URI
+ . 'works?work_search[complete]=T&tag_id=F*s*F',
+ ),
+ ),
+ 'Bookmarks' => array(
+ 'user' => array(
+ 'name' => 'user',
+ 'required' => true,
+ // Example: Nyaaru's bookmarks
+ 'exampleValue' => 'Nyaaru',
+ ),
+ ),
+ 'Work' => array(
+ 'id' => array(
+ 'name' => 'id',
+ 'required' => true,
+ // Example: latest chapters from A Better Past by LysSerris
+ 'exampleValue' => '18181853',
+ ),
+ )
+ );
+
+ // Feed for lists of works (e.g. recent works, search results, filtered tags,
+ // bookmarks, series, collections).
+ private function collectList($url) {
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('could not request AO3');
+ $html = defaultLinkTo($html, self::URI);
+
+ foreach($html->find('.index.group > li') as $element) {
+ $item = array();
+
+ $title = $element->find('div h4 a', 0);
+ if (!isset($title)) continue; // discard deleted works
+ $item['title'] = $title->plaintext;
+ $item['content'] = $element;
+ $item['uri'] = $title->href;
+
+ $strdate = $element->find('div p.datetime', 0)->plaintext;
+ $item['timestamp'] = strtotime($strdate);
+
+ $chapters = $element->find('dl dd.chapters', 0);
+ // bookmarked series and external works do not have a chapters count
+ $chapters = (isset($chapters) ? $chapters->plaintext : 0);
+ $item['uid'] = $item['uri'] . "/$strdate/$chapters";
+
+ $this->items[] = $item;
+ }
+ }
+
+ // Feed for recent chapters of a specific work.
+ private function collectWork($id) {
+ $url = self::URI . "/works/$id/navigate";
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('could not request AO3');
+ $html = defaultLinkTo($html, self::URI);
+
+ $this->title = $html->find('h2 a', 0)->plaintext;
+
+ foreach($html->find('ol.index.group > li') as $element) {
+ $item = array();
+
+ $item['title'] = $element->find('a', 0)->plaintext;
+ $item['content'] = $element;
+ $item['uri'] = $element->find('a', 0)->href;
+
+ $strdate = $element->find('span.datetime', 0)->plaintext;
+ $strdate = str_replace('(', '', $strdate);
+ $strdate = str_replace(')', '', $strdate);
+ $item['timestamp'] = strtotime($strdate);
+
+ $item['uid'] = $item['uri'] . "/$strdate";
+
+ $this->items[] = $item;
+ }
+
+ $this->items = array_reverse($this->items);
+ }
+
+ public function collectData() {
+ switch($this->queriedContext) {
+ case 'Bookmarks':
+ $user = $this->getInput('user');
+ $this->title = $user;
+ $url = self::URI
+ . '/users/' . $user
+ . '/bookmarks?bookmark_search[sort_column]=bookmarkable_date';
+ return $this->collectList($url);
+ case 'List': return $this->collectList(
+ $this->getInput('url')
+ );
+ case 'Work': return $this->collectWork(
+ $this->getInput('id')
+ );
+ }
+ }
+
+ public function getName() {
+ $name = parent::getName() . " $this->queriedContext";
+ if (isset($this->title)) $name .= " - $this->title";
+ return $name;
+ }
+
+ public function getIcon() {
+ return self::URI . '/favicon.ico';
+ }
+}
diff --git a/bridges/AllocineFRBridge.php b/bridges/AllocineFRBridge.php
index 50a41ec..17da903 100644
--- a/bridges/AllocineFRBridge.php
+++ b/bridges/AllocineFRBridge.php
@@ -10,7 +10,6 @@ class AllocineFRBridge extends BridgeAbstract {
'category' => array(
'name' => 'category',
'type' => 'list',
- 'required' => true,
'exampleValue' => 'Faux Raccord',
'title' => 'Select your category',
'values' => array(
diff --git a/bridges/AmazonBridge.php b/bridges/AmazonBridge.php
index c9d4dc9..bcd83dc 100644
--- a/bridges/AmazonBridge.php
+++ b/bridges/AmazonBridge.php
@@ -16,7 +16,6 @@ class AmazonBridge extends BridgeAbstract {
'sort' => array(
'name' => 'Sort by',
'type' => 'list',
- 'required' => false,
'values' => array(
'Relevance' => 'relevanceblender',
'Price: Low to High' => 'price-asc-rank',
@@ -29,7 +28,6 @@ class AmazonBridge extends BridgeAbstract {
'tld' => array(
'name' => 'Country',
'type' => 'list',
- 'required' => true,
'values' => array(
'Australia' => 'com.au',
'Brazil' => 'com.br',
diff --git a/bridges/AmazonPriceTrackerBridge.php b/bridges/AmazonPriceTrackerBridge.php
index e31a03b..6fa11c9 100644
--- a/bridges/AmazonPriceTrackerBridge.php
+++ b/bridges/AmazonPriceTrackerBridge.php
@@ -19,7 +19,6 @@ class AmazonPriceTrackerBridge extends BridgeAbstract {
'tld' => array(
'name' => 'Country',
'type' => 'list',
- 'required' => true,
'values' => array(
'Australia' => 'com.au',
'Brazil' => 'com.br',
diff --git a/bridges/AppleMusicBridge.php b/bridges/AppleMusicBridge.php
new file mode 100644
index 0000000..5a4f40a
--- /dev/null
+++ b/bridges/AppleMusicBridge.php
@@ -0,0 +1,62 @@
+<?php
+
+class AppleMusicBridge extends BridgeAbstract {
+ const NAME = 'Apple Music';
+ const URI = 'https://www.apple.com';
+ const DESCRIPTION = 'Fetches the latest releases from an artist';
+ const MAINTAINER = 'Limero';
+ const PARAMETERS = [[
+ 'url' => [
+ 'name' => 'Artist URL',
+ 'exampleValue' => 'https://itunes.apple.com/us/artist/dunderpatrullen/329796274',
+ 'required' => true,
+ ],
+ 'imgSize' => [
+ 'name' => 'Image size for thumbnails (in px)',
+ 'type' => 'number',
+ 'defaultValue' => 512,
+ 'required' => true,
+ ]
+ ]];
+ const CACHE_TIMEOUT = 21600; // 6 hours
+
+ public function collectData() {
+ $url = $this->getInput('url');
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Could not request: ' . $url);
+
+ $imgSize = $this->getInput('imgSize');
+
+ // Grab the json data from the page
+ $html = $html->find('script[id=shoebox-ember-data-store]', 0);
+ $html = strstr($html, '{');
+ $html = substr($html, 0, -9);
+ $json = json_decode($html);
+
+ // Loop through each object
+ foreach ($json->included as $obj) {
+ if ($obj->type === 'lockup/album') {
+ $this->items[] = [
+ 'title' => $obj->attributes->artistName . ' - ' . $obj->attributes->name,
+ 'uri' => $obj->attributes->url,
+ 'timestamp' => $obj->attributes->releaseDate,
+ 'enclosures' => $obj->relationships->artwork->data->id,
+ ];
+ } elseif ($obj->type === 'image') {
+ $images[$obj->id] = $obj->attributes->url;
+ }
+ }
+
+ // Add the images to each item
+ foreach ($this->items as &$item) {
+ $item['enclosures'] = [
+ str_replace('{w}x{h}bb.{f}', $imgSize . 'x0w.jpg', $images[$item['enclosures']]),
+ ];
+ }
+
+ // Sort the order to put the latest albums first
+ usort($this->items, function($a, $b){
+ return $a['timestamp'] < $b['timestamp'];
+ });
+ }
+}
diff --git a/bridges/ArtStationBridge.php b/bridges/ArtStationBridge.php
new file mode 100644
index 0000000..9c12add
--- /dev/null
+++ b/bridges/ArtStationBridge.php
@@ -0,0 +1,93 @@
+<?php
+class ArtStationBridge extends BridgeAbstract {
+ const NAME = 'ArtStation';
+ const URI = 'https://www.artstation.com';
+ const DESCRIPTION = 'Fetches the latest ten artworks from a search query on ArtStation.';
+ const MAINTAINER = 'thefranke';
+ const CACHE_TIMEOUT = 3600; // 1h
+
+ const PARAMETERS = array(
+ 'Search Query' => array(
+ 'q' => array(
+ 'name' => 'Search term',
+ 'required' => true
+ )
+ )
+ );
+
+ public function getIcon() {
+ return 'https://www.artstation.com/assets/favicon-58653022bc38c1905ac7aa1b10bffa6b.ico';
+ }
+
+ public function getName() {
+ return self::NAME . ': ' . $this->getInput('q');
+ }
+
+ private function fetchSearch($searchQuery) {
+ $data = '{"query":"' . $searchQuery . '","page":1,"per_page":50,"sorting":"date",';
+ $data .= '"pro_first":"1","filters":[],"additional_fields":[]}';
+
+ $header = array(
+ 'Content-Type: application/json',
+ 'Accept: application/json'
+ );
+
+ $opts = array(
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $data,
+ CURLOPT_RETURNTRANSFER => true
+ );
+
+ $jsonSearchURL = self::URI . '/api/v2/search/projects.json';
+ $jsonSearchStr = getContents($jsonSearchURL, $header, $opts)
+ or returnServerError('Could not fetch JSON for search query.');
+ return json_decode($jsonSearchStr);
+ }
+
+ private function fetchProject($hashID) {
+ $jsonProjectURL = self::URI . '/projects/' . $hashID . '.json';
+ $jsonProjectStr = getContents($jsonProjectURL)
+ or returnServerError('Could not fetch JSON for project.');
+ return json_decode($jsonProjectStr);
+ }
+
+ public function collectData() {
+ $searchTerm = $this->getInput('q');
+ $jsonQuery = $this->fetchSearch($searchTerm);
+
+ foreach($jsonQuery->data as $media) {
+ // get detailed info about media item
+ $jsonProject = $this->fetchProject($media->hash_id);
+
+ // create item
+ $item = array();
+ $item['title'] = $media->title;
+ $item['uri'] = $media->url;
+ $item['timestamp'] = strtotime($jsonProject->published_at);
+ $item['author'] = $media->user->full_name;
+ $item['categories'] = implode(',', $jsonProject->tags);
+
+ $item['content'] = '<a href="'
+ . $media->url
+ . '"><img style="max-width: 100%" src="'
+ . $jsonProject->cover_url
+ . '"></a><p>'
+ . $jsonProject->description
+ . '</p>';
+
+ $numAssets = count($jsonProject->assets);
+
+ if ($numAssets > 1)
+ $item['content'] .= '<p><a href="'
+ . $media->url
+ . '">Project contains '
+ . ($numAssets - 1)
+ . ' more item(s).</a></p>';
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10)
+ break;
+ }
+ }
+}
diff --git a/bridges/Arte7Bridge.php b/bridges/Arte7Bridge.php
index ff72211..562f648 100644
--- a/bridges/Arte7Bridge.php
+++ b/bridges/Arte7Bridge.php
@@ -91,7 +91,8 @@ class Arte7Bridge extends BridgeAbstract {
'Authorization: Bearer ' . self::API_TOKEN
);
- $input = getContents($url, $header) or die('Could not request ARTE.');
+ $input = getContents($url, $header)
+ or returnServerError('Could not request ARTE.');
$input_json = json_decode($input, true);
foreach($input_json['videos'] as $element) {
diff --git a/bridges/AsahiShimbunAJWBridge.php b/bridges/AsahiShimbunAJWBridge.php
new file mode 100644
index 0000000..0ceb038
--- /dev/null
+++ b/bridges/AsahiShimbunAJWBridge.php
@@ -0,0 +1,72 @@
+<?php
+class AsahiShimbunAJWBridge extends BridgeAbstract {
+ const NAME = 'Asahi Shimbun AJW';
+ const BASE_URI = 'http://www.asahi.com';
+ const URI = self::BASE_URI . '/ajw/';
+ const DESCRIPTION = 'Asahi Shimbun - Asia & Japan Watch';
+ const MAINTAINER = 'somini';
+ const PARAMETERS = array(
+ array(
+ 'section' => array(
+ 'type' => 'list',
+ 'name' => 'Section',
+ 'values' => array(
+ 'Japan » Social Affairs' => 'japan/social',
+ 'Japan » People' => 'japan/people',
+ 'Japan » 3/11 Disaster' => 'japan/0311disaster',
+ 'Japan » Sci & Tech' => 'japan/sci_tech',
+ 'Politics' => 'politics',
+ 'Business' => 'business',
+ 'Culture » Style' => 'culture/style',
+ 'Culture » Movies' => 'culture/movies',
+ 'Culture » Manga & Anime' => 'culture/manga_anime',
+ 'Asia » China' => 'asia/china',
+ 'Asia » Korean Peninsula' => 'asia/korean_peninsula',
+ 'Asia » Around Asia' => 'asia/around_asia',
+ 'Opinion » Editorial' => 'opinion/editorial',
+ 'Opinion » Vox Populi' => 'opinion/vox',
+ ),
+ 'defaultValue' => 'Politics',
+ )
+ )
+ );
+
+ private function getSectionURI($section) {
+ return self::getURI() . $section . '/';
+ }
+
+ public function collectData() {
+ $html = getSimpleHTMLDOM($this->getSectionURI($this->getInput('section')))
+ or returnServerError('Could not load content');
+
+ foreach($html->find('#MainInner li a') as $element) {
+ if ($element->parent()->class == 'HeadlineTopImage-S') {
+ Debug::log('Skip Headline, it is repeated below');
+ continue;
+ }
+ $item = array();
+
+ $item['uri'] = self::BASE_URI . $element->href;
+ $e_lead = $element->find('span.Lead', 0);
+ if ($e_lead) {
+ $item['content'] = $e_lead->innertext;
+ $e_lead->outertext = '';
+ } else {
+ $item['content'] = $element->innertext;
+ }
+ $e_date = $element->find('span.EnDate', 0);
+ if ($e_date) {
+ $item['timestamp'] = strtotime($e_date->innertext);
+ $e_date->outertext = '';
+ }
+ $e_video = $element->find('span.EnVideo', 0);
+ if ($e_video) {
+ $e_video->outertext = '';
+ $element->innertext = "VIDEO: $element->innertext";
+ }
+ $item['title'] = $element->innertext;
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/AtmoNouvelleAquitaineBridge.php b/bridges/AtmoNouvelleAquitaineBridge.php
new file mode 100644
index 0000000..2ded81a
--- /dev/null
+++ b/bridges/AtmoNouvelleAquitaineBridge.php
@@ -0,0 +1,4638 @@
+<?php
+class AtmoNouvelleAquitaineBridge extends BridgeAbstract {
+
+ const NAME = 'Atmo Nouvelle Aquitaine';
+ const URI = 'https://www.atmo-nouvelleaquitaine.org/monair/commune/';
+ const DESCRIPTION = 'Fetches the latest air polution of Bordeaux from Atmo Nouvelle Aquitaine';
+ const MAINTAINER = 'floviolleau';
+ const PARAMETERS = array(array(
+ 'cities' => array(
+ 'name' => 'Choisir une ville',
+ 'type' => 'list',
+ 'values' => self::CITIES
+ )
+ ));
+ const CACHE_TIMEOUT = 7200;
+
+ private $dom;
+
+ private function getClosest($search, $arr) {
+ $closest = null;
+ foreach ($arr as $key => $value) {
+ if ($closest === null || abs((int)$search - $closest) > abs((int)$key - (int)$search)) {
+ $closest = (int)$key;
+ }
+ }
+ return $arr[$closest];
+ }
+
+ public function collectData() {
+ $uri = self::URI . $this->getInput('cities');
+
+ $html = getSimpleHTMLDOM($uri)
+ or returnServerError('Could not request ' . $uri);
+
+ $this->dom = $html->find('#block-system-main .city-prevision-map', 0);
+
+ $message = $this->getIndexMessage() . ' ' . $this->getQualityMessage();
+ $message .= ' ' . $this->getTomorrowTrendIndexMessage() . ' ' . $this->getTomorrowTrendQualityMessage();
+
+ $item['uri'] = $uri;
+ $today = date('d/m/Y');
+ $item['title'] = "Bulletin de l'air du $today pour la région Nouvelle Aquitaine.";
+ $item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-nouvelleaquitaine.org #QualiteAir.';
+ $item['author'] = 'floviolleau';
+ $item['content'] = $message;
+ $item['uid'] = hash('sha256', $item['title']);
+
+ $this->items[] = $item;
+ }
+
+ private function getIndex() {
+ $index = $this->dom->find('.indice', 0)->innertext;
+
+ if ($index == 'XX') {
+ return -1;
+ }
+
+ return $index;
+ }
+
+ private function getMaxIndexText() {
+ // will return '/100'
+ return $this->dom->find('.pourcent', 0)->innertext;
+ }
+
+ private function getQualityText($index, $indexes) {
+ if ($index == -1) {
+ if (array_key_exists('no-available', $indexes)) {
+ return $indexes['no-available'];
+ }
+
+ return 'Aucune donnée';
+ }
+
+ return $this->getClosest($index, $indexes);
+ }
+
+ private function getLegendIndexes() {
+ $rawIndexes = $this->dom->find('.prevision-legend .prevision-legend-label');
+ $indexes = [];
+ for ($i = 0; $i < count($rawIndexes); $i++) {
+ if ($rawIndexes[$i]->hasAttribute('data-color')) {
+ $indexes[$rawIndexes[$i]->getAttribute('data-color')] = $rawIndexes[$i]->innertext;
+ }
+ }
+
+ return $indexes;
+ }
+
+ private function getTomorrowTrendIndex() {
+ $tomorrowTrendDomNode = $this->dom
+ ->find('.day-controls.raster-controls .list-raster-controls .raster-control', 2);
+ $tomorrowTrendIndexNode = null;
+
+ if ($tomorrowTrendDomNode) {
+ $tomorrowTrendIndexNode = $tomorrowTrendDomNode->find('.raster-control-link', 0);
+ }
+
+ if ($tomorrowTrendIndexNode && $tomorrowTrendIndexNode->hasAttribute('data-index')) {
+ $tomorrowTrendIndex = $tomorrowTrendIndexNode->getAttribute('data-index');
+ } else {
+ return -1;
+ }
+
+ return $tomorrowTrendIndex;
+ }
+
+ private function getTomorrowTrendQualityText($trendIndex, $indexes) {
+ if ($trendIndex == -1) {
+ if (array_key_exists('no-available', $indexes)) {
+ return $indexes['no-available'];
+ }
+
+ return 'Aucune donnée';
+ }
+
+ return $this->getClosest($trendIndex, $indexes);
+ }
+
+ private function getIndexMessage() {
+ $index = $this->getIndex();
+ $maxIndexText = $this->getMaxIndexText();
+
+ if ($index == -1) {
+ return 'Aucune donnée pour l\'indice.';
+ }
+
+ return "L'indice d'aujourd'hui est $index$maxIndexText.";
+ }
+
+ private function getQualityMessage() {
+ $index = $index = $this->getIndex();
+ $indexes = $this->getLegendIndexes();
+ $quality = $this->getQualityText($index, $indexes);
+
+ if ($index == -1) {
+ return 'Aucune donnée pour la qualité de l\'air.';
+ }
+
+ return "La qualité de l'air est $quality.";
+ }
+
+ private function getTomorrowTrendIndexMessage() {
+ $trendIndex = $this->getTomorrowTrendIndex();
+ $maxIndexText = $this->getMaxIndexText();
+
+ if ($trendIndex == -1) {
+ return 'Aucune donnée pour l\'indice prévu demain.';
+ }
+
+ return "L'indice prévu pour demain est $trendIndex$maxIndexText.";
+ }
+
+ private function getTomorrowTrendQualityMessage() {
+ $trendIndex = $this->getTomorrowTrendIndex();
+ $indexes = $this->getLegendIndexes();
+ $trendQuality = $this->getTomorrowTrendQualityText($trendIndex, $indexes);
+
+ if ($trendIndex == -1) {
+ return 'Aucune donnée pour la qualité de l\'air de demain.';
+ }
+ return "La qualite de l'air pour demain sera $trendQuality.";
+ }
+
+ const CITIES = array(
+ 'Aast (64460)' => '64001',
+ 'Abère (64160)' => '64002',
+ 'Abidos (64150)' => '64003',
+ 'Abitain (64390)' => '64004',
+ 'Abjat-sur-Bandiat (24300)' => '24001',
+ 'Abos (64360)' => '64005',
+ 'Abzac (16500)' => '16001',
+ 'Abzac (33230)' => '33001',
+ 'Accous (64490)' => '64006',
+ 'Adilly (79200)' => '79002',
+ 'Adriers (86430)' => '86001',
+ 'Affieux (19260)' => '19001',
+ 'Agen (47000)' => '47001',
+ 'Agmé (47350)' => '47002',
+ 'Agnac (47800)' => '47003',
+ 'Agnos (64400)' => '64007',
+ 'Agonac (24460)' => '24002',
+ 'Agris (16110)' => '16003',
+ 'Agudelle (17500)' => '17002',
+ 'Ahaxe-Alciette-Bascassan (64220)' => '64008',
+ 'Ahetze (64210)' => '64009',
+ 'Ahun (23150)' => '23001',
+ 'Aïcirits-Camou-Suhast (64120)' => '64010',
+ 'Aiffres (79230)' => '79003',
+ 'Aignes-et-Puypéroux (16190)' => '16004',
+ 'Aigonnay (79370)' => '79004',
+ 'Aigre (16140)' => '16005',
+ 'Aigrefeuille-d\'Aunis (17290)' => '17003',
+ 'Aiguillon (47190)' => '47004',
+ 'Aillas (33124)' => '33002',
+ 'Aincille (64220)' => '64011',
+ 'Ainharp (64130)' => '64012',
+ 'Ainhice-Mongelos (64220)' => '64013',
+ 'Ainhoa (64250)' => '64014',
+ 'Aire-sur-l\'Adour (40800)' => '40001',
+ 'Airvault (79600)' => '79005',
+ 'Aix (19200)' => '19002',
+ 'Aixe-sur-Vienne (87700)' => '87001',
+ 'Ajain (23380)' => '23002',
+ 'Ajat (24210)' => '24004',
+ 'Albignac (19190)' => '19003',
+ 'Albussac (19380)' => '19004',
+ 'Alçay-Alçabéhéty-Sunharette (64470)' => '64015',
+ 'Aldudes (64430)' => '64016',
+ 'Allas-Bocage (17150)' => '17005',
+ 'Allas-Champagne (17500)' => '17006',
+ 'Allas-les-Mines (24220)' => '24006',
+ 'Allassac (19240)' => '19005',
+ 'Allemans (24600)' => '24007',
+ 'Allemans-du-Dropt (47800)' => '47005',
+ 'Alles-sur-Dordogne (24480)' => '24005',
+ 'Alleyrat (19200)' => '19006',
+ 'Alleyrat (23200)' => '23003',
+ 'Allez-et-Cazeneuve (47110)' => '47006',
+ 'Allonne (79130)' => '79007',
+ 'Allons (47420)' => '47007',
+ 'Alloue (16490)' => '16007',
+ 'Alos-Sibas-Abense (64470)' => '64017',
+ 'Altillac (19120)' => '19007',
+ 'Amailloux (79350)' => '79008',
+ 'Ambarès-et-Lagrave (33440)' => '33003',
+ 'Ambazac (87240)' => '87002',
+ 'Ambérac (16140)' => '16008',
+ 'Ambernac (16490)' => '16009',
+ 'Amberre (86110)' => '86002',
+ 'Ambès (33810)' => '33004',
+ 'Ambleville (16300)' => '16010',
+ 'Ambrugeat (19250)' => '19008',
+ 'Ambrus (47160)' => '47008',
+ 'Amendeuix-Oneix (64120)' => '64018',
+ 'Amorots-Succos (64120)' => '64019',
+ 'Amou (40330)' => '40002',
+ 'Amuré (79210)' => '79009',
+ 'Anais (16560)' => '16011',
+ 'Anais (17540)' => '17007',
+ 'Ance (64570)' => '64020',
+ 'Anché (86700)' => '86003',
+ 'Andernos-les-Bains (33510)' => '33005',
+ 'Andilly (17230)' => '17008',
+ 'Andiran (47170)' => '47009',
+ 'Andoins (64420)' => '64021',
+ 'Andrein (64390)' => '64022',
+ 'Angaïs (64510)' => '64023',
+ 'Angeac-Champagne (16130)' => '16012',
+ 'Angeac-Charente (16120)' => '16013',
+ 'Angeduc (16300)' => '16014',
+ 'Anglade (33390)' => '33006',
+ 'Angles-sur-l\'Anglin (86260)' => '86004',
+ 'Anglet (64600)' => '64024',
+ 'Angliers (17540)' => '17009',
+ 'Angliers (86330)' => '86005',
+ 'Angoisse (24270)' => '24008',
+ 'Angoulême (16000)' => '16015',
+ 'Angoulins (17690)' => '17010',
+ 'Angoumé (40990)' => '40003',
+ 'Angous (64190)' => '64025',
+ 'Angresse (40150)' => '40004',
+ 'Anhaux (64220)' => '64026',
+ 'Anlhiac (24160)' => '24009',
+ 'Annepont (17350)' => '17011',
+ 'Annesse-et-Beaulieu (24430)' => '24010',
+ 'Annezay (17380)' => '17012',
+ 'Anos (64160)' => '64027',
+ 'Anoye (64350)' => '64028',
+ 'Ansac-sur-Vienne (16500)' => '16016',
+ 'Antagnac (47700)' => '47010',
+ 'Antezant-la-Chapelle (17400)' => '17013',
+ 'Anthé (47370)' => '47011',
+ 'Antigny (86310)' => '86006',
+ 'Antonne-et-Trigonant (24420)' => '24011',
+ 'Antran (86100)' => '86007',
+ 'Anville (16170)' => '16017',
+ 'Anzême (23000)' => '23004',
+ 'Anzex (47700)' => '47012',
+ 'Aramits (64570)' => '64029',
+ 'Arancou (64270)' => '64031',
+ 'Araujuzon (64190)' => '64032',
+ 'Araux (64190)' => '64033',
+ 'Arbanats (33640)' => '33007',
+ 'Arbérats-Sillègue (64120)' => '64034',
+ 'Arbis (33760)' => '33008',
+ 'Arbonne (64210)' => '64035',
+ 'Arboucave (40320)' => '40005',
+ 'Arbouet-Sussaute (64120)' => '64036',
+ 'Arbus (64230)' => '64037',
+ 'Arcachon (33120)' => '33009',
+ 'Arçais (79210)' => '79010',
+ 'Arcangues (64200)' => '64038',
+ 'Arçay (86200)' => '86008',
+ 'Arces (17120)' => '17015',
+ 'Archiac (17520)' => '17016',
+ 'Archignac (24590)' => '24012',
+ 'Archigny (86210)' => '86009',
+ 'Archingeay (17380)' => '17017',
+ 'Arcins (33460)' => '33010',
+ 'Ardilleux (79110)' => '79011',
+ 'Ardillières (17290)' => '17018',
+ 'Ardin (79160)' => '79012',
+ 'Aren (64400)' => '64039',
+ 'Arengosse (40110)' => '40006',
+ 'Arès (33740)' => '33011',
+ 'Aressy (64320)' => '64041',
+ 'Arette (64570)' => '64040',
+ 'Arfeuille-Châtain (23700)' => '23005',
+ 'Argagnon (64300)' => '64042',
+ 'Argelos (40700)' => '40007',
+ 'Argelos (64450)' => '64043',
+ 'Argelouse (40430)' => '40008',
+ 'Argentat (19400)' => '19010',
+ 'Argenton (47250)' => '47013',
+ 'Argenton-l\'Église (79290)' => '79014',
+ 'Argentonnay (79150)' => '79013',
+ 'Arget (64410)' => '64044',
+ 'Arhansus (64120)' => '64045',
+ 'Arjuzanx (40110)' => '40009',
+ 'Armendarits (64640)' => '64046',
+ 'Armillac (47800)' => '47014',
+ 'Arnac-la-Poste (87160)' => '87003',
+ 'Arnac-Pompadour (19230)' => '19011',
+ 'Arnéguy (64220)' => '64047',
+ 'Arnos (64370)' => '64048',
+ 'Aroue-Ithorots-Olhaïby (64120)' => '64049',
+ 'Arrast-Larrebieu (64130)' => '64050',
+ 'Arraute-Charritte (64120)' => '64051',
+ 'Arrènes (23210)' => '23006',
+ 'Arricau-Bordes (64350)' => '64052',
+ 'Arrien (64420)' => '64053',
+ 'Arros-de-Nay (64800)' => '64054',
+ 'Arrosès (64350)' => '64056',
+ 'Ars (16130)' => '16018',
+ 'Ars (23480)' => '23007',
+ 'Ars-en-Ré (17590)' => '17019',
+ 'Arsac (33460)' => '33012',
+ 'Arsague (40330)' => '40011',
+ 'Artassenx (40090)' => '40012',
+ 'Arthenac (17520)' => '17020',
+ 'Arthez-d\'Armagnac (40190)' => '40013',
+ 'Arthez-d\'Asson (64800)' => '64058',
+ 'Arthez-de-Béarn (64370)' => '64057',
+ 'Artigueloutan (64420)' => '64059',
+ 'Artiguelouve (64230)' => '64060',
+ 'Artigues-près-Bordeaux (33370)' => '33013',
+ 'Artix (64170)' => '64061',
+ 'Arudy (64260)' => '64062',
+ 'Arue (40120)' => '40014',
+ 'Arvert (17530)' => '17021',
+ 'Arveyres (33500)' => '33015',
+ 'Arx (40310)' => '40015',
+ 'Arzacq-Arraziguet (64410)' => '64063',
+ 'Asasp-Arros (64660)' => '64064',
+ 'Ascain (64310)' => '64065',
+ 'Ascarat (64220)' => '64066',
+ 'Aslonnes (86340)' => '86010',
+ 'Asnières-en-Poitou (79170)' => '79015',
+ 'Asnières-la-Giraud (17400)' => '17022',
+ 'Asnières-sur-Blour (86430)' => '86011',
+ 'Asnières-sur-Nouère (16290)' => '16019',
+ 'Asnois (86250)' => '86012',
+ 'Asques (33240)' => '33016',
+ 'Assais-les-Jumeaux (79600)' => '79016',
+ 'Assat (64510)' => '64067',
+ 'Asson (64800)' => '64068',
+ 'Astaffort (47220)' => '47015',
+ 'Astaillac (19120)' => '19012',
+ 'Aste-Béon (64260)' => '64069',
+ 'Astis (64450)' => '64070',
+ 'Athos-Aspis (64390)' => '64071',
+ 'Aubagnan (40700)' => '40016',
+ 'Aubas (24290)' => '24014',
+ 'Aubazines (19190)' => '19013',
+ 'Aubertin (64290)' => '64072',
+ 'Aubeterre-sur-Dronne (16390)' => '16020',
+ 'Aubiac (33430)' => '33017',
+ 'Aubiac (47310)' => '47016',
+ 'Aubigné (79110)' => '79018',
+ 'Aubigny (79390)' => '79019',
+ 'Aubin (64230)' => '64073',
+ 'Aubous (64330)' => '64074',
+ 'Aubusson (23200)' => '23008',
+ 'Audaux (64190)' => '64075',
+ 'Audenge (33980)' => '33019',
+ 'Audignon (40500)' => '40017',
+ 'Audon (40400)' => '40018',
+ 'Audrix (24260)' => '24015',
+ 'Auga (64450)' => '64077',
+ 'Auge (23170)' => '23009',
+ 'Augé (79400)' => '79020',
+ 'Auge-Saint-Médard (16170)' => '16339',
+ 'Augères (23210)' => '23010',
+ 'Augignac (24300)' => '24016',
+ 'Augne (87120)' => '87004',
+ 'Aujac (17770)' => '17023',
+ 'Aulnay (17470)' => '17024',
+ 'Aulnay (86330)' => '86013',
+ 'Aulon (23210)' => '23011',
+ 'Aumagne (17770)' => '17025',
+ 'Aunac (16460)' => '16023',
+ 'Auradou (47140)' => '47017',
+ 'Aureil (87220)' => '87005',
+ 'Aureilhan (40200)' => '40019',
+ 'Auriac (19220)' => '19014',
+ 'Auriac (64450)' => '64078',
+ 'Auriac-du-Périgord (24290)' => '24018',
+ 'Auriac-sur-Dropt (47120)' => '47018',
+ 'Auriat (23400)' => '23012',
+ 'Aurice (40500)' => '40020',
+ 'Auriolles (33790)' => '33020',
+ 'Aurions-Idernes (64350)' => '64079',
+ 'Auros (33124)' => '33021',
+ 'Aussac-Vadalle (16560)' => '16024',
+ 'Aussevielle (64230)' => '64080',
+ 'Aussurucq (64130)' => '64081',
+ 'Auterrive (64270)' => '64082',
+ 'Autevielle-Saint-Martin-Bideren (64390)' => '64083',
+ 'Authon-Ébéon (17770)' => '17026',
+ 'Auzances (23700)' => '23013',
+ 'Availles-en-Châtellerault (86530)' => '86014',
+ 'Availles-Limouzine (86460)' => '86015',
+ 'Availles-Thouarsais (79600)' => '79022',
+ 'Avanton (86170)' => '86016',
+ 'Avensan (33480)' => '33022',
+ 'Avon (79800)' => '79023',
+ 'Avy (17800)' => '17027',
+ 'Aydie (64330)' => '64084',
+ 'Aydius (64490)' => '64085',
+ 'Ayen (19310)' => '19015',
+ 'Ayguemorte-les-Graves (33640)' => '33023',
+ 'Ayherre (64240)' => '64086',
+ 'Ayron (86190)' => '86017',
+ 'Aytré (17440)' => '17028',
+ 'Azat-Châtenet (23210)' => '23014',
+ 'Azat-le-Ris (87360)' => '87006',
+ 'Azay-le-Brûlé (79400)' => '79024',
+ 'Azay-sur-Thouet (79130)' => '79025',
+ 'Azerables (23160)' => '23015',
+ 'Azerat (24210)' => '24019',
+ 'Azur (40140)' => '40021',
+ 'Badefols-d\'Ans (24390)' => '24021',
+ 'Badefols-sur-Dordogne (24150)' => '24022',
+ 'Bagas (33190)' => '33024',
+ 'Bagnizeau (17160)' => '17029',
+ 'Bahus-Soubiran (40320)' => '40022',
+ 'Baigneaux (33760)' => '33025',
+ 'Baignes-Sainte-Radegonde (16360)' => '16025',
+ 'Baigts (40380)' => '40023',
+ 'Baigts-de-Béarn (64300)' => '64087',
+ 'Bajamont (47480)' => '47019',
+ 'Balansun (64300)' => '64088',
+ 'Balanzac (17600)' => '17030',
+ 'Baleix (64460)' => '64089',
+ 'Baleyssagues (47120)' => '47020',
+ 'Baliracq-Maumusson (64330)' => '64090',
+ 'Baliros (64510)' => '64091',
+ 'Balizac (33730)' => '33026',
+ 'Ballans (17160)' => '17031',
+ 'Balledent (87290)' => '87007',
+ 'Ballon (17290)' => '17032',
+ 'Balzac (16430)' => '16026',
+ 'Banca (64430)' => '64092',
+ 'Baneuil (24150)' => '24023',
+ 'Banize (23120)' => '23016',
+ 'Banos (40500)' => '40024',
+ 'Bar (19800)' => '19016',
+ 'Barbaste (47230)' => '47021',
+ 'Barbezières (16140)' => '16027',
+ 'Barbezieux-Saint-Hilaire (16300)' => '16028',
+ 'Barcus (64130)' => '64093',
+ 'Bardenac (16210)' => '16029',
+ 'Bardos (64520)' => '64094',
+ 'Bardou (24560)' => '24024',
+ 'Barie (33190)' => '33027',
+ 'Barinque (64160)' => '64095',
+ 'Baron (33750)' => '33028',
+ 'Barraute-Camu (64390)' => '64096',
+ 'Barret (16300)' => '16030',
+ 'Barro (16700)' => '16031',
+ 'Bars (24210)' => '24025',
+ 'Barsac (33720)' => '33030',
+ 'Barzan (17120)' => '17034',
+ 'Barzun (64530)' => '64097',
+ 'Bas-Mauco (40500)' => '40026',
+ 'Bascons (40090)' => '40025',
+ 'Bassac (16120)' => '16032',
+ 'Bassanne (33190)' => '33031',
+ 'Bassens (33530)' => '33032',
+ 'Bassercles (40700)' => '40027',
+ 'Basses (86200)' => '86018',
+ 'Bassignac-le-Bas (19430)' => '19017',
+ 'Bassignac-le-Haut (19220)' => '19018',
+ 'Bassillac (24330)' => '24026',
+ 'Bassillon-Vauzé (64350)' => '64098',
+ 'Bassussarry (64200)' => '64100',
+ 'Bastanès (64190)' => '64099',
+ 'Bastennes (40360)' => '40028',
+ 'Basville (23260)' => '23017',
+ 'Bats (40320)' => '40029',
+ 'Baudignan (40310)' => '40030',
+ 'Baudreix (64800)' => '64101',
+ 'Baurech (33880)' => '33033',
+ 'Bayac (24150)' => '24027',
+ 'Bayas (33230)' => '33034',
+ 'Bayers (16460)' => '16033',
+ 'Bayon-sur-Gironde (33710)' => '33035',
+ 'Bayonne (64100)' => '64102',
+ 'Bazac (16210)' => '16034',
+ 'Bazas (33430)' => '33036',
+ 'Bazauges (17490)' => '17035',
+ 'Bazelat (23160)' => '23018',
+ 'Bazens (47130)' => '47022',
+ 'Beaugas (47290)' => '47023',
+ 'Beaugeay (17620)' => '17036',
+ 'Beaulieu-sous-Parthenay (79420)' => '79029',
+ 'Beaulieu-sur-Dordogne (19120)' => '19019',
+ 'Beaulieu-sur-Sonnette (16450)' => '16035',
+ 'Beaumont (19390)' => '19020',
+ 'Beaumont (86490)' => '86019',
+ 'Beaumont-du-Lac (87120)' => '87009',
+ 'Beaumontois en Périgord (24440)' => '24028',
+ 'Beaupouyet (24400)' => '24029',
+ 'Beaupuy (47200)' => '47024',
+ 'Beauregard-de-Terrasson (24120)' => '24030',
+ 'Beauregard-et-Bassac (24140)' => '24031',
+ 'Beauronne (24400)' => '24032',
+ 'Beaussac (24340)' => '24033',
+ 'Beaussais-Vitré (79370)' => '79030',
+ 'Beautiran (33640)' => '33037',
+ 'Beauvais-sur-Matha (17490)' => '17037',
+ 'Beauville (47470)' => '47025',
+ 'Beauvoir-sur-Niort (79360)' => '79031',
+ 'Beauziac (47700)' => '47026',
+ 'Béceleuf (79160)' => '79032',
+ 'Bécheresse (16250)' => '16036',
+ 'Bédeille (64460)' => '64103',
+ 'Bedenac (17210)' => '17038',
+ 'Bedous (64490)' => '64104',
+ 'Bégaar (40400)' => '40031',
+ 'Bégadan (33340)' => '33038',
+ 'Bègles (33130)' => '33039',
+ 'Béguey (33410)' => '33040',
+ 'Béguios (64120)' => '64105',
+ 'Béhasque-Lapiste (64120)' => '64106',
+ 'Béhorléguy (64220)' => '64107',
+ 'Beissat (23260)' => '23019',
+ 'Beleymas (24140)' => '24034',
+ 'Belhade (40410)' => '40032',
+ 'Belin-Béliet (33830)' => '33042',
+ 'Bélis (40120)' => '40033',
+ 'Bellac (87300)' => '87011',
+ 'Bellebat (33760)' => '33043',
+ 'Bellechassagne (19290)' => '19021',
+ 'Bellefond (33760)' => '33044',
+ 'Bellefonds (86210)' => '86020',
+ 'Bellegarde-en-Marche (23190)' => '23020',
+ 'Belleville (79360)' => '79033',
+ 'Bellocq (64270)' => '64108',
+ 'Bellon (16210)' => '16037',
+ 'Belluire (17800)' => '17039',
+ 'Bélus (40300)' => '40034',
+ 'Belvès-de-Castillon (33350)' => '33045',
+ 'Benassay (86470)' => '86021',
+ 'Benayes (19510)' => '19022',
+ 'Bénéjacq (64800)' => '64109',
+ 'Bénesse-lès-Dax (40180)' => '40035',
+ 'Bénesse-Maremne (40230)' => '40036',
+ 'Benest (16350)' => '16038',
+ 'Bénévent-l\'Abbaye (23210)' => '23021',
+ 'Benon (17170)' => '17041',
+ 'Benquet (40280)' => '40037',
+ 'Bentayou-Sérée (64460)' => '64111',
+ 'Béost (64440)' => '64110',
+ 'Berbiguières (24220)' => '24036',
+ 'Bercloux (17770)' => '17042',
+ 'Bérenx (64300)' => '64112',
+ 'Bergerac (24100)' => '24037',
+ 'Bergouey (40250)' => '40038',
+ 'Bergouey-Viellenave (64270)' => '64113',
+ 'Bernac (16700)' => '16039',
+ 'Bernadets (64160)' => '64114',
+ 'Bernay-Saint-Martin (17330)' => '17043',
+ 'Berneuil (16480)' => '16040',
+ 'Berneuil (17460)' => '17044',
+ 'Berneuil (87300)' => '87012',
+ 'Bernos-Beaulac (33430)' => '33046',
+ 'Berrie (86120)' => '86022',
+ 'Berrogain-Laruns (64130)' => '64115',
+ 'Bersac-sur-Rivalier (87370)' => '87013',
+ 'Berson (33390)' => '33047',
+ 'Berthegon (86420)' => '86023',
+ 'Berthez (33124)' => '33048',
+ 'Bertric-Burée (24320)' => '24038',
+ 'Béruges (86190)' => '86024',
+ 'Bescat (64260)' => '64116',
+ 'Bésingrand (64150)' => '64117',
+ 'Bessac (16250)' => '16041',
+ 'Bessé (16140)' => '16042',
+ 'Besse (24550)' => '24039',
+ 'Bessines (79000)' => '79034',
+ 'Bessines-sur-Gartempe (87250)' => '87014',
+ 'Betbezer-d\'Armagnac (40240)' => '40039',
+ 'Bétête (23270)' => '23022',
+ 'Béthines (86310)' => '86025',
+ 'Bétracq (64350)' => '64118',
+ 'Beurlay (17250)' => '17045',
+ 'Beuste (64800)' => '64119',
+ 'Beuxes (86120)' => '86026',
+ 'Beychac-et-Caillau (33750)' => '33049',
+ 'Beylongue (40370)' => '40040',
+ 'Beynac (87700)' => '87015',
+ 'Beynac-et-Cazenac (24220)' => '24040',
+ 'Beynat (19190)' => '19023',
+ 'Beyrie-en-Béarn (64230)' => '64121',
+ 'Beyrie-sur-Joyeuse (64120)' => '64120',
+ 'Beyries (40700)' => '40041',
+ 'Beyssac (19230)' => '19024',
+ 'Beyssenac (19230)' => '19025',
+ 'Bézenac (24220)' => '24041',
+ 'Biard (86580)' => '86027',
+ 'Biarritz (64200)' => '64122',
+ 'Biarrotte (40390)' => '40042',
+ 'Bias (40170)' => '40043',
+ 'Bias (47300)' => '47027',
+ 'Biaudos (40390)' => '40044',
+ 'Bidache (64520)' => '64123',
+ 'Bidarray (64780)' => '64124',
+ 'Bidart (64210)' => '64125',
+ 'Bidos (64400)' => '64126',
+ 'Bielle (64260)' => '64127',
+ 'Bieujac (33210)' => '33050',
+ 'Biganos (33380)' => '33051',
+ 'Bignay (17400)' => '17046',
+ 'Bignoux (86800)' => '86028',
+ 'Bilhac (19120)' => '19026',
+ 'Bilhères (64260)' => '64128',
+ 'Billère (64140)' => '64129',
+ 'Bioussac (16700)' => '16044',
+ 'Birac (16120)' => '16045',
+ 'Birac (33430)' => '33053',
+ 'Birac-sur-Trec (47200)' => '47028',
+ 'Biras (24310)' => '24042',
+ 'Biriatou (64700)' => '64130',
+ 'Biron (17800)' => '17047',
+ 'Biron (24540)' => '24043',
+ 'Biron (64300)' => '64131',
+ 'Biscarrosse (40600)' => '40046',
+ 'Bizanos (64320)' => '64132',
+ 'Blaignac (33190)' => '33054',
+ 'Blaignan (33340)' => '33055',
+ 'Blanquefort (33290)' => '33056',
+ 'Blanquefort-sur-Briolance (47500)' => '47029',
+ 'Blanzac (87300)' => '87017',
+ 'Blanzac-lès-Matha (17160)' => '17048',
+ 'Blanzac-Porcheresse (16250)' => '16046',
+ 'Blanzaguet-Saint-Cybard (16320)' => '16047',
+ 'Blanzay (86400)' => '86029',
+ 'Blanzay-sur-Boutonne (17470)' => '17049',
+ 'Blasimon (33540)' => '33057',
+ 'Blaslay (86170)' => '86030',
+ 'Blaudeix (23140)' => '23023',
+ 'Blaye (33390)' => '33058',
+ 'Blaymont (47470)' => '47030',
+ 'Blésignac (33670)' => '33059',
+ 'Blessac (23200)' => '23024',
+ 'Blis-et-Born (24330)' => '24044',
+ 'Blond (87300)' => '87018',
+ 'Boé (47550)' => '47031',
+ 'Boeil-Bezing (64510)' => '64133',
+ 'Bois (17240)' => '17050',
+ 'Boisbreteau (16480)' => '16048',
+ 'Boismé (79300)' => '79038',
+ 'Boisné-La Tude (16320)' => '16082',
+ 'Boisredon (17150)' => '17052',
+ 'Boisse (24560)' => '24045',
+ 'Boisserolles (79360)' => '79039',
+ 'Boisseuil (87220)' => '87019',
+ 'Boisseuilh (24390)' => '24046',
+ 'Bommes (33210)' => '33060',
+ 'Bon-Encontre (47240)' => '47032',
+ 'Bonloc (64240)' => '64134',
+ 'Bonnac-la-Côte (87270)' => '87020',
+ 'Bonnat (23220)' => '23025',
+ 'Bonnefond (19170)' => '19027',
+ 'Bonnegarde (40330)' => '40047',
+ 'Bonnes (16390)' => '16049',
+ 'Bonnes (86300)' => '86031',
+ 'Bonnetan (33370)' => '33061',
+ 'Bonneuil (16120)' => '16050',
+ 'Bonneuil-Matours (86210)' => '86032',
+ 'Bonneville (16170)' => '16051',
+ 'Bonneville-et-Saint-Avit-de-Fumadières (24230)' => '24048',
+ 'Bonnut (64300)' => '64135',
+ 'Bonzac (33910)' => '33062',
+ 'Boos (40370)' => '40048',
+ 'Borce (64490)' => '64136',
+ 'Bord-Saint-Georges (23230)' => '23026',
+ 'Bordeaux (33000)' => '33063',
+ 'Bordères (64800)' => '64137',
+ 'Bordères-et-Lamensans (40270)' => '40049',
+ 'Bordes (64510)' => '64138',
+ 'Bords (17430)' => '17053',
+ 'Boresse-et-Martron (17270)' => '17054',
+ 'Borrèze (24590)' => '24050',
+ 'Bors (Canton de Baignes-Sainte-Radegonde) (16360)' => '16053',
+ 'Bors (Canton de Montmoreau-Saint-Cybard) (16190)' => '16052',
+ 'Bort-les-Orgues (19110)' => '19028',
+ 'Boscamnant (17360)' => '17055',
+ 'Bosdarros (64290)' => '64139',
+ 'Bosmie-l\'Aiguille (87110)' => '87021',
+ 'Bosmoreau-les-Mines (23400)' => '23027',
+ 'Bosroger (23200)' => '23028',
+ 'Bosset (24130)' => '24051',
+ 'Bossugan (33350)' => '33064',
+ 'Bostens (40090)' => '40050',
+ 'Boucau (64340)' => '64140',
+ 'Boudy-de-Beauregard (47290)' => '47033',
+ 'Boueilh-Boueilho-Lasque (64330)' => '64141',
+ 'Bouëx (16410)' => '16055',
+ 'Bougarber (64230)' => '64142',
+ 'Bouglon (47250)' => '47034',
+ 'Bougneau (17800)' => '17056',
+ 'Bougon (79800)' => '79042',
+ 'Bougue (40090)' => '40051',
+ 'Bouhet (17540)' => '17057',
+ 'Bouillac (24480)' => '24052',
+ 'Bouillé-Loretz (79290)' => '79043',
+ 'Bouillé-Saint-Paul (79290)' => '79044',
+ 'Bouillon (64410)' => '64143',
+ 'Bouin (79110)' => '79045',
+ 'Boulazac Isle Manoire (24750)' => '24053',
+ 'Bouliac (33270)' => '33065',
+ 'Boumourt (64370)' => '64144',
+ 'Bouniagues (24560)' => '24054',
+ 'Bourcefranc-le-Chapus (17560)' => '17058',
+ 'Bourdalat (40190)' => '40052',
+ 'Bourdeilles (24310)' => '24055',
+ 'Bourdelles (33190)' => '33066',
+ 'Bourdettes (64800)' => '64145',
+ 'Bouresse (86410)' => '86034',
+ 'Bourg (33710)' => '33067',
+ 'Bourg-Archambault (86390)' => '86035',
+ 'Bourg-Charente (16200)' => '16056',
+ 'Bourg-des-Maisons (24320)' => '24057',
+ 'Bourg-du-Bost (24600)' => '24058',
+ 'Bourganeuf (23400)' => '23030',
+ 'Bourgnac (24400)' => '24059',
+ 'Bourgneuf (17220)' => '17059',
+ 'Bourgougnague (47410)' => '47035',
+ 'Bourideys (33113)' => '33068',
+ 'Bourlens (47370)' => '47036',
+ 'Bournand (86120)' => '86036',
+ 'Bournel (47210)' => '47037',
+ 'Bourniquel (24150)' => '24060',
+ 'Bournos (64450)' => '64146',
+ 'Bourran (47320)' => '47038',
+ 'Bourriot-Bergonce (40120)' => '40053',
+ 'Bourrou (24110)' => '24061',
+ 'Boussac (23600)' => '23031',
+ 'Boussac-Bourg (23600)' => '23032',
+ 'Boussais (79600)' => '79047',
+ 'Boussès (47420)' => '47039',
+ 'Bouteilles-Saint-Sébastien (24320)' => '24062',
+ 'Boutenac-Touvent (17120)' => '17060',
+ 'Bouteville (16120)' => '16057',
+ 'Boutiers-Saint-Trojan (16100)' => '16058',
+ 'Bouzic (24250)' => '24063',
+ 'Brach (33480)' => '33070',
+ 'Bran (17210)' => '17061',
+ 'Branceilles (19500)' => '19029',
+ 'Branne (33420)' => '33071',
+ 'Brannens (33124)' => '33072',
+ 'Brantôme en Périgord (24310)' => '24064',
+ 'Brassempouy (40330)' => '40054',
+ 'Braud-et-Saint-Louis (33820)' => '33073',
+ 'Brax (47310)' => '47040',
+ 'Bresdon (17490)' => '17062',
+ 'Bressuire (79300)' => '79049',
+ 'Bretagne-de-Marsan (40280)' => '40055',
+ 'Bretignolles (79140)' => '79050',
+ 'Brettes (16240)' => '16059',
+ 'Breuil-la-Réorte (17700)' => '17063',
+ 'Breuil-Magné (17870)' => '17065',
+ 'Breuilaufa (87300)' => '87022',
+ 'Breuilh (24380)' => '24065',
+ 'Breuillet (17920)' => '17064',
+ 'Bréville (16370)' => '16060',
+ 'Brie (16590)' => '16061',
+ 'Brie (79100)' => '79054',
+ 'Brie-sous-Archiac (17520)' => '17066',
+ 'Brie-sous-Barbezieux (16300)' => '16062',
+ 'Brie-sous-Chalais (16210)' => '16063',
+ 'Brie-sous-Matha (17160)' => '17067',
+ 'Brie-sous-Mortagne (17120)' => '17068',
+ 'Brieuil-sur-Chizé (79170)' => '79055',
+ 'Brignac-la-Plaine (19310)' => '19030',
+ 'Brigueil-le-Chantre (86290)' => '86037',
+ 'Brigueuil (16420)' => '16064',
+ 'Brillac (16500)' => '16065',
+ 'Brion (86160)' => '86038',
+ 'Brion-près-Thouet (79290)' => '79056',
+ 'Brioux-sur-Boutonne (79170)' => '79057',
+ 'Briscous (64240)' => '64147',
+ 'Brive-la-Gaillarde (19100)' => '19031',
+ 'Brives-sur-Charente (17800)' => '17069',
+ 'Brivezac (19120)' => '19032',
+ 'Brizambourg (17770)' => '17070',
+ 'Brocas (40420)' => '40056',
+ 'Brossac (16480)' => '16066',
+ 'Brouchaud (24210)' => '24066',
+ 'Brouqueyran (33124)' => '33074',
+ 'Brousse (23700)' => '23034',
+ 'Bruch (47130)' => '47041',
+ 'Bruges (33520)' => '33075',
+ 'Bruges-Capbis-Mifaget (64800)' => '64148',
+ 'Brugnac (47260)' => '47042',
+ 'Brûlain (79230)' => '79058',
+ 'Brux (86510)' => '86039',
+ 'Buanes (40320)' => '40057',
+ 'Budelière (23170)' => '23035',
+ 'Budos (33720)' => '33076',
+ 'Bugeat (19170)' => '19033',
+ 'Bugnein (64190)' => '64149',
+ 'Bujaleuf (87460)' => '87024',
+ 'Bunus (64120)' => '64150',
+ 'Bunzac (16110)' => '16067',
+ 'Burgaronne (64390)' => '64151',
+ 'Burgnac (87800)' => '87025',
+ 'Burie (17770)' => '17072',
+ 'Buros (64160)' => '64152',
+ 'Burosse-Mendousse (64330)' => '64153',
+ 'Bussac (24350)' => '24069',
+ 'Bussac-Forêt (17210)' => '17074',
+ 'Bussac-sur-Charente (17100)' => '17073',
+ 'Busserolles (24360)' => '24070',
+ 'Bussière-Badil (24360)' => '24071',
+ 'Bussière-Dunoise (23320)' => '23036',
+ 'Bussière-Galant (87230)' => '87027',
+ 'Bussière-Nouvelle (23700)' => '23037',
+ 'Bussière-Poitevine (87320)' => '87028',
+ 'Bussière-Saint-Georges (23600)' => '23038',
+ 'Bussunarits-Sarrasquette (64220)' => '64154',
+ 'Bustince-Iriberry (64220)' => '64155',
+ 'Buxerolles (86180)' => '86041',
+ 'Buxeuil (37160)' => '86042',
+ 'Buzet-sur-Baïse (47160)' => '47043',
+ 'Buziet (64680)' => '64156',
+ 'Buzy (64260)' => '64157',
+ 'Cabanac-et-Villagrains (33650)' => '33077',
+ 'Cabara (33420)' => '33078',
+ 'Cabariot (17430)' => '17075',
+ 'Cabidos (64410)' => '64158',
+ 'Cachen (40120)' => '40058',
+ 'Cadarsac (33750)' => '33079',
+ 'Cadaujac (33140)' => '33080',
+ 'Cadillac (33410)' => '33081',
+ 'Cadillac-en-Fronsadais (33240)' => '33082',
+ 'Cadillon (64330)' => '64159',
+ 'Cagnotte (40300)' => '40059',
+ 'Cahuzac (47330)' => '47044',
+ 'Calès (24150)' => '24073',
+ 'Calignac (47600)' => '47045',
+ 'Callen (40430)' => '40060',
+ 'Calonges (47430)' => '47046',
+ 'Calviac-en-Périgord (24370)' => '24074',
+ 'Camarsac (33750)' => '33083',
+ 'Cambes (33880)' => '33084',
+ 'Cambes (47350)' => '47047',
+ 'Camblanes-et-Meynac (33360)' => '33085',
+ 'Cambo-les-Bains (64250)' => '64160',
+ 'Came (64520)' => '64161',
+ 'Camiac-et-Saint-Denis (33420)' => '33086',
+ 'Camiran (33190)' => '33087',
+ 'Camou-Cihigue (64470)' => '64162',
+ 'Campagnac-lès-Quercy (24550)' => '24075',
+ 'Campagne (24260)' => '24076',
+ 'Campagne (40090)' => '40061',
+ 'Campet-et-Lamolère (40090)' => '40062',
+ 'Camps-Saint-Mathurin-Léobazel (19430)' => '19034',
+ 'Camps-sur-l\'Isle (33660)' => '33088',
+ 'Campsegret (24140)' => '24077',
+ 'Campugnan (33390)' => '33089',
+ 'Cancon (47290)' => '47048',
+ 'Candresse (40180)' => '40063',
+ 'Canéjan (33610)' => '33090',
+ 'Canenx-et-Réaut (40090)' => '40064',
+ 'Cantenac (33460)' => '33091',
+ 'Cantillac (24530)' => '24079',
+ 'Cantois (33760)' => '33092',
+ 'Capbreton (40130)' => '40065',
+ 'Capdrot (24540)' => '24080',
+ 'Capian (33550)' => '33093',
+ 'Caplong (33220)' => '33094',
+ 'Captieux (33840)' => '33095',
+ 'Carbon-Blanc (33560)' => '33096',
+ 'Carcans (33121)' => '33097',
+ 'Carcarès-Sainte-Croix (40400)' => '40066',
+ 'Carcen-Ponson (40400)' => '40067',
+ 'Cardan (33410)' => '33098',
+ 'Cardesse (64360)' => '64165',
+ 'Carignan-de-Bordeaux (33360)' => '33099',
+ 'Carlux (24370)' => '24081',
+ 'Caro (64220)' => '64166',
+ 'Carrère (64160)' => '64167',
+ 'Carresse-Cassaber (64270)' => '64168',
+ 'Cars (33390)' => '33100',
+ 'Carsac-Aillac (24200)' => '24082',
+ 'Carsac-de-Gurson (24610)' => '24083',
+ 'Cartelègue (33390)' => '33101',
+ 'Carves (24170)' => '24084',
+ 'Cassen (40380)' => '40068',
+ 'Casseneuil (47440)' => '47049',
+ 'Casseuil (33190)' => '33102',
+ 'Cassignas (47340)' => '47050',
+ 'Castagnède (64270)' => '64170',
+ 'Castaignos-Souslens (40700)' => '40069',
+ 'Castandet (40270)' => '40070',
+ 'Casteide-Cami (64170)' => '64171',
+ 'Casteide-Candau (64370)' => '64172',
+ 'Casteide-Doat (64460)' => '64173',
+ 'Castel-Sarrazin (40330)' => '40074',
+ 'Castelculier (47240)' => '47051',
+ 'Casteljaloux (47700)' => '47052',
+ 'Castella (47340)' => '47053',
+ 'Castelmoron-d\'Albret (33540)' => '33103',
+ 'Castelmoron-sur-Lot (47260)' => '47054',
+ 'Castelnau-Chalosse (40360)' => '40071',
+ 'Castelnau-de-Médoc (33480)' => '33104',
+ 'Castelnau-sur-Gupie (47180)' => '47056',
+ 'Castelnau-Tursan (40320)' => '40072',
+ 'Castelnaud-de-Gratecambe (47290)' => '47055',
+ 'Castelnaud-la-Chapelle (24250)' => '24086',
+ 'Castelner (40700)' => '40073',
+ 'Castels (24220)' => '24087',
+ 'Castelviel (33540)' => '33105',
+ 'Castéra-Loubix (64460)' => '64174',
+ 'Castet (64260)' => '64175',
+ 'Castetbon (64190)' => '64176',
+ 'Castétis (64300)' => '64177',
+ 'Castetnau-Camblong (64190)' => '64178',
+ 'Castetner (64300)' => '64179',
+ 'Castetpugon (64330)' => '64180',
+ 'Castets (40260)' => '40075',
+ 'Castets-en-Dorthe (33210)' => '33106',
+ 'Castillon (Canton d\'Arthez-de-Béarn) (64370)' => '64181',
+ 'Castillon (Canton de Lembeye) (64350)' => '64182',
+ 'Castillon-de-Castets (33210)' => '33107',
+ 'Castillon-la-Bataille (33350)' => '33108',
+ 'Castillonnès (47330)' => '47057',
+ 'Castres-Gironde (33640)' => '33109',
+ 'Caubeyres (47160)' => '47058',
+ 'Caubios-Loos (64230)' => '64183',
+ 'Caubon-Saint-Sauveur (47120)' => '47059',
+ 'Caudecoste (47220)' => '47060',
+ 'Caudrot (33490)' => '33111',
+ 'Caumont (33540)' => '33112',
+ 'Caumont-sur-Garonne (47430)' => '47061',
+ 'Cauna (40500)' => '40076',
+ 'Caunay (79190)' => '79060',
+ 'Cauneille (40300)' => '40077',
+ 'Caupenne (40250)' => '40078',
+ 'Cause-de-Clérans (24150)' => '24088',
+ 'Cauvignac (33690)' => '33113',
+ 'Cauzac (47470)' => '47062',
+ 'Cavarc (47330)' => '47063',
+ 'Cavignac (33620)' => '33114',
+ 'Cazalis (33113)' => '33115',
+ 'Cazalis (40700)' => '40079',
+ 'Cazats (33430)' => '33116',
+ 'Cazaugitat (33790)' => '33117',
+ 'Cazères-sur-l\'Adour (40270)' => '40080',
+ 'Cazideroque (47370)' => '47064',
+ 'Cazoulès (24370)' => '24089',
+ 'Ceaux-en-Couhé (86700)' => '86043',
+ 'Ceaux-en-Loudun (86200)' => '86044',
+ 'Celle-Lévescault (86600)' => '86045',
+ 'Cellefrouin (16260)' => '16068',
+ 'Celles (17520)' => '17076',
+ 'Celles (24600)' => '24090',
+ 'Celles-sur-Belle (79370)' => '79061',
+ 'Cellettes (16230)' => '16069',
+ 'Cénac (33360)' => '33118',
+ 'Cénac-et-Saint-Julien (24250)' => '24091',
+ 'Cendrieux (24380)' => '24092',
+ 'Cenon (33150)' => '33119',
+ 'Cenon-sur-Vienne (86530)' => '86046',
+ 'Cercles (24320)' => '24093',
+ 'Cercoux (17270)' => '17077',
+ 'Cère (40090)' => '40081',
+ 'Cerizay (79140)' => '79062',
+ 'Cernay (86140)' => '86047',
+ 'Cérons (33720)' => '33120',
+ 'Cersay (79290)' => '79063',
+ 'Cescau (64170)' => '64184',
+ 'Cessac (33760)' => '33121',
+ 'Cestas (33610)' => '33122',
+ 'Cette-Eygun (64490)' => '64185',
+ 'Ceyroux (23210)' => '23042',
+ 'Cézac (33620)' => '33123',
+ 'Chabanais (16150)' => '16070',
+ 'Chabournay (86380)' => '86048',
+ 'Chabrac (16150)' => '16071',
+ 'Chabrignac (19350)' => '19035',
+ 'Chadenac (17800)' => '17078',
+ 'Chadurie (16250)' => '16072',
+ 'Chail (79500)' => '79064',
+ 'Chaillac-sur-Vienne (87200)' => '87030',
+ 'Chaillevette (17890)' => '17079',
+ 'Chalagnac (24380)' => '24094',
+ 'Chalais (16210)' => '16073',
+ 'Chalais (24800)' => '24095',
+ 'Chalais (86200)' => '86049',
+ 'Chalandray (86190)' => '86050',
+ 'Challignac (16300)' => '16074',
+ 'Châlus (87230)' => '87032',
+ 'Chamadelle (33230)' => '33124',
+ 'Chamberaud (23480)' => '23043',
+ 'Chamberet (19370)' => '19036',
+ 'Chambon (17290)' => '17080',
+ 'Chambon-Sainte-Croix (23220)' => '23044',
+ 'Chambon-sur-Voueize (23170)' => '23045',
+ 'Chambonchard (23110)' => '23046',
+ 'Chamborand (23240)' => '23047',
+ 'Chamboret (87140)' => '87033',
+ 'Chamboulive (19450)' => '19037',
+ 'Chameyrat (19330)' => '19038',
+ 'Chamouillac (17130)' => '17081',
+ 'Champagnac (17500)' => '17082',
+ 'Champagnac-de-Belair (24530)' => '24096',
+ 'Champagnac-la-Noaille (19320)' => '19039',
+ 'Champagnac-la-Prune (19320)' => '19040',
+ 'Champagnac-la-Rivière (87150)' => '87034',
+ 'Champagnat (23190)' => '23048',
+ 'Champagne (17620)' => '17083',
+ 'Champagne-et-Fontaine (24320)' => '24097',
+ 'Champagné-le-Sec (86510)' => '86051',
+ 'Champagne-Mouton (16350)' => '16076',
+ 'Champagné-Saint-Hilaire (86160)' => '86052',
+ 'Champagne-Vigny (16250)' => '16075',
+ 'Champagnolles (17240)' => '17084',
+ 'Champcevinel (24750)' => '24098',
+ 'Champdeniers-Saint-Denis (79220)' => '79066',
+ 'Champdolent (17430)' => '17085',
+ 'Champeaux-et-la-Chapelle-Pommier (24340)' => '24099',
+ 'Champigny-le-Sec (86170)' => '86053',
+ 'Champmillon (16290)' => '16077',
+ 'Champnétery (87400)' => '87035',
+ 'Champniers (16430)' => '16078',
+ 'Champniers (86400)' => '86054',
+ 'Champniers-et-Reilhac (24360)' => '24100',
+ 'Champs-Romain (24470)' => '24101',
+ 'Champsac (87230)' => '87036',
+ 'Champsanglard (23220)' => '23049',
+ 'Chanac-les-Mines (19150)' => '19041',
+ 'Chancelade (24650)' => '24102',
+ 'Chaniers (17610)' => '17086',
+ 'Chantecorps (79340)' => '79068',
+ 'Chanteix (19330)' => '19042',
+ 'Chanteloup (79320)' => '79069',
+ 'Chantemerle-sur-la-Soie (17380)' => '17087',
+ 'Chantérac (24190)' => '24104',
+ 'Chantillac (16360)' => '16079',
+ 'Chapdeuil (24320)' => '24105',
+ 'Chapelle-Spinasse (19300)' => '19046',
+ 'Chapelle-Viviers (86300)' => '86059',
+ 'Chaptelat (87270)' => '87038',
+ 'Chard (23700)' => '23053',
+ 'Charmé (16140)' => '16083',
+ 'Charrais (86170)' => '86060',
+ 'Charras (16380)' => '16084',
+ 'Charre (64190)' => '64186',
+ 'Charritte-de-Bas (64130)' => '64187',
+ 'Charron (17230)' => '17091',
+ 'Charron (23700)' => '23054',
+ 'Charroux (86250)' => '86061',
+ 'Chartrier-Ferrière (19600)' => '19047',
+ 'Chartuzac (17130)' => '17092',
+ 'Chassaignes (24600)' => '24114',
+ 'Chasseneuil-du-Poitou (86360)' => '86062',
+ 'Chasseneuil-sur-Bonnieure (16260)' => '16085',
+ 'Chassenon (16150)' => '16086',
+ 'Chassiecq (16350)' => '16087',
+ 'Chassors (16200)' => '16088',
+ 'Chasteaux (19600)' => '19049',
+ 'Chatain (86250)' => '86063',
+ 'Château-Chervix (87380)' => '87039',
+ 'Château-Garnier (86350)' => '86064',
+ 'Château-l\'Évêque (24460)' => '24115',
+ 'Château-Larcher (86370)' => '86065',
+ 'Châteaubernard (16100)' => '16089',
+ 'Châteauneuf-la-Forêt (87130)' => '87040',
+ 'Châteauneuf-sur-Charente (16120)' => '16090',
+ 'Châteauponsac (87290)' => '87041',
+ 'Châtelaillon-Plage (17340)' => '17094',
+ 'Châtelard (23700)' => '23055',
+ 'Châtellerault (86100)' => '86066',
+ 'Châtelus-le-Marcheix (23430)' => '23056',
+ 'Châtelus-Malvaleix (23270)' => '23057',
+ 'Chatenet (17210)' => '17095',
+ 'Châtignac (16480)' => '16091',
+ 'Châtillon (86700)' => '86067',
+ 'Châtillon-sur-Thouet (79200)' => '79080',
+ 'Châtres (24120)' => '24116',
+ 'Chauffour-sur-Vell (19500)' => '19050',
+ 'Chaumeil (19390)' => '19051',
+ 'Chaunac (17130)' => '17096',
+ 'Chaunay (86510)' => '86068',
+ 'Chauray (79180)' => '79081',
+ 'Chauvigny (86300)' => '86070',
+ 'Chavagnac (24120)' => '24117',
+ 'Chavanac (19290)' => '19052',
+ 'Chavanat (23250)' => '23060',
+ 'Chaveroche (19200)' => '19053',
+ 'Chazelles (16380)' => '16093',
+ 'Chef-Boutonne (79110)' => '79083',
+ 'Cheissoux (87460)' => '87043',
+ 'Chenac-Saint-Seurin-d\'Uzet (17120)' => '17098',
+ 'Chenailler-Mascheix (19120)' => '19054',
+ 'Chenay (79120)' => '79084',
+ 'Cheneché (86380)' => '86071',
+ 'Chénérailles (23130)' => '23061',
+ 'Chenevelles (86450)' => '86072',
+ 'Chéniers (23220)' => '23062',
+ 'Chenommet (16460)' => '16094',
+ 'Chenon (16460)' => '16095',
+ 'Chepniers (17210)' => '17099',
+ 'Chérac (17610)' => '17100',
+ 'Chéraute (64130)' => '64188',
+ 'Cherbonnières (17470)' => '17101',
+ 'Chérigné (79170)' => '79085',
+ 'Chermignac (17460)' => '17102',
+ 'Chéronnac (87600)' => '87044',
+ 'Cherval (24320)' => '24119',
+ 'Cherveix-Cubas (24390)' => '24120',
+ 'Cherves (86170)' => '86073',
+ 'Cherves-Châtelars (16310)' => '16096',
+ 'Cherves-Richemont (16370)' => '16097',
+ 'Chervettes (17380)' => '17103',
+ 'Cherveux (79410)' => '79086',
+ 'Chevanceaux (17210)' => '17104',
+ 'Chey (79120)' => '79087',
+ 'Chiché (79350)' => '79088',
+ 'Chillac (16480)' => '16099',
+ 'Chirac (16150)' => '16100',
+ 'Chirac-Bellevue (19160)' => '19055',
+ 'Chiré-en-Montreuil (86190)' => '86074',
+ 'Chives (17510)' => '17105',
+ 'Chizé (79170)' => '79090',
+ 'Chouppes (86110)' => '86075',
+ 'Chourgnac (24640)' => '24121',
+ 'Ciboure (64500)' => '64189',
+ 'Cierzac (17520)' => '17106',
+ 'Cieux (87520)' => '87045',
+ 'Ciré-d\'Aunis (17290)' => '17107',
+ 'Cirières (79140)' => '79091',
+ 'Cissac-Médoc (33250)' => '33125',
+ 'Cissé (86170)' => '86076',
+ 'Civaux (86320)' => '86077',
+ 'Civrac-de-Blaye (33920)' => '33126',
+ 'Civrac-en-Médoc (33340)' => '33128',
+ 'Civrac-sur-Dordogne (33350)' => '33127',
+ 'Civray (86400)' => '86078',
+ 'Cladech (24170)' => '24122',
+ 'Clairac (47320)' => '47065',
+ 'Clairavaux (23500)' => '23063',
+ 'Claix (16440)' => '16101',
+ 'Clam (17500)' => '17108',
+ 'Claracq (64330)' => '64190',
+ 'Classun (40320)' => '40082',
+ 'Clavé (79420)' => '79092',
+ 'Clavette (17220)' => '17109',
+ 'Clèdes (40320)' => '40083',
+ 'Clérac (17270)' => '17110',
+ 'Clergoux (19320)' => '19056',
+ 'Clermont (40180)' => '40084',
+ 'Clermont-d\'Excideuil (24160)' => '24124',
+ 'Clermont-de-Beauregard (24140)' => '24123',
+ 'Clermont-Dessous (47130)' => '47066',
+ 'Clermont-Soubiran (47270)' => '47067',
+ 'Clessé (79350)' => '79094',
+ 'Cleyrac (33540)' => '33129',
+ 'Clion (17240)' => '17111',
+ 'Cloué (86600)' => '86080',
+ 'Clugnat (23270)' => '23064',
+ 'Clussais-la-Pommeraie (79190)' => '79095',
+ 'Coarraze (64800)' => '64191',
+ 'Cocumont (47250)' => '47068',
+ 'Cognac (16100)' => '16102',
+ 'Cognac-la-Forêt (87310)' => '87046',
+ 'Coimères (33210)' => '33130',
+ 'Coirac (33540)' => '33131',
+ 'Coivert (17330)' => '17114',
+ 'Colayrac-Saint-Cirq (47450)' => '47069',
+ 'Collonges-la-Rouge (19500)' => '19057',
+ 'Colombier (24560)' => '24126',
+ 'Colombiers (17460)' => '17115',
+ 'Colombiers (86490)' => '86081',
+ 'Colondannes (23800)' => '23065',
+ 'Coly (24120)' => '24127',
+ 'Comberanche-et-Épeluche (24600)' => '24128',
+ 'Combiers (16320)' => '16103',
+ 'Combrand (79140)' => '79096',
+ 'Combressol (19250)' => '19058',
+ 'Commensacq (40210)' => '40085',
+ 'Compreignac (87140)' => '87047',
+ 'Comps (33710)' => '33132',
+ 'Concèze (19350)' => '19059',
+ 'Conchez-de-Béarn (64330)' => '64192',
+ 'Condac (16700)' => '16104',
+ 'Condat-sur-Ganaveix (19140)' => '19060',
+ 'Condat-sur-Trincou (24530)' => '24129',
+ 'Condat-sur-Vézère (24570)' => '24130',
+ 'Condat-sur-Vienne (87920)' => '87048',
+ 'Condéon (16360)' => '16105',
+ 'Condezaygues (47500)' => '47070',
+ 'Confolens (16500)' => '16106',
+ 'Confolent-Port-Dieu (19200)' => '19167',
+ 'Conne-de-Labarde (24560)' => '24132',
+ 'Connezac (24300)' => '24131',
+ 'Consac (17150)' => '17116',
+ 'Contré (17470)' => '17117',
+ 'Corbère-Abères (64350)' => '64193',
+ 'Corgnac-sur-l\'Isle (24800)' => '24134',
+ 'Corignac (17130)' => '17118',
+ 'Corme-Écluse (17600)' => '17119',
+ 'Corme-Royal (17600)' => '17120',
+ 'Cornil (19150)' => '19061',
+ 'Cornille (24750)' => '24135',
+ 'Corrèze (19800)' => '19062',
+ 'Coslédaà-Lube-Boast (64160)' => '64194',
+ 'Cosnac (19360)' => '19063',
+ 'Coubeyrac (33890)' => '33133',
+ 'Coubjours (24390)' => '24136',
+ 'Coublucq (64410)' => '64195',
+ 'Coudures (40500)' => '40086',
+ 'Couffy-sur-Sarsonne (19340)' => '19064',
+ 'Couhé (86700)' => '86082',
+ 'Coulaures (24420)' => '24137',
+ 'Coulgens (16560)' => '16107',
+ 'Coulombiers (86600)' => '86083',
+ 'Coulon (79510)' => '79100',
+ 'Coulonges (16330)' => '16108',
+ 'Coulonges (17800)' => '17122',
+ 'Coulonges (86290)' => '86084',
+ 'Coulonges-sur-l\'Autize (79160)' => '79101',
+ 'Coulonges-Thouarsais (79330)' => '79102',
+ 'Coulounieix-Chamiers (24660)' => '24138',
+ 'Coulx (47260)' => '47071',
+ 'Couquèques (33340)' => '33134',
+ 'Courant (17330)' => '17124',
+ 'Courbiac (47370)' => '47072',
+ 'Courbillac (16200)' => '16109',
+ 'Courcelles (17400)' => '17125',
+ 'Courcerac (17160)' => '17126',
+ 'Courcôme (16240)' => '16110',
+ 'Courçon (17170)' => '17127',
+ 'Courcoury (17100)' => '17128',
+ 'Courgeac (16190)' => '16111',
+ 'Courlac (16210)' => '16112',
+ 'Courlay (79440)' => '79103',
+ 'Courpiac (33760)' => '33135',
+ 'Courpignac (17130)' => '17129',
+ 'Cours (47360)' => '47073',
+ 'Cours (79220)' => '79104',
+ 'Cours-de-Monségur (33580)' => '33136',
+ 'Cours-de-Pile (24520)' => '24140',
+ 'Cours-les-Bains (33690)' => '33137',
+ 'Coursac (24430)' => '24139',
+ 'Courteix (19340)' => '19065',
+ 'Coussac-Bonneval (87500)' => '87049',
+ 'Coussay (86110)' => '86085',
+ 'Coussay-les-Bois (86270)' => '86086',
+ 'Couthures-sur-Garonne (47180)' => '47074',
+ 'Coutières (79340)' => '79105',
+ 'Coutras (33230)' => '33138',
+ 'Couture (16460)' => '16114',
+ 'Couture-d\'Argenson (79110)' => '79106',
+ 'Coutures (24320)' => '24141',
+ 'Coutures (33580)' => '33139',
+ 'Coux (17130)' => '17130',
+ 'Coux et Bigaroque-Mouzens (24220)' => '24142',
+ 'Couze-et-Saint-Front (24150)' => '24143',
+ 'Couzeix (87270)' => '87050',
+ 'Cozes (17120)' => '17131',
+ 'Cramchaban (17170)' => '17132',
+ 'Craon (86110)' => '86087',
+ 'Cravans (17260)' => '17133',
+ 'Crazannes (17350)' => '17134',
+ 'Créon (33670)' => '33140',
+ 'Créon-d\'Armagnac (40240)' => '40087',
+ 'Cressac-Saint-Genis (16250)' => '16115',
+ 'Cressat (23140)' => '23068',
+ 'Cressé (17160)' => '17135',
+ 'Creyssac (24350)' => '24144',
+ 'Creysse (24100)' => '24145',
+ 'Creyssensac-et-Pissot (24380)' => '24146',
+ 'Crézières (79110)' => '79107',
+ 'Criteuil-la-Magdeleine (16300)' => '16116',
+ 'Crocq (23260)' => '23069',
+ 'Croignon (33750)' => '33141',
+ 'Croix-Chapeau (17220)' => '17136',
+ 'Cromac (87160)' => '87053',
+ 'Crouseilles (64350)' => '64196',
+ 'Croutelle (86240)' => '86088',
+ 'Crozant (23160)' => '23070',
+ 'Croze (23500)' => '23071',
+ 'Cubjac (24640)' => '24147',
+ 'Cublac (19520)' => '19066',
+ 'Cubnezais (33620)' => '33142',
+ 'Cubzac-les-Ponts (33240)' => '33143',
+ 'Cudos (33430)' => '33144',
+ 'Cuhon (86110)' => '86089',
+ 'Cunèges (24240)' => '24148',
+ 'Cuq (47220)' => '47076',
+ 'Cuqueron (64360)' => '64197',
+ 'Curac (16210)' => '16117',
+ 'Curçay-sur-Dive (86120)' => '86090',
+ 'Curemonte (19500)' => '19067',
+ 'Cursan (33670)' => '33145',
+ 'Curzay-sur-Vonne (86600)' => '86091',
+ 'Cussac (87150)' => '87054',
+ 'Cussac-Fort-Médoc (33460)' => '33146',
+ 'Cuzorn (47500)' => '47077',
+ 'Daglan (24250)' => '24150',
+ 'Daignac (33420)' => '33147',
+ 'Damazan (47160)' => '47078',
+ 'Dampierre-sur-Boutonne (17470)' => '17138',
+ 'Dampniat (19360)' => '19068',
+ 'Dangé-Saint-Romain (86220)' => '86092',
+ 'Darazac (19220)' => '19069',
+ 'Dardenac (33420)' => '33148',
+ 'Darnac (87320)' => '87055',
+ 'Darnets (19300)' => '19070',
+ 'Daubèze (33540)' => '33149',
+ 'Dausse (47140)' => '47079',
+ 'Davignac (19250)' => '19071',
+ 'Dax (40100)' => '40088',
+ 'Denguin (64230)' => '64198',
+ 'Dercé (86420)' => '86093',
+ 'Deviat (16190)' => '16118',
+ 'Dévillac (47210)' => '47080',
+ 'Dienné (86410)' => '86094',
+ 'Dieulivol (33580)' => '33150',
+ 'Dignac (16410)' => '16119',
+ 'Dinsac (87210)' => '87056',
+ 'Dirac (16410)' => '16120',
+ 'Dissay (86130)' => '86095',
+ 'Diusse (64330)' => '64199',
+ 'Doazit (40700)' => '40089',
+ 'Doazon (64370)' => '64200',
+ 'Doeuil-sur-le-Mignon (17330)' => '17139',
+ 'Dognen (64190)' => '64201',
+ 'Doissat (24170)' => '24151',
+ 'Dolmayrac (47110)' => '47081',
+ 'Dolus-d\'Oléron (17550)' => '17140',
+ 'Domeyrot (23140)' => '23072',
+ 'Domezain-Berraute (64120)' => '64202',
+ 'Domme (24250)' => '24152',
+ 'Dompierre-les-Églises (87190)' => '87057',
+ 'Dompierre-sur-Charente (17610)' => '17141',
+ 'Dompierre-sur-Mer (17139)' => '17142',
+ 'Domps (87120)' => '87058',
+ 'Dondas (47470)' => '47082',
+ 'Donnezac (33860)' => '33151',
+ 'Dontreix (23700)' => '23073',
+ 'Donzac (33410)' => '33152',
+ 'Donzacq (40360)' => '40090',
+ 'Donzenac (19270)' => '19072',
+ 'Douchapt (24350)' => '24154',
+ 'Doudrac (47210)' => '47083',
+ 'Doulezon (33350)' => '33153',
+ 'Doumy (64450)' => '64203',
+ 'Dournazac (87230)' => '87060',
+ 'Doussay (86140)' => '86096',
+ 'Douville (24140)' => '24155',
+ 'Doux (79390)' => '79108',
+ 'Douzains (47330)' => '47084',
+ 'Douzat (16290)' => '16121',
+ 'Douzillac (24190)' => '24157',
+ 'Droux (87190)' => '87061',
+ 'Duhort-Bachen (40800)' => '40091',
+ 'Dumes (40500)' => '40092',
+ 'Dun-le-Palestel (23800)' => '23075',
+ 'Durance (47420)' => '47085',
+ 'Duras (47120)' => '47086',
+ 'Dussac (24270)' => '24158',
+ 'Eaux-Bonnes (64440)' => '64204',
+ 'Ébréon (16140)' => '16122',
+ 'Échallat (16170)' => '16123',
+ 'Échebrune (17800)' => '17145',
+ 'Échillais (17620)' => '17146',
+ 'Échiré (79410)' => '79109',
+ 'Échourgnac (24410)' => '24159',
+ 'Écoyeux (17770)' => '17147',
+ 'Écuras (16220)' => '16124',
+ 'Écurat (17810)' => '17148',
+ 'Édon (16320)' => '16125',
+ 'Égletons (19300)' => '19073',
+ 'Église-Neuve-d\'Issac (24400)' => '24161',
+ 'Église-Neuve-de-Vergt (24380)' => '24160',
+ 'Empuré (16240)' => '16127',
+ 'Engayrac (47470)' => '47087',
+ 'Ensigné (79170)' => '79111',
+ 'Épannes (79270)' => '79112',
+ 'Épargnes (17120)' => '17152',
+ 'Épenède (16490)' => '16128',
+ 'Éraville (16120)' => '16129',
+ 'Escalans (40310)' => '40093',
+ 'Escassefort (47350)' => '47088',
+ 'Escaudes (33840)' => '33155',
+ 'Escaunets (65500)' => '65160',
+ 'Esclottes (47120)' => '47089',
+ 'Escoire (24420)' => '24162',
+ 'Escos (64270)' => '64205',
+ 'Escot (64490)' => '64206',
+ 'Escou (64870)' => '64207',
+ 'Escoubès (64160)' => '64208',
+ 'Escource (40210)' => '40094',
+ 'Escoussans (33760)' => '33156',
+ 'Escout (64870)' => '64209',
+ 'Escurès (64350)' => '64210',
+ 'Eslourenties-Daban (64420)' => '64211',
+ 'Esnandes (17137)' => '17153',
+ 'Espagnac (19150)' => '19075',
+ 'Espartignac (19140)' => '19076',
+ 'Espéchède (64160)' => '64212',
+ 'Espelette (64250)' => '64213',
+ 'Espès-Undurein (64130)' => '64214',
+ 'Espiens (47600)' => '47090',
+ 'Espiet (33420)' => '33157',
+ 'Espiute (64390)' => '64215',
+ 'Espoey (64420)' => '64216',
+ 'Esquiule (64400)' => '64217',
+ 'Esse (16500)' => '16131',
+ 'Essouvert (17400)' => '17277',
+ 'Estérençuby (64220)' => '64218',
+ 'Estialescq (64290)' => '64219',
+ 'Estibeaux (40290)' => '40095',
+ 'Estigarde (40240)' => '40096',
+ 'Estillac (47310)' => '47091',
+ 'Estivals (19600)' => '19077',
+ 'Estivaux (19410)' => '19078',
+ 'Estos (64400)' => '64220',
+ 'Étagnac (16150)' => '16132',
+ 'Étaules (17750)' => '17155',
+ 'Étauliers (33820)' => '33159',
+ 'Etcharry (64120)' => '64221',
+ 'Etchebar (64470)' => '64222',
+ 'Étouars (24360)' => '24163',
+ 'Étriac (16250)' => '16133',
+ 'Etsaut (64490)' => '64223',
+ 'Eugénie-les-Bains (40320)' => '40097',
+ 'Évaux-les-Bains (23110)' => '23076',
+ 'Excideuil (24160)' => '24164',
+ 'Exideuil (16150)' => '16134',
+ 'Exireuil (79400)' => '79114',
+ 'Exoudun (79800)' => '79115',
+ 'Expiremont (17130)' => '17156',
+ 'Eybouleuf (87400)' => '87062',
+ 'Eyburie (19140)' => '19079',
+ 'Eygurande (19340)' => '19080',
+ 'Eygurande-et-Gardedeuil (24700)' => '24165',
+ 'Eyjeaux (87220)' => '87063',
+ 'Eyliac (24330)' => '24166',
+ 'Eymet (24500)' => '24167',
+ 'Eymouthiers (16220)' => '16135',
+ 'Eymoutiers (87120)' => '87064',
+ 'Eynesse (33220)' => '33160',
+ 'Eyrans (33390)' => '33161',
+ 'Eyrein (19800)' => '19081',
+ 'Eyres-Moncube (40500)' => '40098',
+ 'Eysines (33320)' => '33162',
+ 'Eysus (64400)' => '64224',
+ 'Eyvirat (24460)' => '24170',
+ 'Eyzerac (24800)' => '24171',
+ 'Faleyras (33760)' => '33163',
+ 'Fals (47220)' => '47092',
+ 'Fanlac (24290)' => '24174',
+ 'Fargues (33210)' => '33164',
+ 'Fargues (40500)' => '40099',
+ 'Fargues-Saint-Hilaire (33370)' => '33165',
+ 'Fargues-sur-Ourbise (47700)' => '47093',
+ 'Fauguerolles (47400)' => '47094',
+ 'Fauillet (47400)' => '47095',
+ 'Faurilles (24560)' => '24176',
+ 'Faux (24560)' => '24177',
+ 'Faux-la-Montagne (23340)' => '23077',
+ 'Faux-Mazuras (23400)' => '23078',
+ 'Favars (19330)' => '19082',
+ 'Faye-l\'Abbesse (79350)' => '79116',
+ 'Faye-sur-Ardin (79160)' => '79117',
+ 'Féas (64570)' => '64225',
+ 'Felletin (23500)' => '23079',
+ 'Fénery (79450)' => '79118',
+ 'Féniers (23100)' => '23080',
+ 'Fenioux (17350)' => '17157',
+ 'Fenioux (79160)' => '79119',
+ 'Ferrensac (47330)' => '47096',
+ 'Ferrières (17170)' => '17158',
+ 'Festalemps (24410)' => '24178',
+ 'Feugarolles (47230)' => '47097',
+ 'Feuillade (16380)' => '16137',
+ 'Feyt (19340)' => '19083',
+ 'Feytiat (87220)' => '87065',
+ 'Fichous-Riumayou (64410)' => '64226',
+ 'Fieux (47600)' => '47098',
+ 'Firbeix (24450)' => '24180',
+ 'Flaugeac (24240)' => '24181',
+ 'Flaujagues (33350)' => '33168',
+ 'Flavignac (87230)' => '87066',
+ 'Flayat (23260)' => '23081',
+ 'Fléac (16730)' => '16138',
+ 'Fléac-sur-Seugne (17800)' => '17159',
+ 'Fleix (86300)' => '86098',
+ 'Fleurac (16200)' => '16139',
+ 'Fleurac (24580)' => '24183',
+ 'Fleurat (23320)' => '23082',
+ 'Fleuré (86340)' => '86099',
+ 'Floirac (17120)' => '17160',
+ 'Floirac (33270)' => '33167',
+ 'Florimont-Gaumier (24250)' => '24184',
+ 'Floudès (33190)' => '33169',
+ 'Folles (87250)' => '87067',
+ 'Fomperron (79340)' => '79121',
+ 'Fongrave (47260)' => '47099',
+ 'Fonroque (24500)' => '24186',
+ 'Fontaine-Chalendray (17510)' => '17162',
+ 'Fontaine-le-Comte (86240)' => '86100',
+ 'Fontaines-d\'Ozillac (17500)' => '17163',
+ 'Fontanières (23110)' => '23083',
+ 'Fontclaireau (16230)' => '16140',
+ 'Fontcouverte (17100)' => '17164',
+ 'Fontenet (17400)' => '17165',
+ 'Fontenille (16230)' => '16141',
+ 'Fontenille-Saint-Martin-d\'Entraigues (79110)' => '79122',
+ 'Fontet (33190)' => '33170',
+ 'Forges (17290)' => '17166',
+ 'Forgès (19380)' => '19084',
+ 'Fors (79230)' => '79125',
+ 'Fossemagne (24210)' => '24188',
+ 'Fossès-et-Baleyssac (33190)' => '33171',
+ 'Fougueyrolles (33220)' => '24189',
+ 'Foulayronnes (47510)' => '47100',
+ 'Fouleix (24380)' => '24190',
+ 'Fouquebrune (16410)' => '16143',
+ 'Fouqueure (16140)' => '16144',
+ 'Fouras (17450)' => '17168',
+ 'Fourques-sur-Garonne (47200)' => '47101',
+ 'Fours (33390)' => '33172',
+ 'Foussignac (16200)' => '16145',
+ 'Fraisse (24130)' => '24191',
+ 'Francescas (47600)' => '47102',
+ 'François (79260)' => '79128',
+ 'Francs (33570)' => '33173',
+ 'Fransèches (23480)' => '23086',
+ 'Fréchou (47600)' => '47103',
+ 'Frégimont (47360)' => '47104',
+ 'Frespech (47140)' => '47105',
+ 'Fresselines (23450)' => '23087',
+ 'Fressines (79370)' => '79129',
+ 'Fromental (87250)' => '87068',
+ 'Fronsac (33126)' => '33174',
+ 'Frontenac (33760)' => '33175',
+ 'Frontenay-Rohan-Rohan (79270)' => '79130',
+ 'Frozes (86190)' => '86102',
+ 'Fumel (47500)' => '47106',
+ 'Gaas (40350)' => '40101',
+ 'Gabarnac (33410)' => '33176',
+ 'Gabarret (40310)' => '40102',
+ 'Gabaston (64160)' => '64227',
+ 'Gabat (64120)' => '64228',
+ 'Gabillou (24210)' => '24192',
+ 'Gageac-et-Rouillac (24240)' => '24193',
+ 'Gaillan-en-Médoc (33340)' => '33177',
+ 'Gaillères (40090)' => '40103',
+ 'Gajac (33430)' => '33178',
+ 'Gajoubert (87330)' => '87069',
+ 'Galapian (47190)' => '47107',
+ 'Galgon (33133)' => '33179',
+ 'Gamarde-les-Bains (40380)' => '40104',
+ 'Gamarthe (64220)' => '64229',
+ 'Gan (64290)' => '64230',
+ 'Gans (33430)' => '33180',
+ 'Garat (16410)' => '16146',
+ 'Gardegan-et-Tourtirac (33350)' => '33181',
+ 'Gardères (65320)' => '65185',
+ 'Gardes-le-Pontaroux (16320)' => '16147',
+ 'Gardonne (24680)' => '24194',
+ 'Garein (40420)' => '40105',
+ 'Garindein (64130)' => '64231',
+ 'Garlède-Mondebat (64450)' => '64232',
+ 'Garlin (64330)' => '64233',
+ 'Garos (64410)' => '64234',
+ 'Garrey (40180)' => '40106',
+ 'Garris (64120)' => '64235',
+ 'Garrosse (40110)' => '40107',
+ 'Gartempe (23320)' => '23088',
+ 'Gastes (40160)' => '40108',
+ 'Gaugeac (24540)' => '24195',
+ 'Gaujac (47200)' => '47108',
+ 'Gaujacq (40330)' => '40109',
+ 'Gauriac (33710)' => '33182',
+ 'Gauriaguet (33240)' => '33183',
+ 'Gavaudun (47150)' => '47109',
+ 'Gayon (64350)' => '64236',
+ 'Geaune (40320)' => '40110',
+ 'Geay (17250)' => '17171',
+ 'Geay (79330)' => '79131',
+ 'Gelos (64110)' => '64237',
+ 'Geloux (40090)' => '40111',
+ 'Gémozac (17260)' => '17172',
+ 'Genac-Bignac (16170)' => '16148',
+ 'Gençay (86160)' => '86103',
+ 'Générac (33920)' => '33184',
+ 'Génis (24160)' => '24196',
+ 'Génissac (33420)' => '33185',
+ 'Genneton (79150)' => '79132',
+ 'Genouillac (16270)' => '16149',
+ 'Genouillac (23350)' => '23089',
+ 'Genouillé (17430)' => '17174',
+ 'Genouillé (86250)' => '86104',
+ 'Gensac (33890)' => '33186',
+ 'Gensac-la-Pallue (16130)' => '16150',
+ 'Genté (16130)' => '16151',
+ 'Gentioux-Pigerolles (23340)' => '23090',
+ 'Ger (64530)' => '64238',
+ 'Gerderest (64160)' => '64239',
+ 'Gère-Bélesten (64260)' => '64240',
+ 'Germignac (17520)' => '17175',
+ 'Germond-Rouvre (79220)' => '79133',
+ 'Géronce (64400)' => '64241',
+ 'Gestas (64190)' => '64242',
+ 'Géus-d\'Arzacq (64370)' => '64243',
+ 'Geüs-d\'Oloron (64400)' => '64244',
+ 'Gibourne (17160)' => '17176',
+ 'Gibret (40380)' => '40112',
+ 'Gimel-les-Cascades (19800)' => '19085',
+ 'Gimeux (16130)' => '16152',
+ 'Ginestet (24130)' => '24197',
+ 'Gioux (23500)' => '23091',
+ 'Gironde-sur-Dropt (33190)' => '33187',
+ 'Giscos (33840)' => '33188',
+ 'Givrezac (17260)' => '17178',
+ 'Gizay (86340)' => '86105',
+ 'Glandon (87500)' => '87071',
+ 'Glanges (87380)' => '87072',
+ 'Glénay (79330)' => '79134',
+ 'Glénic (23380)' => '23092',
+ 'Glénouze (86200)' => '86106',
+ 'Goès (64400)' => '64245',
+ 'Gomer (64420)' => '64246',
+ 'Gond-Pontouvre (16160)' => '16154',
+ 'Gondeville (16200)' => '16153',
+ 'Gontaud-de-Nogaret (47400)' => '47110',
+ 'Goos (40180)' => '40113',
+ 'Gornac (33540)' => '33189',
+ 'Gorre (87310)' => '87073',
+ 'Gotein-Libarrenx (64130)' => '64247',
+ 'Goualade (33840)' => '33190',
+ 'Gouex (86320)' => '86107',
+ 'Goulles (19430)' => '19086',
+ 'Gourbera (40990)' => '40114',
+ 'Gourdon-Murat (19170)' => '19087',
+ 'Gourgé (79200)' => '79135',
+ 'Gournay-Loizé (79110)' => '79136',
+ 'Gours (33660)' => '33191',
+ 'Gourville (16170)' => '16156',
+ 'Gourvillette (17490)' => '17180',
+ 'Gousse (40465)' => '40115',
+ 'Gout-Rossignol (24320)' => '24199',
+ 'Gouts (40400)' => '40116',
+ 'Gouzon (23230)' => '23093',
+ 'Gradignan (33170)' => '33192',
+ 'Grand-Brassac (24350)' => '24200',
+ 'Grandjean (17350)' => '17181',
+ 'Grandsaigne (19300)' => '19088',
+ 'Granges-d\'Ans (24390)' => '24202',
+ 'Granges-sur-Lot (47260)' => '47111',
+ 'Granzay-Gript (79360)' => '79137',
+ 'Grassac (16380)' => '16158',
+ 'Grateloup-Saint-Gayrand (47400)' => '47112',
+ 'Graves-Saint-Amant (16120)' => '16297',
+ 'Grayan-et-l\'Hôpital (33590)' => '33193',
+ 'Grayssas (47270)' => '47113',
+ 'Grenade-sur-l\'Adour (40270)' => '40117',
+ 'Grézac (17120)' => '17183',
+ 'Grèzes (24120)' => '24204',
+ 'Grézet-Cavagnan (47250)' => '47114',
+ 'Grézillac (33420)' => '33194',
+ 'Grignols (24110)' => '24205',
+ 'Grignols (33690)' => '33195',
+ 'Grives (24170)' => '24206',
+ 'Groléjac (24250)' => '24207',
+ 'Gros-Chastang (19320)' => '19089',
+ 'Grun-Bordas (24380)' => '24208',
+ 'Guéret (23000)' => '23096',
+ 'Guérin (47250)' => '47115',
+ 'Guesnes (86420)' => '86109',
+ 'Guéthary (64210)' => '64249',
+ 'Guiche (64520)' => '64250',
+ 'Guillac (33420)' => '33196',
+ 'Guillos (33720)' => '33197',
+ 'Guimps (16300)' => '16160',
+ 'Guinarthe-Parenties (64390)' => '64251',
+ 'Guitinières (17500)' => '17187',
+ 'Guîtres (33230)' => '33198',
+ 'Guizengeard (16480)' => '16161',
+ 'Gujan-Mestras (33470)' => '33199',
+ 'Gumond (19320)' => '19090',
+ 'Gurat (16320)' => '16162',
+ 'Gurmençon (64400)' => '64252',
+ 'Gurs (64190)' => '64253',
+ 'Habas (40290)' => '40118',
+ 'Hagetaubin (64370)' => '64254',
+ 'Hagetmau (40700)' => '40119',
+ 'Haimps (17160)' => '17188',
+ 'Haims (86310)' => '86110',
+ 'Halsou (64480)' => '64255',
+ 'Hanc (79110)' => '79140',
+ 'Hasparren (64240)' => '64256',
+ 'Hastingues (40300)' => '40120',
+ 'Hauriet (40250)' => '40121',
+ 'Haut-de-Bosdarros (64800)' => '64257',
+ 'Haut-Mauco (40280)' => '40122',
+ 'Hautefage (19400)' => '19091',
+ 'Hautefage-la-Tour (47340)' => '47117',
+ 'Hautefaye (24300)' => '24209',
+ 'Hautefort (24390)' => '24210',
+ 'Hautesvignes (47400)' => '47118',
+ 'Haux (33550)' => '33201',
+ 'Haux (64470)' => '64258',
+ 'Hélette (64640)' => '64259',
+ 'Hendaye (64700)' => '64260',
+ 'Herm (40990)' => '40123',
+ 'Herré (40310)' => '40124',
+ 'Herrère (64680)' => '64261',
+ 'Heugas (40180)' => '40125',
+ 'Hiers-Brouage (17320)' => '17189',
+ 'Hiersac (16290)' => '16163',
+ 'Hiesse (16490)' => '16164',
+ 'Higuères-Souye (64160)' => '64262',
+ 'Hinx (40180)' => '40126',
+ 'Hontanx (40190)' => '40127',
+ 'Horsarrieu (40700)' => '40128',
+ 'Hosta (64120)' => '64265',
+ 'Hostens (33125)' => '33202',
+ 'Houeillès (47420)' => '47119',
+ 'Houlette (16200)' => '16165',
+ 'Hours (64420)' => '64266',
+ 'Hourtin (33990)' => '33203',
+ 'Hure (33190)' => '33204',
+ 'Ibarrolle (64120)' => '64267',
+ 'Idaux-Mendy (64130)' => '64268',
+ 'Idron (64320)' => '64269',
+ 'Igon (64800)' => '64270',
+ 'Iholdy (64640)' => '64271',
+ 'Île-d\'Aix (17123)' => '17004',
+ 'Ilharre (64120)' => '64272',
+ 'Illats (33720)' => '33205',
+ 'Ingrandes (86220)' => '86111',
+ 'Irais (79600)' => '79141',
+ 'Irissarry (64780)' => '64273',
+ 'Irouléguy (64220)' => '64274',
+ 'Isle (87170)' => '87075',
+ 'Isle-Saint-Georges (33640)' => '33206',
+ 'Ispoure (64220)' => '64275',
+ 'Issac (24400)' => '24211',
+ 'Issigeac (24560)' => '24212',
+ 'Issor (64570)' => '64276',
+ 'Issoudun-Létrieix (23130)' => '23097',
+ 'Isturits (64240)' => '64277',
+ 'Iteuil (86240)' => '86113',
+ 'Itxassou (64250)' => '64279',
+ 'Izeste (64260)' => '64280',
+ 'Izon (33450)' => '33207',
+ 'Jabreilles-les-Bordes (87370)' => '87076',
+ 'Jalesches (23270)' => '23098',
+ 'Janailhac (87800)' => '87077',
+ 'Janaillat (23250)' => '23099',
+ 'Jardres (86800)' => '86114',
+ 'Jarnac (16200)' => '16167',
+ 'Jarnac-Champagne (17520)' => '17192',
+ 'Jarnages (23140)' => '23100',
+ 'Jasses (64190)' => '64281',
+ 'Jatxou (64480)' => '64282',
+ 'Jau-Dignac-et-Loirac (33590)' => '33208',
+ 'Jauldes (16560)' => '16168',
+ 'Jaunay-Clan (86130)' => '86115',
+ 'Jaure (24140)' => '24213',
+ 'Javerdat (87520)' => '87078',
+ 'Javerlhac-et-la-Chapelle-Saint-Robert (24300)' => '24214',
+ 'Javrezac (16100)' => '16169',
+ 'Jaxu (64220)' => '64283',
+ 'Jayac (24590)' => '24215',
+ 'Jazeneuil (86600)' => '86116',
+ 'Jazennes (17260)' => '17196',
+ 'Jonzac (17500)' => '17197',
+ 'Josse (40230)' => '40129',
+ 'Jouac (87890)' => '87080',
+ 'Jouhet (86500)' => '86117',
+ 'Jouillat (23220)' => '23101',
+ 'Jourgnac (87800)' => '87081',
+ 'Journet (86290)' => '86118',
+ 'Journiac (24260)' => '24217',
+ 'Joussé (86350)' => '86119',
+ 'Jugazan (33420)' => '33209',
+ 'Jugeals-Nazareth (19500)' => '19093',
+ 'Juicq (17770)' => '17198',
+ 'Juignac (16190)' => '16170',
+ 'Juillac (19350)' => '19094',
+ 'Juillac (33890)' => '33210',
+ 'Juillac-le-Coq (16130)' => '16171',
+ 'Juillé (16230)' => '16173',
+ 'Juillé (79170)' => '79142',
+ 'Julienne (16200)' => '16174',
+ 'Jumilhac-le-Grand (24630)' => '24218',
+ 'Jurançon (64110)' => '64284',
+ 'Juscorps (79230)' => '79144',
+ 'Jusix (47180)' => '47120',
+ 'Jussas (17130)' => '17199',
+ 'Juxue (64120)' => '64285',
+ 'L\'Absie (79240)' => '79001',
+ 'L\'Église-aux-Bois (19170)' => '19074',
+ 'L\'Éguille (17600)' => '17151',
+ 'L\'Hôpital-d\'Orion (64270)' => '64263',
+ 'L\'Hôpital-Saint-Blaise (64130)' => '64264',
+ 'L\'Houmeau (17137)' => '17190',
+ 'L\'Isle-d\'Espagnac (16340)' => '16166',
+ 'L\'Isle-Jourdain (86150)' => '86112',
+ 'La Bachellerie (24210)' => '24020',
+ 'La Barde (17360)' => '17033',
+ 'La Bastide-Clairence (64240)' => '64289',
+ 'La Bataille (79110)' => '79027',
+ 'La Bazeuge (87210)' => '87008',
+ 'La Boissière-d\'Ans (24640)' => '24047',
+ 'La Boissière-en-Gâtine (79310)' => '79040',
+ 'La Brède (33650)' => '33213',
+ 'La Brée-les-Bains (17840)' => '17486',
+ 'La Brionne (23000)' => '23033',
+ 'La Brousse (17160)' => '17071',
+ 'La Bussière (86310)' => '86040',
+ 'La Cassagne (24120)' => '24085',
+ 'La Celle-Dunoise (23800)' => '23039',
+ 'La Celle-sous-Gouzon (23230)' => '23040',
+ 'La Cellette (23350)' => '23041',
+ 'La Chapelle (16140)' => '16081',
+ 'La Chapelle-Aubareil (24290)' => '24106',
+ 'La Chapelle-aux-Brocs (19360)' => '19043',
+ 'La Chapelle-aux-Saints (19120)' => '19044',
+ 'La Chapelle-Baloue (23160)' => '23050',
+ 'La Chapelle-Bâton (79220)' => '79070',
+ 'La Chapelle-Bâton (86250)' => '86055',
+ 'La Chapelle-Bertrand (79200)' => '79071',
+ 'La Chapelle-des-Pots (17100)' => '17089',
+ 'La Chapelle-Faucher (24530)' => '24107',
+ 'La Chapelle-Gonaguet (24350)' => '24108',
+ 'La Chapelle-Grésignac (24320)' => '24109',
+ 'La Chapelle-Montabourlet (24320)' => '24110',
+ 'La Chapelle-Montbrandeix (87440)' => '87037',
+ 'La Chapelle-Montmoreau (24300)' => '24111',
+ 'La Chapelle-Montreuil (86470)' => '86056',
+ 'La Chapelle-Moulière (86210)' => '86058',
+ 'La Chapelle-Pouilloux (79190)' => '79074',
+ 'La Chapelle-Saint-Étienne (79240)' => '79075',
+ 'La Chapelle-Saint-Géraud (19430)' => '19045',
+ 'La Chapelle-Saint-Jean (24390)' => '24113',
+ 'La Chapelle-Saint-Laurent (79430)' => '79076',
+ 'La Chapelle-Saint-Martial (23250)' => '23051',
+ 'La Chapelle-Taillefert (23000)' => '23052',
+ 'La Chapelle-Thireuil (79160)' => '79077',
+ 'La Chaussade (23200)' => '23059',
+ 'La Chaussée (86330)' => '86069',
+ 'La Chèvrerie (16240)' => '16098',
+ 'La Clisse (17600)' => '17112',
+ 'La Clotte (17360)' => '17113',
+ 'La Coquille (24450)' => '24133',
+ 'La Couarde (79800)' => '79098',
+ 'La Couarde-sur-Mer (17670)' => '17121',
+ 'La Couronne (16400)' => '16113',
+ 'La Courtine (23100)' => '23067',
+ 'La Crèche (79260)' => '79048',
+ 'La Croisille-sur-Briance (87130)' => '87051',
+ 'La Croix-Blanche (47340)' => '47075',
+ 'La Croix-Comtesse (17330)' => '17137',
+ 'La Croix-sur-Gartempe (87210)' => '87052',
+ 'La Dornac (24120)' => '24153',
+ 'La Douze (24330)' => '24156',
+ 'La Faye (16700)' => '16136',
+ 'La Ferrière-Airoux (86160)' => '86097',
+ 'La Ferrière-en-Parthenay (79390)' => '79120',
+ 'La Feuillade (24120)' => '24179',
+ 'La Flotte (17630)' => '17161',
+ 'La Force (24130)' => '24222',
+ 'La Forêt-de-Tessé (16240)' => '16142',
+ 'La Forêt-du-Temple (23360)' => '23084',
+ 'La Forêt-sur-Sèvre (79380)' => '79123',
+ 'La Foye-Monjault (79360)' => '79127',
+ 'La Frédière (17770)' => '17169',
+ 'La Genétouze (17360)' => '17173',
+ 'La Geneytouse (87400)' => '87070',
+ 'La Gonterie-Boulouneix (24310)' => '24198',
+ 'La Grève-sur-Mignon (17170)' => '17182',
+ 'La Grimaudière (86330)' => '86108',
+ 'La Gripperie-Saint-Symphorien (17620)' => '17184',
+ 'La Jard (17460)' => '17191',
+ 'La Jarne (17220)' => '17193',
+ 'La Jarrie (17220)' => '17194',
+ 'La Jarrie-Audouin (17330)' => '17195',
+ 'La Jemaye (24410)' => '24216',
+ 'La Jonchère-Saint-Maurice (87340)' => '87079',
+ 'La Laigne (17170)' => '17201',
+ 'La Lande-de-Fronsac (33240)' => '33219',
+ 'La Magdeleine (16240)' => '16197',
+ 'La Mazière-aux-Bons-Hommes (23260)' => '23129',
+ 'La Meyze (87800)' => '87096',
+ 'La Mothe-Saint-Héray (79800)' => '79184',
+ 'La Nouaille (23500)' => '23144',
+ 'La Péruse (16270)' => '16259',
+ 'La Petite-Boissière (79700)' => '79207',
+ 'La Peyratte (79200)' => '79208',
+ 'La Porcherie (87380)' => '87120',
+ 'La Pouge (23250)' => '23157',
+ 'La Puye (86260)' => '86202',
+ 'La Réole (33190)' => '33352',
+ 'La Réunion (47700)' => '47222',
+ 'La Rivière (33126)' => '33356',
+ 'La Roche-Canillac (19320)' => '19174',
+ 'La Roche-Chalais (24490)' => '24354',
+ 'La Roche-l\'Abeille (87800)' => '87127',
+ 'La Roche-Posay (86270)' => '86207',
+ 'La Roche-Rigault (86200)' => '86079',
+ 'La Rochebeaucourt-et-Argentine (24340)' => '24353',
+ 'La Rochefoucauld (16110)' => '16281',
+ 'La Rochelle (17000)' => '17300',
+ 'La Rochénard (79270)' => '79229',
+ 'La Rochette (16110)' => '16282',
+ 'La Ronde (17170)' => '17303',
+ 'La Roque-Gageac (24250)' => '24355',
+ 'La Roquille (33220)' => '33360',
+ 'La Saunière (23000)' => '23169',
+ 'La Sauve (33670)' => '33505',
+ 'La Sauvetat-de-Savères (47270)' => '47289',
+ 'La Sauvetat-du-Dropt (47800)' => '47290',
+ 'La Sauvetat-sur-Lède (47150)' => '47291',
+ 'La Serre-Bussière-Vieille (23190)' => '23172',
+ 'La Souterraine (23300)' => '23176',
+ 'La Tâche (16260)' => '16377',
+ 'La Teste-de-Buch (33260)' => '33529',
+ 'La Tour-Blanche (24320)' => '24554',
+ 'La Tremblade (17390)' => '17452',
+ 'La Trimouille (86290)' => '86273',
+ 'La Vallée (17250)' => '17455',
+ 'La Vergne (17400)' => '17465',
+ 'La Villedieu (17470)' => '17471',
+ 'La Villedieu (23340)' => '23264',
+ 'La Villedieu-du-Clain (86340)' => '86290',
+ 'La Villeneuve (23260)' => '23265',
+ 'La Villetelle (23260)' => '23266',
+ 'Laà-Mondrans (64300)' => '64286',
+ 'Laàs (64390)' => '64287',
+ 'Labarde (33460)' => '33211',
+ 'Labastide-Castel-Amouroux (47250)' => '47121',
+ 'Labastide-Cézéracq (64170)' => '64288',
+ 'Labastide-Chalosse (40700)' => '40130',
+ 'Labastide-d\'Armagnac (40240)' => '40131',
+ 'Labastide-Monréjeau (64170)' => '64290',
+ 'Labastide-Villefranche (64270)' => '64291',
+ 'Labatmale (64530)' => '64292',
+ 'Labatut (40300)' => '40132',
+ 'Labatut (64460)' => '64293',
+ 'Labenne (40530)' => '40133',
+ 'Labescau (33690)' => '33212',
+ 'Labets-Biscay (64120)' => '64294',
+ 'Labeyrie (64300)' => '64295',
+ 'Labouheyre (40210)' => '40134',
+ 'Labretonie (47350)' => '47122',
+ 'Labrit (40420)' => '40135',
+ 'Lacadée (64300)' => '64296',
+ 'Lacajunte (40320)' => '40136',
+ 'Lacanau (33680)' => '33214',
+ 'Lacapelle-Biron (47150)' => '47123',
+ 'Lacarre (64220)' => '64297',
+ 'Lacarry-Arhan-Charritte-de-Haut (64470)' => '64298',
+ 'Lacaussade (47150)' => '47124',
+ 'Lacelle (19170)' => '19095',
+ 'Lacépède (47360)' => '47125',
+ 'Lachaise (16300)' => '16176',
+ 'Lachapelle (47350)' => '47126',
+ 'Lacommande (64360)' => '64299',
+ 'Lacq (64170)' => '64300',
+ 'Lacquy (40120)' => '40137',
+ 'Lacrabe (40700)' => '40138',
+ 'Lacropte (24380)' => '24220',
+ 'Ladapeyre (23270)' => '23102',
+ 'Ladaux (33760)' => '33215',
+ 'Ladignac-le-Long (87500)' => '87082',
+ 'Ladignac-sur-Rondelles (19150)' => '19096',
+ 'Ladiville (16120)' => '16177',
+ 'Lados (33124)' => '33216',
+ 'Lafage-sur-Sombre (19320)' => '19097',
+ 'Lafat (23800)' => '23103',
+ 'Lafitte-sur-Lot (47320)' => '47127',
+ 'Lafox (47240)' => '47128',
+ 'Lagarde-Enval (19150)' => '19098',
+ 'Lagarde-sur-le-Né (16300)' => '16178',
+ 'Lagarrigue (47190)' => '47129',
+ 'Lageon (79200)' => '79145',
+ 'Lagleygeolle (19500)' => '19099',
+ 'Laglorieuse (40090)' => '40139',
+ 'Lagor (64150)' => '64301',
+ 'Lagorce (33230)' => '33218',
+ 'Lagord (17140)' => '17200',
+ 'Lagos (64800)' => '64302',
+ 'Lagrange (40240)' => '40140',
+ 'Lagraulière (19700)' => '19100',
+ 'Lagruère (47400)' => '47130',
+ 'Laguenne (19150)' => '19101',
+ 'Laguinge-Restoue (64470)' => '64303',
+ 'Lagupie (47180)' => '47131',
+ 'Lahonce (64990)' => '64304',
+ 'Lahontan (64270)' => '64305',
+ 'Lahosse (40250)' => '40141',
+ 'Lahourcade (64150)' => '64306',
+ 'Lalande-de-Pomerol (33500)' => '33222',
+ 'Lalandusse (47330)' => '47132',
+ 'Lalinde (24150)' => '24223',
+ 'Lalongue (64350)' => '64307',
+ 'Lalonquette (64450)' => '64308',
+ 'Laluque (40465)' => '40142',
+ 'Lamarque (33460)' => '33220',
+ 'Lamayou (64460)' => '64309',
+ 'Lamazière-Basse (19160)' => '19102',
+ 'Lamazière-Haute (19340)' => '19103',
+ 'Lamongerie (19510)' => '19104',
+ 'Lamontjoie (47310)' => '47133',
+ 'Lamonzie-Montastruc (24520)' => '24224',
+ 'Lamonzie-Saint-Martin (24680)' => '24225',
+ 'Lamothe (40250)' => '40143',
+ 'Lamothe-Landerron (33190)' => '33221',
+ 'Lamothe-Montravel (24230)' => '24226',
+ 'Landerrouat (33790)' => '33223',
+ 'Landerrouet-sur-Ségur (33540)' => '33224',
+ 'Landes (17380)' => '17202',
+ 'Landiras (33720)' => '33225',
+ 'Landrais (17290)' => '17203',
+ 'Langoiran (33550)' => '33226',
+ 'Langon (33210)' => '33227',
+ 'Lanne-en-Barétous (64570)' => '64310',
+ 'Lannecaube (64350)' => '64311',
+ 'Lanneplaà (64300)' => '64312',
+ 'Lannes (47170)' => '47134',
+ 'Lanouaille (24270)' => '24227',
+ 'Lanquais (24150)' => '24228',
+ 'Lansac (33710)' => '33228',
+ 'Lantabat (64640)' => '64313',
+ 'Lanteuil (19190)' => '19105',
+ 'Lanton (33138)' => '33229',
+ 'Laparade (47260)' => '47135',
+ 'Laperche (47800)' => '47136',
+ 'Lapleau (19550)' => '19106',
+ 'Laplume (47310)' => '47137',
+ 'Lapouyade (33620)' => '33230',
+ 'Laprade (16390)' => '16180',
+ 'Larbey (40250)' => '40144',
+ 'Larceveau-Arros-Cibits (64120)' => '64314',
+ 'Larche (19600)' => '19107',
+ 'Largeasse (79240)' => '79147',
+ 'Laroche-près-Feyt (19340)' => '19108',
+ 'Laroin (64110)' => '64315',
+ 'Laroque (33410)' => '33231',
+ 'Laroque-Timbaut (47340)' => '47138',
+ 'Larrau (64560)' => '64316',
+ 'Larressore (64480)' => '64317',
+ 'Larreule (64410)' => '64318',
+ 'Larribar-Sorhapuru (64120)' => '64319',
+ 'Larrivière-Saint-Savin (40270)' => '40145',
+ 'Lartigue (33840)' => '33232',
+ 'Laruns (64440)' => '64320',
+ 'Laruscade (33620)' => '33233',
+ 'Larzac (24170)' => '24230',
+ 'Lascaux (19130)' => '19109',
+ 'Lasclaveries (64450)' => '64321',
+ 'Lasse (64220)' => '64322',
+ 'Lasserre (47600)' => '47139',
+ 'Lasserre (64350)' => '64323',
+ 'Lasseube (64290)' => '64324',
+ 'Lasseubetat (64290)' => '64325',
+ 'Lathus-Saint-Rémy (86390)' => '86120',
+ 'Latillé (86190)' => '86121',
+ 'Latresne (33360)' => '33234',
+ 'Latrille (40800)' => '40146',
+ 'Latronche (19160)' => '19110',
+ 'Laugnac (47360)' => '47140',
+ 'Laurède (40250)' => '40147',
+ 'Lauret (40320)' => '40148',
+ 'Laurière (87370)' => '87083',
+ 'Laussou (47150)' => '47141',
+ 'Lauthiers (86300)' => '86122',
+ 'Lauzun (47410)' => '47142',
+ 'Laval-sur-Luzège (19550)' => '19111',
+ 'Lavalade (24540)' => '24231',
+ 'Lavardac (47230)' => '47143',
+ 'Lavaufranche (23600)' => '23104',
+ 'Lavaur (24550)' => '24232',
+ 'Lavausseau (86470)' => '86123',
+ 'Lavaveix-les-Mines (23150)' => '23105',
+ 'Lavazan (33690)' => '33235',
+ 'Lavergne (47800)' => '47144',
+ 'Laveyssière (24130)' => '24233',
+ 'Lavignac (87230)' => '87084',
+ 'Lavoux (86800)' => '86124',
+ 'Lay-Lamidou (64190)' => '64326',
+ 'Layrac (47390)' => '47145',
+ 'Le Barp (33114)' => '33029',
+ 'Le Beugnon (79130)' => '79035',
+ 'Le Bois-Plage-en-Ré (17580)' => '17051',
+ 'Le Bouchage (16350)' => '16054',
+ 'Le Bourdeix (24300)' => '24056',
+ 'Le Bourdet (79210)' => '79046',
+ 'Le Bourg-d\'Hem (23220)' => '23029',
+ 'Le Bouscat (33110)' => '33069',
+ 'Le Breuil-Bernard (79320)' => '79051',
+ 'Le Bugue (24260)' => '24067',
+ 'Le Buis (87140)' => '87023',
+ 'Le Buisson-de-Cadouin (24480)' => '24068',
+ 'Le Busseau (79240)' => '79059',
+ 'Le Chalard (87500)' => '87031',
+ 'Le Change (24640)' => '24103',
+ 'Le Chastang (19190)' => '19048',
+ 'Le Château-d\'Oléron (17480)' => '17093',
+ 'Le Châtenet-en-Dognon (87400)' => '87042',
+ 'Le Chauchet (23130)' => '23058',
+ 'Le Chay (17600)' => '17097',
+ 'Le Chillou (79600)' => '79089',
+ 'Le Compas (23700)' => '23066',
+ 'Le Donzeil (23480)' => '23074',
+ 'Le Dorat (87210)' => '87059',
+ 'Le Douhet (17100)' => '17143',
+ 'Le Fieu (33230)' => '33166',
+ 'Le Fleix (24130)' => '24182',
+ 'Le Fouilloux (17270)' => '17167',
+ 'Le Frêche (40190)' => '40100',
+ 'Le Gicq (17160)' => '17177',
+ 'Le Grand-Bourg (23240)' => '23095',
+ 'Le Grand-Madieu (16450)' => '16157',
+ 'Le Grand-Village-Plage (17370)' => '17485',
+ 'Le Gua (17600)' => '17185',
+ 'Le Gué-d\'Alleré (17540)' => '17186',
+ 'Le Haillan (33185)' => '33200',
+ 'Le Jardin (19300)' => '19092',
+ 'Le Lardin-Saint-Lazare (24570)' => '24229',
+ 'Le Leuy (40250)' => '40153',
+ 'Le Lindois (16310)' => '16188',
+ 'Le Lonzac (19470)' => '19118',
+ 'Le Mas-d\'Agenais (47430)' => '47159',
+ 'Le Mas-d\'Artige (23100)' => '23125',
+ 'Le Monteil-au-Vicomte (23460)' => '23134',
+ 'Le Mung (17350)' => '17252',
+ 'Le Nizan (33430)' => '33305',
+ 'Le Palais-sur-Vienne (87410)' => '87113',
+ 'Le Passage (47520)' => '47201',
+ 'Le Pescher (19190)' => '19163',
+ 'Le Pian-Médoc (33290)' => '33322',
+ 'Le Pian-sur-Garonne (33490)' => '33323',
+ 'Le Pin (17210)' => '17276',
+ 'Le Pin (79140)' => '79210',
+ 'Le Pizou (24700)' => '24329',
+ 'Le Porge (33680)' => '33333',
+ 'Le Pout (33670)' => '33335',
+ 'Le Puy (33580)' => '33345',
+ 'Le Retail (79130)' => '79226',
+ 'Le Rochereau (86170)' => '86208',
+ 'Le Sen (40420)' => '40297',
+ 'Le Seure (17770)' => '17426',
+ 'Le Taillan-Médoc (33320)' => '33519',
+ 'Le Tallud (79200)' => '79322',
+ 'Le Tâtre (16360)' => '16380',
+ 'Le Teich (33470)' => '33527',
+ 'Le Temple (33680)' => '33528',
+ 'Le Temple-sur-Lot (47110)' => '47306',
+ 'Le Thou (17290)' => '17447',
+ 'Le Tourne (33550)' => '33534',
+ 'Le Tuzan (33125)' => '33536',
+ 'Le Vanneau-Irleau (79270)' => '79337',
+ 'Le Verdon-sur-Mer (33123)' => '33544',
+ 'Le Vert (79170)' => '79346',
+ 'Le Vieux-Cérier (16350)' => '16403',
+ 'Le Vigeant (86150)' => '86289',
+ 'Le Vigen (87110)' => '87205',
+ 'Le Vignau (40270)' => '40329',
+ 'Lecumberry (64220)' => '64327',
+ 'Lédat (47300)' => '47146',
+ 'Ledeuix (64400)' => '64328',
+ 'Lée (64320)' => '64329',
+ 'Lées-Athas (64490)' => '64330',
+ 'Lège-Cap-Ferret (33950)' => '33236',
+ 'Léguillac-de-Cercles (24340)' => '24235',
+ 'Léguillac-de-l\'Auche (24110)' => '24236',
+ 'Leigné-les-Bois (86450)' => '86125',
+ 'Leigné-sur-Usseau (86230)' => '86127',
+ 'Leignes-sur-Fontaine (86300)' => '86126',
+ 'Lembeye (64350)' => '64331',
+ 'Lembras (24100)' => '24237',
+ 'Lème (64450)' => '64332',
+ 'Lempzours (24800)' => '24238',
+ 'Lencloître (86140)' => '86128',
+ 'Lencouacq (40120)' => '40149',
+ 'Léogeats (33210)' => '33237',
+ 'Léognan (33850)' => '33238',
+ 'Léon (40550)' => '40150',
+ 'Léoville (17500)' => '17204',
+ 'Lépaud (23170)' => '23106',
+ 'Lépinas (23150)' => '23107',
+ 'Léren (64270)' => '64334',
+ 'Lerm-et-Musset (33840)' => '33239',
+ 'Les Adjots (16700)' => '16002',
+ 'Les Alleuds (79190)' => '79006',
+ 'Les Angles-sur-Corrèze (19000)' => '19009',
+ 'Les Artigues-de-Lussac (33570)' => '33014',
+ 'Les Billanges (87340)' => '87016',
+ 'Les Billaux (33500)' => '33052',
+ 'Les Cars (87230)' => '87029',
+ 'Les Éduts (17510)' => '17149',
+ 'Les Églises-d\'Argenteuil (17400)' => '17150',
+ 'Les Églisottes-et-Chalaures (33230)' => '33154',
+ 'Les Essards (16210)' => '16130',
+ 'Les Essards (17250)' => '17154',
+ 'Les Esseintes (33190)' => '33158',
+ 'Les Eyzies-de-Tayac-Sireuil (24620)' => '24172',
+ 'Les Farges (24290)' => '24175',
+ 'Les Forges (79340)' => '79124',
+ 'Les Fosses (79360)' => '79126',
+ 'Les Gonds (17100)' => '17179',
+ 'Les Gours (16140)' => '16155',
+ 'Les Grands-Chézeaux (87160)' => '87074',
+ 'Les Graulges (24340)' => '24203',
+ 'Les Groseillers (79220)' => '79139',
+ 'Les Lèches (24400)' => '24234',
+ 'Les Lèves-et-Thoumeyragues (33220)' => '33242',
+ 'Les Mars (23700)' => '23123',
+ 'Les Mathes (17570)' => '17225',
+ 'Les Métairies (16200)' => '16220',
+ 'Les Nouillers (17380)' => '17266',
+ 'Les Ormes (86220)' => '86183',
+ 'Les Peintures (33230)' => '33315',
+ 'Les Pins (16260)' => '16261',
+ 'Les Portes-en-Ré (17880)' => '17286',
+ 'Les Salles-de-Castillon (33350)' => '33499',
+ 'Les Salles-Lavauguyon (87440)' => '87189',
+ 'Les Touches-de-Périgny (17160)' => '17451',
+ 'Les Trois-Moutiers (86120)' => '86274',
+ 'Lescar (64230)' => '64335',
+ 'Lescun (64490)' => '64336',
+ 'Lesgor (40400)' => '40151',
+ 'Lésignac-Durand (16310)' => '16183',
+ 'Lésigny (86270)' => '86129',
+ 'Lesparre-Médoc (33340)' => '33240',
+ 'Lesperon (40260)' => '40152',
+ 'Lespielle (64350)' => '64337',
+ 'Lespourcy (64160)' => '64338',
+ 'Lessac (16500)' => '16181',
+ 'Lestards (19170)' => '19112',
+ 'Lestelle-Bétharram (64800)' => '64339',
+ 'Lesterps (16420)' => '16182',
+ 'Lestiac-sur-Garonne (33550)' => '33241',
+ 'Leugny (86220)' => '86130',
+ 'Lévignac-de-Guyenne (47120)' => '47147',
+ 'Lévignacq (40170)' => '40154',
+ 'Leyrat (23600)' => '23108',
+ 'Leyritz-Moncassin (47700)' => '47148',
+ 'Lezay (79120)' => '79148',
+ 'Lhommaizé (86410)' => '86131',
+ 'Lhoumois (79390)' => '79149',
+ 'Libourne (33500)' => '33243',
+ 'Lichans-Sunhar (64470)' => '64340',
+ 'Lichères (16460)' => '16184',
+ 'Lichos (64130)' => '64341',
+ 'Licq-Athérey (64560)' => '64342',
+ 'Liginiac (19160)' => '19113',
+ 'Liglet (86290)' => '86132',
+ 'Lignan-de-Bazas (33430)' => '33244',
+ 'Lignan-de-Bordeaux (33360)' => '33245',
+ 'Lignareix (19200)' => '19114',
+ 'Ligné (16140)' => '16185',
+ 'Ligneyrac (19500)' => '19115',
+ 'Lignières-Sonneville (16130)' => '16186',
+ 'Ligueux (33220)' => '33246',
+ 'Ligugé (86240)' => '86133',
+ 'Limalonges (79190)' => '79150',
+ 'Limendous (64420)' => '64343',
+ 'Limeuil (24510)' => '24240',
+ 'Limeyrat (24210)' => '24241',
+ 'Limoges (87000)' => '87085',
+ 'Linard (23220)' => '23109',
+ 'Linards (87130)' => '87086',
+ 'Linars (16730)' => '16187',
+ 'Linazay (86400)' => '86134',
+ 'Liniers (86800)' => '86135',
+ 'Linxe (40260)' => '40155',
+ 'Liorac-sur-Louyre (24520)' => '24242',
+ 'Liourdres (19120)' => '19116',
+ 'Lioux-les-Monges (23700)' => '23110',
+ 'Liposthey (40410)' => '40156',
+ 'Lisle (24350)' => '24243',
+ 'Lissac-sur-Couze (19600)' => '19117',
+ 'Listrac-de-Durèze (33790)' => '33247',
+ 'Listrac-Médoc (33480)' => '33248',
+ 'Lit-et-Mixe (40170)' => '40157',
+ 'Livron (64530)' => '64344',
+ 'Lizant (86400)' => '86136',
+ 'Lizières (23240)' => '23111',
+ 'Lohitzun-Oyhercq (64120)' => '64345',
+ 'Loire-les-Marais (17870)' => '17205',
+ 'Loiré-sur-Nie (17470)' => '17206',
+ 'Loix (17111)' => '17207',
+ 'Lolme (24540)' => '24244',
+ 'Lombia (64160)' => '64346',
+ 'Lonçon (64410)' => '64347',
+ 'Londigny (16700)' => '16189',
+ 'Longèves (17230)' => '17208',
+ 'Longré (16240)' => '16190',
+ 'Longueville (47200)' => '47150',
+ 'Lonnes (16230)' => '16191',
+ 'Lons (64140)' => '64348',
+ 'Lonzac (17520)' => '17209',
+ 'Lorignac (17240)' => '17210',
+ 'Lorigné (79190)' => '79152',
+ 'Lormont (33310)' => '33249',
+ 'Losse (40240)' => '40158',
+ 'Lostanges (19500)' => '19119',
+ 'Loubejac (24550)' => '24245',
+ 'Loubens (33190)' => '33250',
+ 'Loubès-Bernac (47120)' => '47151',
+ 'Loubieng (64300)' => '64349',
+ 'Loubigné (79110)' => '79153',
+ 'Loubillé (79110)' => '79154',
+ 'Louchats (33125)' => '33251',
+ 'Loudun (86200)' => '86137',
+ 'Louer (40380)' => '40159',
+ 'Lougratte (47290)' => '47152',
+ 'Louhossoa (64250)' => '64350',
+ 'Louignac (19310)' => '19120',
+ 'Louin (79600)' => '79156',
+ 'Loulay (17330)' => '17211',
+ 'Loupes (33370)' => '33252',
+ 'Loupiac (33410)' => '33253',
+ 'Loupiac-de-la-Réole (33190)' => '33254',
+ 'Lourdios-Ichère (64570)' => '64351',
+ 'Lourdoueix-Saint-Pierre (23360)' => '23112',
+ 'Lourenties (64420)' => '64352',
+ 'Lourquen (40250)' => '40160',
+ 'Louvie-Juzon (64260)' => '64353',
+ 'Louvie-Soubiron (64440)' => '64354',
+ 'Louvigny (64410)' => '64355',
+ 'Louzac-Saint-André (16100)' => '16193',
+ 'Louzignac (17160)' => '17212',
+ 'Louzy (79100)' => '79157',
+ 'Lozay (17330)' => '17213',
+ 'Lubbon (40240)' => '40161',
+ 'Lubersac (19210)' => '19121',
+ 'Luc-Armau (64350)' => '64356',
+ 'Lucarré (64350)' => '64357',
+ 'Lucbardez-et-Bargues (40090)' => '40162',
+ 'Lucgarier (64420)' => '64358',
+ 'Luchapt (86430)' => '86138',
+ 'Luchat (17600)' => '17214',
+ 'Luché-sur-Brioux (79170)' => '79158',
+ 'Luché-Thouarsais (79330)' => '79159',
+ 'Lucmau (33840)' => '33255',
+ 'Lucq-de-Béarn (64360)' => '64359',
+ 'Ludon-Médoc (33290)' => '33256',
+ 'Lüe (40210)' => '40163',
+ 'Lugaignac (33420)' => '33257',
+ 'Lugasson (33760)' => '33258',
+ 'Luglon (40630)' => '40165',
+ 'Lugon-et-l\'Île-du-Carnay (33240)' => '33259',
+ 'Lugos (33830)' => '33260',
+ 'Lunas (24130)' => '24246',
+ 'Lupersat (23190)' => '23113',
+ 'Lupsault (16140)' => '16194',
+ 'Luquet (65320)' => '65292',
+ 'Lurbe-Saint-Christau (64660)' => '64360',
+ 'Lusignac (24320)' => '24247',
+ 'Lusignan (86600)' => '86139',
+ 'Lusignan-Petit (47360)' => '47154',
+ 'Lussac (16450)' => '16195',
+ 'Lussac (17500)' => '17215',
+ 'Lussac (33570)' => '33261',
+ 'Lussac-les-Châteaux (86320)' => '86140',
+ 'Lussac-les-Églises (87360)' => '87087',
+ 'Lussagnet (40270)' => '40166',
+ 'Lussagnet-Lusson (64160)' => '64361',
+ 'Lussant (17430)' => '17216',
+ 'Lussas-et-Nontronneau (24300)' => '24248',
+ 'Lussat (23170)' => '23114',
+ 'Lusseray (79170)' => '79160',
+ 'Luxé (16230)' => '16196',
+ 'Luxe-Sumberraute (64120)' => '64362',
+ 'Luxey (40430)' => '40167',
+ 'Luzay (79100)' => '79161',
+ 'Lys (64260)' => '64363',
+ 'Macau (33460)' => '33262',
+ 'Macaye (64240)' => '64364',
+ 'Macqueville (17490)' => '17217',
+ 'Madaillan (47360)' => '47155',
+ 'Madirac (33670)' => '33263',
+ 'Madranges (19470)' => '19122',
+ 'Magescq (40140)' => '40168',
+ 'Magnac-Bourg (87380)' => '87088',
+ 'Magnac-Laval (87190)' => '87089',
+ 'Magnac-Lavalette-Villars (16320)' => '16198',
+ 'Magnac-sur-Touvre (16600)' => '16199',
+ 'Magnat-l\'Étrange (23260)' => '23115',
+ 'Magné (79460)' => '79162',
+ 'Magné (86160)' => '86141',
+ 'Mailhac-sur-Benaize (87160)' => '87090',
+ 'Maillas (40120)' => '40169',
+ 'Maillé (86190)' => '86142',
+ 'Maillères (40120)' => '40170',
+ 'Maine-de-Boixe (16230)' => '16200',
+ 'Mainsat (23700)' => '23116',
+ 'Mainxe (16200)' => '16202',
+ 'Mainzac (16380)' => '16203',
+ 'Mairé (86270)' => '86143',
+ 'Mairé-Levescault (79190)' => '79163',
+ 'Maison-Feyne (23800)' => '23117',
+ 'Maisonnais-sur-Tardoire (87440)' => '87091',
+ 'Maisonnay (79500)' => '79164',
+ 'Maisonneuve (86170)' => '86144',
+ 'Maisonnisses (23150)' => '23118',
+ 'Maisontiers (79600)' => '79165',
+ 'Malaussanne (64410)' => '64365',
+ 'Malaville (16120)' => '16204',
+ 'Malemort (19360)' => '19123',
+ 'Malleret (23260)' => '23119',
+ 'Malleret-Boussac (23600)' => '23120',
+ 'Malval (23220)' => '23121',
+ 'Manaurie (24620)' => '24249',
+ 'Mano (40410)' => '40171',
+ 'Manot (16500)' => '16205',
+ 'Mansac (19520)' => '19124',
+ 'Mansat-la-Courrière (23400)' => '23122',
+ 'Mansle (16230)' => '16206',
+ 'Mant (40700)' => '40172',
+ 'Manzac-sur-Vern (24110)' => '24251',
+ 'Marans (17230)' => '17218',
+ 'Maransin (33230)' => '33264',
+ 'Marc-la-Tour (19150)' => '19127',
+ 'Marçay (86370)' => '86145',
+ 'Marcellus (47200)' => '47156',
+ 'Marcenais (33620)' => '33266',
+ 'Marcheprime (33380)' => '33555',
+ 'Marcillac (33860)' => '33267',
+ 'Marcillac-la-Croisille (19320)' => '19125',
+ 'Marcillac-la-Croze (19500)' => '19126',
+ 'Marcillac-Lanville (16140)' => '16207',
+ 'Marcillac-Saint-Quentin (24200)' => '24252',
+ 'Marennes (17320)' => '17219',
+ 'Mareuil (16170)' => '16208',
+ 'Mareuil (24340)' => '24253',
+ 'Margaux (33460)' => '33268',
+ 'Margerides (19200)' => '19128',
+ 'Margueron (33220)' => '33269',
+ 'Marignac (17800)' => '17220',
+ 'Marigny (79360)' => '79166',
+ 'Marigny-Brizay (86380)' => '86146',
+ 'Marigny-Chemereau (86370)' => '86147',
+ 'Marillac-le-Franc (16110)' => '16209',
+ 'Marimbault (33430)' => '33270',
+ 'Marions (33690)' => '33271',
+ 'Marmande (47200)' => '47157',
+ 'Marmont-Pachas (47220)' => '47158',
+ 'Marnac (24220)' => '24254',
+ 'Marnay (86160)' => '86148',
+ 'Marnes (79600)' => '79167',
+ 'Marpaps (40330)' => '40173',
+ 'Marquay (24620)' => '24255',
+ 'Marsac (16570)' => '16210',
+ 'Marsac (23210)' => '23124',
+ 'Marsac-sur-l\'Isle (24430)' => '24256',
+ 'Marsais (17700)' => '17221',
+ 'Marsalès (24540)' => '24257',
+ 'Marsaneix (24750)' => '24258',
+ 'Marsas (33620)' => '33272',
+ 'Marsilly (17137)' => '17222',
+ 'Martaizé (86330)' => '86149',
+ 'Marthon (16380)' => '16211',
+ 'Martignas-sur-Jalle (33127)' => '33273',
+ 'Martillac (33650)' => '33274',
+ 'Martres (33760)' => '33275',
+ 'Marval (87440)' => '87092',
+ 'Masbaraud-Mérignat (23400)' => '23126',
+ 'Mascaraàs-Haron (64330)' => '64366',
+ 'Maslacq (64300)' => '64367',
+ 'Masléon (87130)' => '87093',
+ 'Masparraute (64120)' => '64368',
+ 'Maspie-Lalonquère-Juillacq (64350)' => '64369',
+ 'Masquières (47370)' => '47160',
+ 'Massac (17490)' => '17223',
+ 'Massais (79150)' => '79168',
+ 'Masseilles (33690)' => '33276',
+ 'Massels (47140)' => '47161',
+ 'Masseret (19510)' => '19129',
+ 'Massignac (16310)' => '16212',
+ 'Massognes (86170)' => '86150',
+ 'Massoulès (47140)' => '47162',
+ 'Massugas (33790)' => '33277',
+ 'Matha (17160)' => '17224',
+ 'Maucor (64160)' => '64370',
+ 'Maulay (86200)' => '86151',
+ 'Mauléon (79700)' => '79079',
+ 'Mauléon-Licharre (64130)' => '64371',
+ 'Mauprévoir (86460)' => '86152',
+ 'Maure (64460)' => '64372',
+ 'Maurens (24140)' => '24259',
+ 'Mauriac (33540)' => '33278',
+ 'Mauries (40320)' => '40174',
+ 'Maurrin (40270)' => '40175',
+ 'Maussac (19250)' => '19130',
+ 'Mautes (23190)' => '23127',
+ 'Mauvezin-d\'Armagnac (40240)' => '40176',
+ 'Mauvezin-sur-Gupie (47200)' => '47163',
+ 'Mauzac-et-Grand-Castang (24150)' => '24260',
+ 'Mauzé-sur-le-Mignon (79210)' => '79170',
+ 'Mauzé-Thouarsais (79100)' => '79171',
+ 'Mauzens-et-Miremont (24260)' => '24261',
+ 'Mayac (24420)' => '24262',
+ 'Maylis (40250)' => '40177',
+ 'Mazeirat (23150)' => '23128',
+ 'Mazeray (17400)' => '17226',
+ 'Mazères (33210)' => '33279',
+ 'Mazères-Lezons (64110)' => '64373',
+ 'Mazerolles (16310)' => '16213',
+ 'Mazerolles (17800)' => '17227',
+ 'Mazerolles (40090)' => '40178',
+ 'Mazerolles (64230)' => '64374',
+ 'Mazerolles (86320)' => '86153',
+ 'Mazeuil (86110)' => '86154',
+ 'Mazeyrolles (24550)' => '24263',
+ 'Mazières (16270)' => '16214',
+ 'Mazières-en-Gâtine (79310)' => '79172',
+ 'Mazières-Naresse (47210)' => '47164',
+ 'Mazières-sur-Béronne (79500)' => '79173',
+ 'Mazion (33390)' => '33280',
+ 'Méasnes (23360)' => '23130',
+ 'Médillac (16210)' => '16215',
+ 'Médis (17600)' => '17228',
+ 'Mées (40990)' => '40179',
+ 'Méharin (64120)' => '64375',
+ 'Meilhac (87800)' => '87094',
+ 'Meilhan (40400)' => '40180',
+ 'Meilhan-sur-Garonne (47180)' => '47165',
+ 'Meilhards (19510)' => '19131',
+ 'Meillon (64510)' => '64376',
+ 'Melle (79500)' => '79174',
+ 'Melleran (79190)' => '79175',
+ 'Mendionde (64240)' => '64377',
+ 'Menditte (64130)' => '64378',
+ 'Mendive (64220)' => '64379',
+ 'Ménesplet (24700)' => '24264',
+ 'Ménigoute (79340)' => '79176',
+ 'Ménoire (19190)' => '19132',
+ 'Mensignac (24350)' => '24266',
+ 'Méracq (64410)' => '64380',
+ 'Mercoeur (19430)' => '19133',
+ 'Mérignac (16200)' => '16216',
+ 'Mérignac (17210)' => '17229',
+ 'Mérignac (33700)' => '33281',
+ 'Mérignas (33350)' => '33282',
+ 'Mérinchal (23420)' => '23131',
+ 'Méritein (64190)' => '64381',
+ 'Merlines (19340)' => '19134',
+ 'Merpins (16100)' => '16217',
+ 'Meschers-sur-Gironde (17132)' => '17230',
+ 'Mescoules (24240)' => '24267',
+ 'Mesnac (16370)' => '16218',
+ 'Mesplède (64370)' => '64382',
+ 'Messac (17130)' => '17231',
+ 'Messanges (40660)' => '40181',
+ 'Messé (79120)' => '79177',
+ 'Messemé (86200)' => '86156',
+ 'Mesterrieux (33540)' => '33283',
+ 'Mestes (19200)' => '19135',
+ 'Meursac (17120)' => '17232',
+ 'Meux (17500)' => '17233',
+ 'Meuzac (87380)' => '87095',
+ 'Meymac (19250)' => '19136',
+ 'Meyrals (24220)' => '24268',
+ 'Meyrignac-l\'Église (19800)' => '19137',
+ 'Meyssac (19500)' => '19138',
+ 'Mézin (47170)' => '47167',
+ 'Mézos (40170)' => '40182',
+ 'Mialet (24450)' => '24269',
+ 'Mialos (64410)' => '64383',
+ 'Mignaloux-Beauvoir (86550)' => '86157',
+ 'Migné-Auxances (86440)' => '86158',
+ 'Migré (17330)' => '17234',
+ 'Migron (17770)' => '17235',
+ 'Milhac-d\'Auberoche (24330)' => '24270',
+ 'Milhac-de-Nontron (24470)' => '24271',
+ 'Millac (86150)' => '86159',
+ 'Millevaches (19290)' => '19139',
+ 'Mimbaste (40350)' => '40183',
+ 'Mimizan (40200)' => '40184',
+ 'Minzac (24610)' => '24272',
+ 'Mios (33380)' => '33284',
+ 'Miossens-Lanusse (64450)' => '64385',
+ 'Mirambeau (17150)' => '17236',
+ 'Miramont-de-Guyenne (47800)' => '47168',
+ 'Miramont-Sensacq (40320)' => '40185',
+ 'Mirebeau (86110)' => '86160',
+ 'Mirepeix (64800)' => '64386',
+ 'Missé (79100)' => '79178',
+ 'Misson (40290)' => '40186',
+ 'Moëze (17780)' => '17237',
+ 'Moirax (47310)' => '47169',
+ 'Moissannes (87400)' => '87099',
+ 'Molières (24480)' => '24273',
+ 'Moliets-et-Maa (40660)' => '40187',
+ 'Momas (64230)' => '64387',
+ 'Mombrier (33710)' => '33285',
+ 'Momuy (40700)' => '40188',
+ 'Momy (64350)' => '64388',
+ 'Monassut-Audiracq (64160)' => '64389',
+ 'Monbahus (47290)' => '47170',
+ 'Monbalen (47340)' => '47171',
+ 'Monbazillac (24240)' => '24274',
+ 'Moncaup (64350)' => '64390',
+ 'Moncaut (47310)' => '47172',
+ 'Moncayolle-Larrory-Mendibieu (64130)' => '64391',
+ 'Monceaux-sur-Dordogne (19400)' => '19140',
+ 'Moncla (64330)' => '64392',
+ 'Monclar (47380)' => '47173',
+ 'Moncontour (86330)' => '86161',
+ 'Moncoutant (79320)' => '79179',
+ 'Moncrabeau (47600)' => '47174',
+ 'Mondion (86230)' => '86162',
+ 'Monein (64360)' => '64393',
+ 'Monestier (24240)' => '24276',
+ 'Monestier-Merlines (19340)' => '19141',
+ 'Monestier-Port-Dieu (19110)' => '19142',
+ 'Monfaucon (24130)' => '24277',
+ 'Monflanquin (47150)' => '47175',
+ 'Mongaillard (47230)' => '47176',
+ 'Mongauzy (33190)' => '33287',
+ 'Monget (40700)' => '40189',
+ 'Monheurt (47160)' => '47177',
+ 'Monmadalès (24560)' => '24278',
+ 'Monmarvès (24560)' => '24279',
+ 'Monpazier (24540)' => '24280',
+ 'Monpezat (64350)' => '64394',
+ 'Monplaisant (24170)' => '24293',
+ 'Monprimblanc (33410)' => '33288',
+ 'Mons (16140)' => '16221',
+ 'Mons (17160)' => '17239',
+ 'Monsac (24440)' => '24281',
+ 'Monsaguel (24560)' => '24282',
+ 'Monsec (24340)' => '24283',
+ 'Monségur (33580)' => '33289',
+ 'Monségur (40700)' => '40190',
+ 'Monségur (47150)' => '47178',
+ 'Monségur (64460)' => '64395',
+ 'Monsempron-Libos (47500)' => '47179',
+ 'Mont (64300)' => '64396',
+ 'Mont-de-Marsan (40000)' => '40192',
+ 'Mont-Disse (64330)' => '64401',
+ 'Montagnac-d\'Auberoche (24210)' => '24284',
+ 'Montagnac-la-Crempse (24140)' => '24285',
+ 'Montagnac-sur-Auvignon (47600)' => '47180',
+ 'Montagnac-sur-Lède (47150)' => '47181',
+ 'Montagne (33570)' => '33290',
+ 'Montagoudin (33190)' => '33291',
+ 'Montagrier (24350)' => '24286',
+ 'Montagut (64410)' => '64397',
+ 'Montaignac-Saint-Hippolyte (19300)' => '19143',
+ 'Montaigut-le-Blanc (23320)' => '23132',
+ 'Montalembert (79190)' => '79180',
+ 'Montamisé (86360)' => '86163',
+ 'Montaner (64460)' => '64398',
+ 'Montardon (64121)' => '64399',
+ 'Montastruc (47380)' => '47182',
+ 'Montauriol (47330)' => '47183',
+ 'Montaut (24560)' => '24287',
+ 'Montaut (40500)' => '40191',
+ 'Montaut (47210)' => '47184',
+ 'Montaut (64800)' => '64400',
+ 'Montayral (47500)' => '47185',
+ 'Montazeau (24230)' => '24288',
+ 'Montboucher (23400)' => '23133',
+ 'Montboyer (16620)' => '16222',
+ 'Montbron (16220)' => '16223',
+ 'Montcaret (24230)' => '24289',
+ 'Montégut (40190)' => '40193',
+ 'Montemboeuf (16310)' => '16225',
+ 'Montendre (17130)' => '17240',
+ 'Montesquieu (47130)' => '47186',
+ 'Monteton (47120)' => '47187',
+ 'Montferrand-du-Périgord (24440)' => '24290',
+ 'Montfort (64190)' => '64403',
+ 'Montfort-en-Chalosse (40380)' => '40194',
+ 'Montgaillard (40500)' => '40195',
+ 'Montgibaud (19210)' => '19144',
+ 'Montguyon (17270)' => '17241',
+ 'Monthoiron (86210)' => '86164',
+ 'Montignac (24290)' => '24291',
+ 'Montignac (33760)' => '33292',
+ 'Montignac-Charente (16330)' => '16226',
+ 'Montignac-de-Lauzun (47800)' => '47188',
+ 'Montignac-le-Coq (16390)' => '16227',
+ 'Montignac-Toupinerie (47350)' => '47189',
+ 'Montigné (16170)' => '16228',
+ 'Montils (17800)' => '17242',
+ 'Montjean (16240)' => '16229',
+ 'Montlieu-la-Garde (17210)' => '17243',
+ 'Montmérac (16300)' => '16224',
+ 'Montmoreau-Saint-Cybard (16190)' => '16230',
+ 'Montmorillon (86500)' => '86165',
+ 'Montory (64470)' => '64404',
+ 'Montpellier-de-Médillan (17260)' => '17244',
+ 'Montpeyroux (24610)' => '24292',
+ 'Montpezat (47360)' => '47190',
+ 'Montpon-Ménestérol (24700)' => '24294',
+ 'Montpouillan (47200)' => '47191',
+ 'Montravers (79140)' => '79183',
+ 'Montrem (24110)' => '24295',
+ 'Montreuil-Bonnin (86470)' => '86166',
+ 'Montrol-Sénard (87330)' => '87100',
+ 'Montrollet (16420)' => '16231',
+ 'Montroy (17220)' => '17245',
+ 'Monts-sur-Guesnes (86420)' => '86167',
+ 'Montsoué (40500)' => '40196',
+ 'Montussan (33450)' => '33293',
+ 'Monviel (47290)' => '47192',
+ 'Moragne (17430)' => '17246',
+ 'Morcenx (40110)' => '40197',
+ 'Morganx (40700)' => '40198',
+ 'Morizès (33190)' => '33294',
+ 'Morlaàs (64160)' => '64405',
+ 'Morlanne (64370)' => '64406',
+ 'Mornac (16600)' => '16232',
+ 'Mornac-sur-Seudre (17113)' => '17247',
+ 'Mortagne-sur-Gironde (17120)' => '17248',
+ 'Mortemart (87330)' => '87101',
+ 'Mortiers (17500)' => '17249',
+ 'Morton (86120)' => '86169',
+ 'Mortroux (23220)' => '23136',
+ 'Mosnac (16120)' => '16233',
+ 'Mosnac (17240)' => '17250',
+ 'Mougon (79370)' => '79185',
+ 'Mouguerre (64990)' => '64407',
+ 'Mouhous (64330)' => '64408',
+ 'Mouillac (33240)' => '33295',
+ 'Mouleydier (24520)' => '24296',
+ 'Moulidars (16290)' => '16234',
+ 'Mouliets-et-Villemartin (33350)' => '33296',
+ 'Moulin-Neuf (24700)' => '24297',
+ 'Moulinet (47290)' => '47193',
+ 'Moulis-en-Médoc (33480)' => '33297',
+ 'Moulismes (86500)' => '86170',
+ 'Moulon (33420)' => '33298',
+ 'Moumour (64400)' => '64409',
+ 'Mourens (33410)' => '33299',
+ 'Mourenx (64150)' => '64410',
+ 'Mourioux-Vieilleville (23210)' => '23137',
+ 'Mouscardès (40290)' => '40199',
+ 'Moussac (86150)' => '86171',
+ 'Moustey (40410)' => '40200',
+ 'Moustier (47800)' => '47194',
+ 'Moustier-Ventadour (19300)' => '19145',
+ 'Mouterre-Silly (86200)' => '86173',
+ 'Mouterre-sur-Blourde (86430)' => '86172',
+ 'Mouthiers-sur-Boëme (16440)' => '16236',
+ 'Moutier-d\'Ahun (23150)' => '23138',
+ 'Moutier-Malcard (23220)' => '23139',
+ 'Moutier-Rozeille (23200)' => '23140',
+ 'Moutiers-sous-Chantemerle (79320)' => '79188',
+ 'Mouton (16460)' => '16237',
+ 'Moutonneau (16460)' => '16238',
+ 'Mouzon (16310)' => '16239',
+ 'Mugron (40250)' => '40201',
+ 'Muron (17430)' => '17253',
+ 'Musculdy (64130)' => '64411',
+ 'Mussidan (24400)' => '24299',
+ 'Nabas (64190)' => '64412',
+ 'Nabinaud (16390)' => '16240',
+ 'Nabirat (24250)' => '24300',
+ 'Nachamps (17380)' => '17254',
+ 'Nadaillac (24590)' => '24301',
+ 'Nailhac (24390)' => '24302',
+ 'Naillat (23800)' => '23141',
+ 'Naintré (86530)' => '86174',
+ 'Nalliers (86310)' => '86175',
+ 'Nanclars (16230)' => '16241',
+ 'Nancras (17600)' => '17255',
+ 'Nanteuil (79400)' => '79189',
+ 'Nanteuil-Auriac-de-Bourzac (24320)' => '24303',
+ 'Nanteuil-en-Vallée (16700)' => '16242',
+ 'Nantheuil (24800)' => '24304',
+ 'Nanthiat (24800)' => '24305',
+ 'Nantiat (87140)' => '87103',
+ 'Nantillé (17770)' => '17256',
+ 'Narcastet (64510)' => '64413',
+ 'Narp (64190)' => '64414',
+ 'Narrosse (40180)' => '40202',
+ 'Nassiet (40330)' => '40203',
+ 'Nastringues (24230)' => '24306',
+ 'Naujac-sur-Mer (33990)' => '33300',
+ 'Naujan-et-Postiac (33420)' => '33301',
+ 'Naussannes (24440)' => '24307',
+ 'Navailles-Angos (64450)' => '64415',
+ 'Navarrenx (64190)' => '64416',
+ 'Naves (19460)' => '19146',
+ 'Nay (64800)' => '64417',
+ 'Néac (33500)' => '33302',
+ 'Nedde (87120)' => '87104',
+ 'Négrondes (24460)' => '24308',
+ 'Néoux (23200)' => '23142',
+ 'Nérac (47600)' => '47195',
+ 'Nerbis (40250)' => '40204',
+ 'Nercillac (16200)' => '16243',
+ 'Néré (17510)' => '17257',
+ 'Nérigean (33750)' => '33303',
+ 'Nérignac (86150)' => '86176',
+ 'Nersac (16440)' => '16244',
+ 'Nespouls (19600)' => '19147',
+ 'Neuffons (33580)' => '33304',
+ 'Neuillac (17520)' => '17258',
+ 'Neulles (17500)' => '17259',
+ 'Neuvic (19160)' => '19148',
+ 'Neuvic (24190)' => '24309',
+ 'Neuvic-Entier (87130)' => '87105',
+ 'Neuvicq (17270)' => '17260',
+ 'Neuvicq-le-Château (17490)' => '17261',
+ 'Neuville (19380)' => '19149',
+ 'Neuville-de-Poitou (86170)' => '86177',
+ 'Neuvy-Bouin (79130)' => '79190',
+ 'Nexon (87800)' => '87106',
+ 'Nicole (47190)' => '47196',
+ 'Nieuil (16270)' => '16245',
+ 'Nieuil-l\'Espoir (86340)' => '86178',
+ 'Nieul (87510)' => '87107',
+ 'Nieul-le-Virouil (17150)' => '17263',
+ 'Nieul-lès-Saintes (17810)' => '17262',
+ 'Nieul-sur-Mer (17137)' => '17264',
+ 'Nieulle-sur-Seudre (17600)' => '17265',
+ 'Niort (79000)' => '79191',
+ 'Noailhac (19500)' => '19150',
+ 'Noaillac (33190)' => '33306',
+ 'Noaillan (33730)' => '33307',
+ 'Noailles (19600)' => '19151',
+ 'Noguères (64150)' => '64418',
+ 'Nomdieu (47600)' => '47197',
+ 'Nonac (16190)' => '16246',
+ 'Nonards (19120)' => '19152',
+ 'Nonaville (16120)' => '16247',
+ 'Nontron (24300)' => '24311',
+ 'Noth (23300)' => '23143',
+ 'Notre-Dame-de-Sanilhac (24660)' => '24312',
+ 'Nouaillé-Maupertuis (86340)' => '86180',
+ 'Nouhant (23170)' => '23145',
+ 'Nouic (87330)' => '87108',
+ 'Nousse (40380)' => '40205',
+ 'Nousty (64420)' => '64419',
+ 'Nouzerines (23600)' => '23146',
+ 'Nouzerolles (23360)' => '23147',
+ 'Nouziers (23350)' => '23148',
+ 'Nuaillé-d\'Aunis (17540)' => '17267',
+ 'Nuaillé-sur-Boutonne (17470)' => '17268',
+ 'Nueil-les-Aubiers (79250)' => '79195',
+ 'Nueil-sous-Faye (86200)' => '86181',
+ 'Objat (19130)' => '19153',
+ 'Oeyregave (40300)' => '40206',
+ 'Oeyreluy (40180)' => '40207',
+ 'Ogenne-Camptort (64190)' => '64420',
+ 'Ogeu-les-Bains (64680)' => '64421',
+ 'Oiron (79100)' => '79196',
+ 'Oloron-Sainte-Marie (64400)' => '64422',
+ 'Omet (33410)' => '33308',
+ 'Onard (40380)' => '40208',
+ 'Ondres (40440)' => '40209',
+ 'Onesse-Laharie (40110)' => '40210',
+ 'Oraàs (64390)' => '64423',
+ 'Oradour (16140)' => '16248',
+ 'Oradour-Fanais (16500)' => '16249',
+ 'Oradour-Saint-Genest (87210)' => '87109',
+ 'Oradour-sur-Glane (87520)' => '87110',
+ 'Oradour-sur-Vayres (87150)' => '87111',
+ 'Orches (86230)' => '86182',
+ 'Ordiarp (64130)' => '64424',
+ 'Ordonnac (33340)' => '33309',
+ 'Orègue (64120)' => '64425',
+ 'Orgedeuil (16220)' => '16250',
+ 'Orgnac-sur-Vézère (19410)' => '19154',
+ 'Origne (33113)' => '33310',
+ 'Orignolles (17210)' => '17269',
+ 'Orin (64400)' => '64426',
+ 'Oriolles (16480)' => '16251',
+ 'Orion (64390)' => '64427',
+ 'Orist (40300)' => '40211',
+ 'Orival (16210)' => '16252',
+ 'Orliac (24170)' => '24313',
+ 'Orliac-de-Bar (19390)' => '19155',
+ 'Orliaguet (24370)' => '24314',
+ 'Oroux (79390)' => '79197',
+ 'Orriule (64390)' => '64428',
+ 'Orsanco (64120)' => '64429',
+ 'Orthevielle (40300)' => '40212',
+ 'Orthez (64300)' => '64430',
+ 'Orx (40230)' => '40213',
+ 'Os-Marsillon (64150)' => '64431',
+ 'Ossages (40290)' => '40214',
+ 'Ossas-Suhare (64470)' => '64432',
+ 'Osse-en-Aspe (64490)' => '64433',
+ 'Ossenx (64190)' => '64434',
+ 'Osserain-Rivareyte (64390)' => '64435',
+ 'Ossès (64780)' => '64436',
+ 'Ostabat-Asme (64120)' => '64437',
+ 'Ouillon (64160)' => '64438',
+ 'Ousse (64320)' => '64439',
+ 'Ousse-Suzan (40110)' => '40215',
+ 'Ouzilly (86380)' => '86184',
+ 'Oyré (86220)' => '86186',
+ 'Ozenx-Montestrucq (64300)' => '64440',
+ 'Ozillac (17500)' => '17270',
+ 'Ozourt (40380)' => '40216',
+ 'Pageas (87230)' => '87112',
+ 'Pagolle (64120)' => '64441',
+ 'Paillé (17470)' => '17271',
+ 'Paillet (33550)' => '33311',
+ 'Pailloles (47440)' => '47198',
+ 'Paizay-le-Chapt (79170)' => '79198',
+ 'Paizay-le-Sec (86300)' => '86187',
+ 'Paizay-le-Tort (79500)' => '79199',
+ 'Paizay-Naudouin-Embourie (16240)' => '16253',
+ 'Palazinges (19190)' => '19156',
+ 'Palisse (19160)' => '19157',
+ 'Palluaud (16390)' => '16254',
+ 'Pamplie (79220)' => '79200',
+ 'Pamproux (79800)' => '79201',
+ 'Panazol (87350)' => '87114',
+ 'Pandrignes (19150)' => '19158',
+ 'Parbayse (64360)' => '64442',
+ 'Parcoul-Chenaud (24410)' => '24316',
+ 'Pardaillan (47120)' => '47199',
+ 'Pardies (64150)' => '64443',
+ 'Pardies-Piétat (64800)' => '64444',
+ 'Parempuyre (33290)' => '33312',
+ 'Parentis-en-Born (40160)' => '40217',
+ 'Parleboscq (40310)' => '40218',
+ 'Parranquet (47210)' => '47200',
+ 'Parsac-Rimondeix (23140)' => '23149',
+ 'Parthenay (79200)' => '79202',
+ 'Parzac (16450)' => '16255',
+ 'Pas-de-Jeu (79100)' => '79203',
+ 'Passirac (16480)' => '16256',
+ 'Pau (64000)' => '64445',
+ 'Pauillac (33250)' => '33314',
+ 'Paulhiac (47150)' => '47202',
+ 'Paulin (24590)' => '24317',
+ 'Paunat (24510)' => '24318',
+ 'Paussac-et-Saint-Vivien (24310)' => '24319',
+ 'Payré (86700)' => '86188',
+ 'Payros-Cazautets (40320)' => '40219',
+ 'Payroux (86350)' => '86189',
+ 'Pays de Belvès (24170)' => '24035',
+ 'Payzac (24270)' => '24320',
+ 'Pazayac (24120)' => '24321',
+ 'Pécorade (40320)' => '40220',
+ 'Pellegrue (33790)' => '33316',
+ 'Penne-d\'Agenais (47140)' => '47203',
+ 'Pensol (87440)' => '87115',
+ 'Péré (17700)' => '17272',
+ 'Péret-Bel-Air (19300)' => '19159',
+ 'Pérignac (16250)' => '16258',
+ 'Pérignac (17800)' => '17273',
+ 'Périgné (79170)' => '79204',
+ 'Périgny (17180)' => '17274',
+ 'Périgueux (24000)' => '24322',
+ 'Périssac (33240)' => '33317',
+ 'Pérols-sur-Vézère (19170)' => '19160',
+ 'Perpezac-le-Blanc (19310)' => '19161',
+ 'Perpezac-le-Noir (19410)' => '19162',
+ 'Perquie (40190)' => '40221',
+ 'Pers (79190)' => '79205',
+ 'Persac (86320)' => '86190',
+ 'Pessac (33600)' => '33318',
+ 'Pessac-sur-Dordogne (33890)' => '33319',
+ 'Pessines (17810)' => '17275',
+ 'Petit-Bersac (24600)' => '24323',
+ 'Petit-Palais-et-Cornemps (33570)' => '33320',
+ 'Peujard (33240)' => '33321',
+ 'Pey (40300)' => '40222',
+ 'Peyrabout (23000)' => '23150',
+ 'Peyrat-de-Bellac (87300)' => '87116',
+ 'Peyrat-la-Nonière (23130)' => '23151',
+ 'Peyrat-le-Château (87470)' => '87117',
+ 'Peyre (40700)' => '40223',
+ 'Peyrehorade (40300)' => '40224',
+ 'Peyrelevade (19290)' => '19164',
+ 'Peyrelongue-Abos (64350)' => '64446',
+ 'Peyrière (47350)' => '47204',
+ 'Peyrignac (24210)' => '24324',
+ 'Peyrilhac (87510)' => '87118',
+ 'Peyrillac-et-Millac (24370)' => '24325',
+ 'Peyrissac (19260)' => '19165',
+ 'Peyzac-le-Moustier (24620)' => '24326',
+ 'Pezuls (24510)' => '24327',
+ 'Philondenx (40320)' => '40225',
+ 'Piégut-Pluviers (24360)' => '24328',
+ 'Pierre-Buffière (87260)' => '87119',
+ 'Pierrefitte (19450)' => '19166',
+ 'Pierrefitte (23130)' => '23152',
+ 'Pierrefitte (79330)' => '79209',
+ 'Piets-Plasence-Moustrou (64410)' => '64447',
+ 'Pillac (16390)' => '16260',
+ 'Pimbo (40320)' => '40226',
+ 'Pindères (47700)' => '47205',
+ 'Pindray (86500)' => '86191',
+ 'Pinel-Hauterive (47380)' => '47206',
+ 'Pineuilh (33220)' => '33324',
+ 'Pionnat (23140)' => '23154',
+ 'Pioussay (79110)' => '79211',
+ 'Pisany (17600)' => '17278',
+ 'Pissos (40410)' => '40227',
+ 'Plaisance (24560)' => '24168',
+ 'Plaisance (86500)' => '86192',
+ 'Plassac (17240)' => '17279',
+ 'Plassac (33390)' => '33325',
+ 'Plassac-Rouffiac (16250)' => '16263',
+ 'Plassay (17250)' => '17280',
+ 'Plazac (24580)' => '24330',
+ 'Pleine-Selve (33820)' => '33326',
+ 'Pleumartin (86450)' => '86193',
+ 'Pleuville (16490)' => '16264',
+ 'Pliboux (79190)' => '79212',
+ 'Podensac (33720)' => '33327',
+ 'Poey-d\'Oloron (64400)' => '64449',
+ 'Poey-de-Lescar (64230)' => '64448',
+ 'Poitiers (86000)' => '86194',
+ 'Polignac (17210)' => '17281',
+ 'Pomarez (40360)' => '40228',
+ 'Pomerol (33500)' => '33328',
+ 'Pommiers-Moulons (17130)' => '17282',
+ 'Pompaire (79200)' => '79213',
+ 'Pompéjac (33730)' => '33329',
+ 'Pompiey (47230)' => '47207',
+ 'Pompignac (33370)' => '33330',
+ 'Pompogne (47420)' => '47208',
+ 'Pomport (24240)' => '24331',
+ 'Pomps (64370)' => '64450',
+ 'Pondaurat (33190)' => '33331',
+ 'Pons (17800)' => '17283',
+ 'Ponson-Debat-Pouts (64460)' => '64451',
+ 'Ponson-Dessus (64460)' => '64452',
+ 'Pont-du-Casse (47480)' => '47209',
+ 'Pont-l\'Abbé-d\'Arnoult (17250)' => '17284',
+ 'Pontacq (64530)' => '64453',
+ 'Pontarion (23250)' => '23155',
+ 'Pontcharraud (23260)' => '23156',
+ 'Pontenx-les-Forges (40200)' => '40229',
+ 'Ponteyraud (24410)' => '24333',
+ 'Pontiacq-Viellepinte (64460)' => '64454',
+ 'Pontonx-sur-l\'Adour (40465)' => '40230',
+ 'Pontours (24150)' => '24334',
+ 'Porchères (33660)' => '33332',
+ 'Port-d\'Envaux (17350)' => '17285',
+ 'Port-de-Lanne (40300)' => '40231',
+ 'Port-de-Piles (86220)' => '86195',
+ 'Port-des-Barques (17730)' => '17484',
+ 'Port-Sainte-Foy-et-Ponchapt (33220)' => '24335',
+ 'Port-Sainte-Marie (47130)' => '47210',
+ 'Portet (64330)' => '64455',
+ 'Portets (33640)' => '33334',
+ 'Pouançay (86120)' => '86196',
+ 'Pouant (86200)' => '86197',
+ 'Poudenas (47170)' => '47211',
+ 'Poudenx (40700)' => '40232',
+ 'Pouffonds (79500)' => '79214',
+ 'Pougne-Hérisson (79130)' => '79215',
+ 'Pouillac (17210)' => '17287',
+ 'Pouillé (86800)' => '86198',
+ 'Pouillon (40350)' => '40233',
+ 'Pouliacq (64410)' => '64456',
+ 'Poullignac (16190)' => '16267',
+ 'Poursac (16700)' => '16268',
+ 'Poursay-Garnaud (17400)' => '17288',
+ 'Poursiugues-Boucoue (64410)' => '64457',
+ 'Poussanges (23500)' => '23158',
+ 'Poussignac (47700)' => '47212',
+ 'Pouydesseaux (40120)' => '40234',
+ 'Poyanne (40380)' => '40235',
+ 'Poyartin (40380)' => '40236',
+ 'Pradines (19170)' => '19168',
+ 'Prahecq (79230)' => '79216',
+ 'Prailles (79370)' => '79217',
+ 'Pranzac (16110)' => '16269',
+ 'Prats-de-Carlux (24370)' => '24336',
+ 'Prats-du-Périgord (24550)' => '24337',
+ 'Prayssas (47360)' => '47213',
+ 'Préchac (33730)' => '33336',
+ 'Préchacq-Josbaig (64190)' => '64458',
+ 'Préchacq-les-Bains (40465)' => '40237',
+ 'Préchacq-Navarrenx (64190)' => '64459',
+ 'Précilhon (64400)' => '64460',
+ 'Préguillac (17460)' => '17289',
+ 'Preignac (33210)' => '33337',
+ 'Pressac (86460)' => '86200',
+ 'Pressignac (16150)' => '16270',
+ 'Pressignac-Vicq (24150)' => '24338',
+ 'Pressigny (79390)' => '79218',
+ 'Preyssac-d\'Excideuil (24160)' => '24339',
+ 'Priaires (79210)' => '79219',
+ 'Prignac (17160)' => '17290',
+ 'Prignac-en-Médoc (33340)' => '33338',
+ 'Prignac-et-Marcamps (33710)' => '33339',
+ 'Prigonrieux (24130)' => '24340',
+ 'Prin-Deyrançon (79210)' => '79220',
+ 'Prinçay (86420)' => '86201',
+ 'Prissé-la-Charrière (79360)' => '79078',
+ 'Proissans (24200)' => '24341',
+ 'Puch-d\'Agenais (47160)' => '47214',
+ 'Pugnac (33710)' => '33341',
+ 'Pugny (79320)' => '79222',
+ 'Puihardy (79160)' => '79223',
+ 'Puilboreau (17138)' => '17291',
+ 'Puisseguin (33570)' => '33342',
+ 'Pujo-le-Plan (40190)' => '40238',
+ 'Pujols (33350)' => '33344',
+ 'Pujols (47300)' => '47215',
+ 'Pujols-sur-Ciron (33210)' => '33343',
+ 'Puy-d\'Arnac (19120)' => '19169',
+ 'Puy-du-Lac (17380)' => '17292',
+ 'Puy-Malsignat (23130)' => '23159',
+ 'Puybarban (33190)' => '33346',
+ 'Puymiclan (47350)' => '47216',
+ 'Puymirol (47270)' => '47217',
+ 'Puymoyen (16400)' => '16271',
+ 'Puynormand (33660)' => '33347',
+ 'Puyol-Cazalet (40320)' => '40239',
+ 'Puyoô (64270)' => '64461',
+ 'Puyravault (17700)' => '17293',
+ 'Puyréaux (16230)' => '16272',
+ 'Puyrenier (24340)' => '24344',
+ 'Puyrolland (17380)' => '17294',
+ 'Puysserampion (47800)' => '47218',
+ 'Queaux (86150)' => '86203',
+ 'Queyrac (33340)' => '33348',
+ 'Queyssac (24140)' => '24345',
+ 'Queyssac-les-Vignes (19120)' => '19170',
+ 'Quinçay (86190)' => '86204',
+ 'Quinsac (24530)' => '24346',
+ 'Quinsac (33360)' => '33349',
+ 'Raix (16240)' => '16273',
+ 'Ramous (64270)' => '64462',
+ 'Rampieux (24440)' => '24347',
+ 'Rancogne (16110)' => '16274',
+ 'Rancon (87290)' => '87121',
+ 'Ranton (86200)' => '86205',
+ 'Ranville-Breuillaud (16140)' => '16275',
+ 'Raslay (86120)' => '86206',
+ 'Rauzan (33420)' => '33350',
+ 'Rayet (47210)' => '47219',
+ 'Razac-d\'Eymet (24500)' => '24348',
+ 'Razac-de-Saussignac (24240)' => '24349',
+ 'Razac-sur-l\'Isle (24430)' => '24350',
+ 'Razès (87640)' => '87122',
+ 'Razimet (47160)' => '47220',
+ 'Réaup-Lisse (47170)' => '47221',
+ 'Réaux sur Trèfle (17500)' => '17295',
+ 'Rébénacq (64260)' => '64463',
+ 'Reffannes (79420)' => '79225',
+ 'Reignac (16360)' => '16276',
+ 'Reignac (33860)' => '33351',
+ 'Rempnat (87120)' => '87123',
+ 'Renung (40270)' => '40240',
+ 'Réparsac (16200)' => '16277',
+ 'Rétaud (17460)' => '17296',
+ 'Reterre (23110)' => '23160',
+ 'Retjons (40120)' => '40164',
+ 'Reygade (19430)' => '19171',
+ 'Ribagnac (24240)' => '24351',
+ 'Ribarrouy (64330)' => '64464',
+ 'Ribérac (24600)' => '24352',
+ 'Rilhac-Lastours (87800)' => '87124',
+ 'Rilhac-Rancon (87570)' => '87125',
+ 'Rilhac-Treignac (19260)' => '19172',
+ 'Rilhac-Xaintrie (19220)' => '19173',
+ 'Rimbez-et-Baudiets (40310)' => '40242',
+ 'Rimons (33580)' => '33353',
+ 'Riocaud (33220)' => '33354',
+ 'Rion-des-Landes (40370)' => '40243',
+ 'Rions (33410)' => '33355',
+ 'Rioux (17460)' => '17298',
+ 'Rioux-Martin (16210)' => '16279',
+ 'Riupeyrous (64160)' => '64465',
+ 'Rivedoux-Plage (17940)' => '17297',
+ 'Rivehaute (64190)' => '64466',
+ 'Rives (47210)' => '47223',
+ 'Rivière-Saas-et-Gourby (40180)' => '40244',
+ 'Rivières (16110)' => '16280',
+ 'Roaillan (33210)' => '33357',
+ 'Roche-le-Peyroux (19160)' => '19175',
+ 'Rochechouart (87600)' => '87126',
+ 'Rochefort (17300)' => '17299',
+ 'Roches (23270)' => '23162',
+ 'Roches-Prémarie-Andillé (86340)' => '86209',
+ 'Roiffé (86120)' => '86210',
+ 'Rom (79120)' => '79230',
+ 'Romagne (33760)' => '33358',
+ 'Romagne (86700)' => '86211',
+ 'Romans (79260)' => '79231',
+ 'Romazières (17510)' => '17301',
+ 'Romegoux (17250)' => '17302',
+ 'Romestaing (47250)' => '47224',
+ 'Ronsenac (16320)' => '16283',
+ 'Rontignon (64110)' => '64467',
+ 'Roquebrune (33580)' => '33359',
+ 'Roquefort (40120)' => '40245',
+ 'Roquefort (47310)' => '47225',
+ 'Roquiague (64130)' => '64468',
+ 'Rosiers-d\'Égletons (19300)' => '19176',
+ 'Rosiers-de-Juillac (19350)' => '19177',
+ 'Rouffiac (16210)' => '16284',
+ 'Rouffiac (17800)' => '17304',
+ 'Rouffignac (17130)' => '17305',
+ 'Rouffignac-de-Sigoulès (24240)' => '24357',
+ 'Rouffignac-Saint-Cernin-de-Reilhac (24580)' => '24356',
+ 'Rougnac (16320)' => '16285',
+ 'Rougnat (23700)' => '23164',
+ 'Rouillac (16170)' => '16286',
+ 'Rouillé (86480)' => '86213',
+ 'Roullet-Saint-Estèphe (16440)' => '16287',
+ 'Roumagne (47800)' => '47226',
+ 'Roumazières-Loubert (16270)' => '16192',
+ 'Roussac (87140)' => '87128',
+ 'Roussines (16310)' => '16289',
+ 'Rouzède (16220)' => '16290',
+ 'Royan (17200)' => '17306',
+ 'Royère-de-Vassivière (23460)' => '23165',
+ 'Royères (87400)' => '87129',
+ 'Roziers-Saint-Georges (87130)' => '87130',
+ 'Ruch (33350)' => '33361',
+ 'Rudeau-Ladosse (24340)' => '24221',
+ 'Ruelle-sur-Touvre (16600)' => '16291',
+ 'Ruffec (16700)' => '16292',
+ 'Ruffiac (47700)' => '47227',
+ 'Sablonceaux (17600)' => '17307',
+ 'Sablons (33910)' => '33362',
+ 'Sabres (40630)' => '40246',
+ 'Sadillac (24500)' => '24359',
+ 'Sadirac (33670)' => '33363',
+ 'Sadroc (19270)' => '19178',
+ 'Sagelat (24170)' => '24360',
+ 'Sagnat (23800)' => '23166',
+ 'Saillac (19500)' => '19179',
+ 'Saillans (33141)' => '33364',
+ 'Saillat-sur-Vienne (87720)' => '87131',
+ 'Saint Aulaye-Puymangou (24410)' => '24376',
+ 'Saint Maurice Étusson (79150)' => '79280',
+ 'Saint-Abit (64800)' => '64469',
+ 'Saint-Adjutory (16310)' => '16293',
+ 'Saint-Agnant (17620)' => '17308',
+ 'Saint-Agnant-de-Versillat (23300)' => '23177',
+ 'Saint-Agnant-près-Crocq (23260)' => '23178',
+ 'Saint-Agne (24520)' => '24361',
+ 'Saint-Agnet (40800)' => '40247',
+ 'Saint-Aignan (33126)' => '33365',
+ 'Saint-Aigulin (17360)' => '17309',
+ 'Saint-Alpinien (23200)' => '23179',
+ 'Saint-Amand (23200)' => '23180',
+ 'Saint-Amand-de-Coly (24290)' => '24364',
+ 'Saint-Amand-de-Vergt (24380)' => '24365',
+ 'Saint-Amand-Jartoudeix (23400)' => '23181',
+ 'Saint-Amand-le-Petit (87120)' => '87132',
+ 'Saint-Amand-Magnazeix (87290)' => '87133',
+ 'Saint-Amand-sur-Sèvre (79700)' => '79235',
+ 'Saint-Amant-de-Boixe (16330)' => '16295',
+ 'Saint-Amant-de-Bonnieure (16230)' => '16296',
+ 'Saint-Amant-de-Montmoreau (16190)' => '16294',
+ 'Saint-Amant-de-Nouère (16170)' => '16298',
+ 'Saint-André-d\'Allas (24200)' => '24366',
+ 'Saint-André-de-Cubzac (33240)' => '33366',
+ 'Saint-André-de-Double (24190)' => '24367',
+ 'Saint-André-de-Lidon (17260)' => '17310',
+ 'Saint-André-de-Seignanx (40390)' => '40248',
+ 'Saint-André-du-Bois (33490)' => '33367',
+ 'Saint-André-et-Appelles (33220)' => '33369',
+ 'Saint-André-sur-Sèvre (79380)' => '79236',
+ 'Saint-Androny (33390)' => '33370',
+ 'Saint-Angeau (16230)' => '16300',
+ 'Saint-Angel (19200)' => '19180',
+ 'Saint-Antoine-Cumond (24410)' => '24368',
+ 'Saint-Antoine-d\'Auberoche (24330)' => '24369',
+ 'Saint-Antoine-de-Breuilh (24230)' => '24370',
+ 'Saint-Antoine-de-Ficalba (47340)' => '47228',
+ 'Saint-Antoine-du-Queyret (33790)' => '33372',
+ 'Saint-Antoine-sur-l\'Isle (33660)' => '33373',
+ 'Saint-Aquilin (24110)' => '24371',
+ 'Saint-Armou (64160)' => '64470',
+ 'Saint-Astier (24110)' => '24372',
+ 'Saint-Astier (47120)' => '47229',
+ 'Saint-Aubin (40250)' => '40249',
+ 'Saint-Aubin (47150)' => '47230',
+ 'Saint-Aubin-de-Blaye (33820)' => '33374',
+ 'Saint-Aubin-de-Branne (33420)' => '33375',
+ 'Saint-Aubin-de-Cadelech (24500)' => '24373',
+ 'Saint-Aubin-de-Lanquais (24560)' => '24374',
+ 'Saint-Aubin-de-Médoc (33160)' => '33376',
+ 'Saint-Aubin-de-Nabirat (24250)' => '24375',
+ 'Saint-Aubin-du-Plain (79300)' => '79238',
+ 'Saint-Aubin-le-Cloud (79450)' => '79239',
+ 'Saint-Augustin (17570)' => '17311',
+ 'Saint-Augustin (19390)' => '19181',
+ 'Saint-Aulaire (19130)' => '19182',
+ 'Saint-Aulais-la-Chapelle (16300)' => '16301',
+ 'Saint-Auvent (87310)' => '87135',
+ 'Saint-Avit (16210)' => '16302',
+ 'Saint-Avit (40090)' => '40250',
+ 'Saint-Avit (47350)' => '47231',
+ 'Saint-Avit-de-Soulège (33220)' => '33377',
+ 'Saint-Avit-de-Tardes (23200)' => '23182',
+ 'Saint-Avit-de-Vialard (24260)' => '24377',
+ 'Saint-Avit-le-Pauvre (23480)' => '23183',
+ 'Saint-Avit-Rivière (24540)' => '24378',
+ 'Saint-Avit-Saint-Nazaire (33220)' => '33378',
+ 'Saint-Avit-Sénieur (24440)' => '24379',
+ 'Saint-Barbant (87330)' => '87136',
+ 'Saint-Bard (23260)' => '23184',
+ 'Saint-Barthélemy (40390)' => '40251',
+ 'Saint-Barthélemy-d\'Agenais (47350)' => '47232',
+ 'Saint-Barthélemy-de-Bellegarde (24700)' => '24380',
+ 'Saint-Barthélemy-de-Bussière (24360)' => '24381',
+ 'Saint-Bazile (87150)' => '87137',
+ 'Saint-Bazile-de-la-Roche (19320)' => '19183',
+ 'Saint-Bazile-de-Meyssac (19500)' => '19184',
+ 'Saint-Benoît (86280)' => '86214',
+ 'Saint-Boès (64300)' => '64471',
+ 'Saint-Bonnet (16300)' => '16303',
+ 'Saint-Bonnet-Avalouze (19150)' => '19185',
+ 'Saint-Bonnet-Briance (87260)' => '87138',
+ 'Saint-Bonnet-de-Bellac (87300)' => '87139',
+ 'Saint-Bonnet-Elvert (19380)' => '19186',
+ 'Saint-Bonnet-l\'Enfantier (19410)' => '19188',
+ 'Saint-Bonnet-la-Rivière (19130)' => '19187',
+ 'Saint-Bonnet-les-Tours-de-Merle (19430)' => '19189',
+ 'Saint-Bonnet-près-Bort (19200)' => '19190',
+ 'Saint-Bonnet-sur-Gironde (17150)' => '17312',
+ 'Saint-Brice (16100)' => '16304',
+ 'Saint-Brice (33540)' => '33379',
+ 'Saint-Brice-sur-Vienne (87200)' => '87140',
+ 'Saint-Bris-des-Bois (17770)' => '17313',
+ 'Saint-Caprais-de-Blaye (33820)' => '33380',
+ 'Saint-Caprais-de-Bordeaux (33880)' => '33381',
+ 'Saint-Caprais-de-Lerm (47270)' => '47234',
+ 'Saint-Capraise-d\'Eymet (24500)' => '24383',
+ 'Saint-Capraise-de-Lalinde (24150)' => '24382',
+ 'Saint-Cassien (24540)' => '24384',
+ 'Saint-Castin (64160)' => '64472',
+ 'Saint-Cernin-de-l\'Herm (24550)' => '24386',
+ 'Saint-Cernin-de-Labarde (24560)' => '24385',
+ 'Saint-Cernin-de-Larche (19600)' => '19191',
+ 'Saint-Césaire (17770)' => '17314',
+ 'Saint-Chabrais (23130)' => '23185',
+ 'Saint-Chamant (19380)' => '19192',
+ 'Saint-Chamassy (24260)' => '24388',
+ 'Saint-Christoly-de-Blaye (33920)' => '33382',
+ 'Saint-Christoly-Médoc (33340)' => '33383',
+ 'Saint-Christophe (16420)' => '16306',
+ 'Saint-Christophe (17220)' => '17315',
+ 'Saint-Christophe (23000)' => '23186',
+ 'Saint-Christophe (86230)' => '86217',
+ 'Saint-Christophe-de-Double (33230)' => '33385',
+ 'Saint-Christophe-des-Bardes (33330)' => '33384',
+ 'Saint-Christophe-sur-Roc (79220)' => '79241',
+ 'Saint-Cibard (33570)' => '33386',
+ 'Saint-Ciers-Champagne (17520)' => '17316',
+ 'Saint-Ciers-d\'Abzac (33910)' => '33387',
+ 'Saint-Ciers-de-Canesse (33710)' => '33388',
+ 'Saint-Ciers-du-Taillon (17240)' => '17317',
+ 'Saint-Ciers-sur-Bonnieure (16230)' => '16307',
+ 'Saint-Ciers-sur-Gironde (33820)' => '33389',
+ 'Saint-Cirgues-la-Loutre (19220)' => '19193',
+ 'Saint-Cirq (24260)' => '24389',
+ 'Saint-Clair (86330)' => '86218',
+ 'Saint-Claud (16450)' => '16308',
+ 'Saint-Clément (19700)' => '19194',
+ 'Saint-Clément-des-Baleines (17590)' => '17318',
+ 'Saint-Colomb-de-Lauzun (47410)' => '47235',
+ 'Saint-Côme (33430)' => '33391',
+ 'Saint-Coutant (16350)' => '16310',
+ 'Saint-Coutant (79120)' => '79243',
+ 'Saint-Coutant-le-Grand (17430)' => '17320',
+ 'Saint-Crépin (17380)' => '17321',
+ 'Saint-Crépin-d\'Auberoche (24330)' => '24390',
+ 'Saint-Crépin-de-Richemont (24310)' => '24391',
+ 'Saint-Crépin-et-Carlucet (24590)' => '24392',
+ 'Saint-Cricq-Chalosse (40700)' => '40253',
+ 'Saint-Cricq-du-Gave (40300)' => '40254',
+ 'Saint-Cricq-Villeneuve (40190)' => '40255',
+ 'Saint-Cybardeaux (16170)' => '16312',
+ 'Saint-Cybranet (24250)' => '24395',
+ 'Saint-Cyprien (19130)' => '19195',
+ 'Saint-Cyprien (24220)' => '24396',
+ 'Saint-Cyr (86130)' => '86219',
+ 'Saint-Cyr (87310)' => '87141',
+ 'Saint-Cyr-du-Doret (17170)' => '17322',
+ 'Saint-Cyr-la-Lande (79100)' => '79244',
+ 'Saint-Cyr-la-Roche (19130)' => '19196',
+ 'Saint-Cyr-les-Champagnes (24270)' => '24397',
+ 'Saint-Denis-d\'Oléron (17650)' => '17323',
+ 'Saint-Denis-de-Pile (33910)' => '33393',
+ 'Saint-Denis-des-Murs (87400)' => '87142',
+ 'Saint-Dizant-du-Bois (17150)' => '17324',
+ 'Saint-Dizant-du-Gua (17240)' => '17325',
+ 'Saint-Dizier-la-Tour (23130)' => '23187',
+ 'Saint-Dizier-les-Domaines (23270)' => '23188',
+ 'Saint-Dizier-Leyrenne (23400)' => '23189',
+ 'Saint-Domet (23190)' => '23190',
+ 'Saint-Dos (64270)' => '64474',
+ 'Saint-Éloi (23000)' => '23191',
+ 'Saint-Éloy-les-Tuileries (19210)' => '19198',
+ 'Saint-Émilion (33330)' => '33394',
+ 'Saint-Esteben (64640)' => '64476',
+ 'Saint-Estèphe (24360)' => '24398',
+ 'Saint-Estèphe (33180)' => '33395',
+ 'Saint-Étienne-aux-Clos (19200)' => '19199',
+ 'Saint-Étienne-d\'Orthe (40300)' => '40256',
+ 'Saint-Étienne-de-Baïgorry (64430)' => '64477',
+ 'Saint-Étienne-de-Fougères (47380)' => '47239',
+ 'Saint-Étienne-de-Fursac (23290)' => '23192',
+ 'Saint-Étienne-de-Lisse (33330)' => '33396',
+ 'Saint-Étienne-de-Puycorbier (24400)' => '24399',
+ 'Saint-Étienne-de-Villeréal (47210)' => '47240',
+ 'Saint-Étienne-la-Cigogne (79360)' => '79247',
+ 'Saint-Étienne-la-Geneste (19160)' => '19200',
+ 'Saint-Eugène (17520)' => '17326',
+ 'Saint-Eutrope (16190)' => '16314',
+ 'Saint-Eutrope-de-Born (47210)' => '47241',
+ 'Saint-Exupéry (33190)' => '33398',
+ 'Saint-Exupéry-les-Roches (19200)' => '19201',
+ 'Saint-Faust (64110)' => '64478',
+ 'Saint-Félix (16480)' => '16315',
+ 'Saint-Félix (17330)' => '17327',
+ 'Saint-Félix-de-Bourdeilles (24340)' => '24403',
+ 'Saint-Félix-de-Foncaude (33540)' => '33399',
+ 'Saint-Félix-de-Reillac-et-Mortemart (24260)' => '24404',
+ 'Saint-Félix-de-Villadeix (24510)' => '24405',
+ 'Saint-Ferme (33580)' => '33400',
+ 'Saint-Fiel (23000)' => '23195',
+ 'Saint-Fort-sur-Gironde (17240)' => '17328',
+ 'Saint-Fort-sur-le-Né (16130)' => '16316',
+ 'Saint-Fraigne (16140)' => '16317',
+ 'Saint-Fréjoux (19200)' => '19204',
+ 'Saint-Frion (23500)' => '23196',
+ 'Saint-Front (16460)' => '16318',
+ 'Saint-Front-d\'Alemps (24460)' => '24408',
+ 'Saint-Front-de-Pradoux (24400)' => '24409',
+ 'Saint-Front-la-Rivière (24300)' => '24410',
+ 'Saint-Front-sur-Lémance (47500)' => '47242',
+ 'Saint-Front-sur-Nizonne (24300)' => '24411',
+ 'Saint-Froult (17780)' => '17329',
+ 'Saint-Gaudent (86400)' => '86220',
+ 'Saint-Gein (40190)' => '40259',
+ 'Saint-Gelais (79410)' => '79249',
+ 'Saint-Génard (79500)' => '79251',
+ 'Saint-Gence (87510)' => '87143',
+ 'Saint-Généroux (79600)' => '79252',
+ 'Saint-Genès-de-Blaye (33390)' => '33405',
+ 'Saint-Genès-de-Castillon (33350)' => '33406',
+ 'Saint-Genès-de-Fronsac (33240)' => '33407',
+ 'Saint-Genès-de-Lombaud (33670)' => '33408',
+ 'Saint-Genest-d\'Ambière (86140)' => '86221',
+ 'Saint-Genest-sur-Roselle (87260)' => '87144',
+ 'Saint-Geniès (24590)' => '24412',
+ 'Saint-Geniez-ô-Merle (19220)' => '19205',
+ 'Saint-Genis-d\'Hiersac (16570)' => '16320',
+ 'Saint-Genis-de-Saintonge (17240)' => '17331',
+ 'Saint-Genis-du-Bois (33760)' => '33409',
+ 'Saint-Georges (16700)' => '16321',
+ 'Saint-Georges (47370)' => '47328',
+ 'Saint-Georges-Antignac (17240)' => '17332',
+ 'Saint-Georges-Blancaneix (24130)' => '24413',
+ 'Saint-Georges-d\'Oléron (17190)' => '17337',
+ 'Saint-Georges-de-Didonne (17110)' => '17333',
+ 'Saint-Georges-de-Longuepierre (17470)' => '17334',
+ 'Saint-Georges-de-Montclard (24140)' => '24414',
+ 'Saint-Georges-de-Noisné (79400)' => '79253',
+ 'Saint-Georges-de-Rex (79210)' => '79254',
+ 'Saint-Georges-des-Agoûts (17150)' => '17335',
+ 'Saint-Georges-des-Coteaux (17810)' => '17336',
+ 'Saint-Georges-du-Bois (17700)' => '17338',
+ 'Saint-Georges-la-Pouge (23250)' => '23197',
+ 'Saint-Georges-lès-Baillargeaux (86130)' => '86222',
+ 'Saint-Georges-les-Landes (87160)' => '87145',
+ 'Saint-Georges-Nigremont (23500)' => '23198',
+ 'Saint-Geours-d\'Auribat (40380)' => '40260',
+ 'Saint-Geours-de-Maremne (40230)' => '40261',
+ 'Saint-Géraud (47120)' => '47245',
+ 'Saint-Géraud-de-Corps (24700)' => '24415',
+ 'Saint-Germain (86310)' => '86223',
+ 'Saint-Germain-Beaupré (23160)' => '23199',
+ 'Saint-Germain-d\'Esteuil (33340)' => '33412',
+ 'Saint-Germain-de-Belvès (24170)' => '24416',
+ 'Saint-Germain-de-Grave (33490)' => '33411',
+ 'Saint-Germain-de-la-Rivière (33240)' => '33414',
+ 'Saint-Germain-de-Longue-Chaume (79200)' => '79255',
+ 'Saint-Germain-de-Lusignan (17500)' => '17339',
+ 'Saint-Germain-de-Marencennes (17700)' => '17340',
+ 'Saint-Germain-de-Montbron (16380)' => '16323',
+ 'Saint-Germain-de-Vibrac (17500)' => '17341',
+ 'Saint-Germain-des-Prés (24160)' => '24417',
+ 'Saint-Germain-du-Puch (33750)' => '33413',
+ 'Saint-Germain-du-Salembre (24190)' => '24418',
+ 'Saint-Germain-du-Seudre (17240)' => '17342',
+ 'Saint-Germain-et-Mons (24520)' => '24419',
+ 'Saint-Germain-Lavolps (19290)' => '19206',
+ 'Saint-Germain-les-Belles (87380)' => '87146',
+ 'Saint-Germain-les-Vergnes (19330)' => '19207',
+ 'Saint-Germier (79340)' => '79256',
+ 'Saint-Gervais (33240)' => '33415',
+ 'Saint-Gervais-les-Trois-Clochers (86230)' => '86224',
+ 'Saint-Géry (24400)' => '24420',
+ 'Saint-Geyrac (24330)' => '24421',
+ 'Saint-Gilles-les-Forêts (87130)' => '87147',
+ 'Saint-Girons-d\'Aiguevives (33920)' => '33416',
+ 'Saint-Girons-en-Béarn (64300)' => '64479',
+ 'Saint-Gladie-Arrive-Munein (64390)' => '64480',
+ 'Saint-Goin (64400)' => '64481',
+ 'Saint-Gor (40120)' => '40262',
+ 'Saint-Gourson (16700)' => '16325',
+ 'Saint-Goussaud (23430)' => '23200',
+ 'Saint-Grégoire-d\'Ardennes (17240)' => '17343',
+ 'Saint-Groux (16230)' => '16326',
+ 'Saint-Hilaire-Bonneval (87260)' => '87148',
+ 'Saint-Hilaire-d\'Estissac (24140)' => '24422',
+ 'Saint-Hilaire-de-la-Noaille (33190)' => '33418',
+ 'Saint-Hilaire-de-Lusignan (47450)' => '47246',
+ 'Saint-Hilaire-de-Villefranche (17770)' => '17344',
+ 'Saint-Hilaire-du-Bois (17500)' => '17345',
+ 'Saint-Hilaire-du-Bois (33540)' => '33419',
+ 'Saint-Hilaire-Foissac (19550)' => '19208',
+ 'Saint-Hilaire-la-Palud (79210)' => '79257',
+ 'Saint-Hilaire-la-Plaine (23150)' => '23201',
+ 'Saint-Hilaire-la-Treille (87190)' => '87149',
+ 'Saint-Hilaire-le-Château (23250)' => '23202',
+ 'Saint-Hilaire-les-Courbes (19170)' => '19209',
+ 'Saint-Hilaire-les-Places (87800)' => '87150',
+ 'Saint-Hilaire-Luc (19160)' => '19210',
+ 'Saint-Hilaire-Peyroux (19560)' => '19211',
+ 'Saint-Hilaire-Taurieux (19400)' => '19212',
+ 'Saint-Hippolyte (17430)' => '17346',
+ 'Saint-Hippolyte (33330)' => '33420',
+ 'Saint-Jacques-de-Thouars (79100)' => '79258',
+ 'Saint-Jal (19700)' => '19213',
+ 'Saint-Jammes (64160)' => '64482',
+ 'Saint-Jean-d\'Angély (17400)' => '17347',
+ 'Saint-Jean-d\'Angle (17620)' => '17348',
+ 'Saint-Jean-d\'Ataux (24190)' => '24424',
+ 'Saint-Jean-d\'Estissac (24140)' => '24426',
+ 'Saint-Jean-d\'Eyraud (24140)' => '24427',
+ 'Saint-Jean-d\'Illac (33127)' => '33422',
+ 'Saint-Jean-de-Blaignac (33420)' => '33421',
+ 'Saint-Jean-de-Côle (24800)' => '24425',
+ 'Saint-Jean-de-Duras (47120)' => '47247',
+ 'Saint-Jean-de-Lier (40380)' => '40263',
+ 'Saint-Jean-de-Liversay (17170)' => '17349',
+ 'Saint-Jean-de-Luz (64500)' => '64483',
+ 'Saint-Jean-de-Marsacq (40230)' => '40264',
+ 'Saint-Jean-de-Sauves (86330)' => '86225',
+ 'Saint-Jean-de-Thouars (79100)' => '79259',
+ 'Saint-Jean-de-Thurac (47270)' => '47248',
+ 'Saint-Jean-le-Vieux (64220)' => '64484',
+ 'Saint-Jean-Ligoure (87260)' => '87151',
+ 'Saint-Jean-Pied-de-Port (64220)' => '64485',
+ 'Saint-Jean-Poudge (64330)' => '64486',
+ 'Saint-Jory-de-Chalais (24800)' => '24428',
+ 'Saint-Jory-las-Bloux (24160)' => '24429',
+ 'Saint-Jouin-de-Marnes (79600)' => '79260',
+ 'Saint-Jouin-de-Milly (79380)' => '79261',
+ 'Saint-Jouvent (87510)' => '87152',
+ 'Saint-Julien-aux-Bois (19220)' => '19214',
+ 'Saint-Julien-Beychevelle (33250)' => '33423',
+ 'Saint-Julien-d\'Armagnac (40240)' => '40265',
+ 'Saint-Julien-d\'Eymet (24500)' => '24433',
+ 'Saint-Julien-de-Crempse (24140)' => '24431',
+ 'Saint-Julien-de-l\'Escap (17400)' => '17350',
+ 'Saint-Julien-de-Lampon (24370)' => '24432',
+ 'Saint-Julien-en-Born (40170)' => '40266',
+ 'Saint-Julien-l\'Ars (86800)' => '86226',
+ 'Saint-Julien-la-Genête (23110)' => '23203',
+ 'Saint-Julien-le-Châtel (23130)' => '23204',
+ 'Saint-Julien-le-Pèlerin (19430)' => '19215',
+ 'Saint-Julien-le-Petit (87460)' => '87153',
+ 'Saint-Julien-le-Vendômois (19210)' => '19216',
+ 'Saint-Julien-Maumont (19500)' => '19217',
+ 'Saint-Julien-près-Bort (19110)' => '19218',
+ 'Saint-Junien (87200)' => '87154',
+ 'Saint-Junien-la-Bregère (23400)' => '23205',
+ 'Saint-Junien-les-Combes (87300)' => '87155',
+ 'Saint-Just (24320)' => '24434',
+ 'Saint-Just-Ibarre (64120)' => '64487',
+ 'Saint-Just-le-Martel (87590)' => '87156',
+ 'Saint-Just-Luzac (17320)' => '17351',
+ 'Saint-Justin (40240)' => '40267',
+ 'Saint-Laon (86200)' => '86227',
+ 'Saint-Laurent (23000)' => '23206',
+ 'Saint-Laurent (47130)' => '47249',
+ 'Saint-Laurent-Bretagne (64160)' => '64488',
+ 'Saint-Laurent-d\'Arce (33240)' => '33425',
+ 'Saint-Laurent-de-Belzagot (16190)' => '16328',
+ 'Saint-Laurent-de-Céris (16450)' => '16329',
+ 'Saint-Laurent-de-Cognac (16100)' => '16330',
+ 'Saint-Laurent-de-Gosse (40390)' => '40268',
+ 'Saint-Laurent-de-Jourdes (86410)' => '86228',
+ 'Saint-Laurent-de-la-Barrière (17380)' => '17352',
+ 'Saint-Laurent-de-la-Prée (17450)' => '17353',
+ 'Saint-Laurent-des-Combes (16480)' => '16331',
+ 'Saint-Laurent-des-Combes (33330)' => '33426',
+ 'Saint-Laurent-des-Hommes (24400)' => '24436',
+ 'Saint-Laurent-des-Vignes (24100)' => '24437',
+ 'Saint-Laurent-du-Bois (33540)' => '33427',
+ 'Saint-Laurent-du-Plan (33190)' => '33428',
+ 'Saint-Laurent-la-Vallée (24170)' => '24438',
+ 'Saint-Laurent-les-Églises (87240)' => '87157',
+ 'Saint-Laurent-Médoc (33112)' => '33424',
+ 'Saint-Laurent-sur-Gorre (87310)' => '87158',
+ 'Saint-Laurs (79160)' => '79263',
+ 'Saint-Léger (16250)' => '16332',
+ 'Saint-Léger (17800)' => '17354',
+ 'Saint-Léger (47160)' => '47250',
+ 'Saint-Léger-Bridereix (23300)' => '23207',
+ 'Saint-Léger-de-Balson (33113)' => '33429',
+ 'Saint-Léger-de-la-Martinière (79500)' => '79264',
+ 'Saint-Léger-de-Montbrillais (86120)' => '86229',
+ 'Saint-Léger-de-Montbrun (79100)' => '79265',
+ 'Saint-Léger-la-Montagne (87340)' => '87159',
+ 'Saint-Léger-le-Guérétois (23000)' => '23208',
+ 'Saint-Léger-Magnazeix (87190)' => '87160',
+ 'Saint-Léomer (86290)' => '86230',
+ 'Saint-Léon (33670)' => '33431',
+ 'Saint-Léon (47160)' => '47251',
+ 'Saint-Léon-d\'Issigeac (24560)' => '24441',
+ 'Saint-Léon-sur-l\'Isle (24110)' => '24442',
+ 'Saint-Léon-sur-Vézère (24290)' => '24443',
+ 'Saint-Léonard-de-Noblat (87400)' => '87161',
+ 'Saint-Lin (79420)' => '79267',
+ 'Saint-Lon-les-Mines (40300)' => '40269',
+ 'Saint-Loubert (33210)' => '33432',
+ 'Saint-Loubès (33450)' => '33433',
+ 'Saint-Loubouer (40320)' => '40270',
+ 'Saint-Louis-de-Montferrand (33440)' => '33434',
+ 'Saint-Louis-en-l\'Isle (24400)' => '24444',
+ 'Saint-Loup (17380)' => '17356',
+ 'Saint-Loup (23130)' => '23209',
+ 'Saint-Loup-Lamairé (79600)' => '79268',
+ 'Saint-Macaire (33490)' => '33435',
+ 'Saint-Macoux (86400)' => '86231',
+ 'Saint-Magne (33125)' => '33436',
+ 'Saint-Magne-de-Castillon (33350)' => '33437',
+ 'Saint-Maigrin (17520)' => '17357',
+ 'Saint-Maime-de-Péreyrol (24380)' => '24459',
+ 'Saint-Maixant (23200)' => '23210',
+ 'Saint-Maixant (33490)' => '33438',
+ 'Saint-Maixent-de-Beugné (79160)' => '79269',
+ 'Saint-Maixent-l\'École (79400)' => '79270',
+ 'Saint-Mandé-sur-Brédoire (17470)' => '17358',
+ 'Saint-Marc-à-Frongier (23200)' => '23211',
+ 'Saint-Marc-à-Loubaud (23460)' => '23212',
+ 'Saint-Marc-la-Lande (79310)' => '79271',
+ 'Saint-Marcel-du-Périgord (24510)' => '24445',
+ 'Saint-Marcory (24540)' => '24446',
+ 'Saint-Mard (17700)' => '17359',
+ 'Saint-Marien (23600)' => '23213',
+ 'Saint-Mariens (33620)' => '33439',
+ 'Saint-Martial (16190)' => '16334',
+ 'Saint-Martial (17330)' => '17361',
+ 'Saint-Martial (33490)' => '33440',
+ 'Saint-Martial-d\'Albarède (24160)' => '24448',
+ 'Saint-Martial-d\'Artenset (24700)' => '24449',
+ 'Saint-Martial-de-Gimel (19150)' => '19220',
+ 'Saint-Martial-de-Mirambeau (17150)' => '17362',
+ 'Saint-Martial-de-Nabirat (24250)' => '24450',
+ 'Saint-Martial-de-Valette (24300)' => '24451',
+ 'Saint-Martial-de-Vitaterne (17500)' => '17363',
+ 'Saint-Martial-Entraygues (19400)' => '19221',
+ 'Saint-Martial-le-Mont (23150)' => '23214',
+ 'Saint-Martial-le-Vieux (23100)' => '23215',
+ 'Saint-Martial-sur-Isop (87330)' => '87163',
+ 'Saint-Martial-sur-Né (17520)' => '17364',
+ 'Saint-Martial-Viveyrol (24320)' => '24452',
+ 'Saint-Martin-Château (23460)' => '23216',
+ 'Saint-Martin-Curton (47700)' => '47254',
+ 'Saint-Martin-d\'Arberoue (64640)' => '64489',
+ 'Saint-Martin-d\'Arrossa (64780)' => '64490',
+ 'Saint-Martin-d\'Ary (17270)' => '17365',
+ 'Saint-Martin-d\'Oney (40090)' => '40274',
+ 'Saint-Martin-de-Beauville (47270)' => '47255',
+ 'Saint-Martin-de-Bernegoue (79230)' => '79273',
+ 'Saint-Martin-de-Coux (17360)' => '17366',
+ 'Saint-Martin-de-Fressengeas (24800)' => '24453',
+ 'Saint-Martin-de-Gurson (24610)' => '24454',
+ 'Saint-Martin-de-Hinx (40390)' => '40272',
+ 'Saint-Martin-de-Juillers (17400)' => '17367',
+ 'Saint-Martin-de-Jussac (87200)' => '87164',
+ 'Saint-Martin-de-Laye (33910)' => '33442',
+ 'Saint-Martin-de-Lerm (33540)' => '33443',
+ 'Saint-Martin-de-Mâcon (79100)' => '79274',
+ 'Saint-Martin-de-Ré (17410)' => '17369',
+ 'Saint-Martin-de-Ribérac (24600)' => '24455',
+ 'Saint-Martin-de-Saint-Maixent (79400)' => '79276',
+ 'Saint-Martin-de-Sanzay (79290)' => '79277',
+ 'Saint-Martin-de-Seignanx (40390)' => '40273',
+ 'Saint-Martin-de-Sescas (33490)' => '33444',
+ 'Saint-Martin-de-Villeréal (47210)' => '47256',
+ 'Saint-Martin-des-Combes (24140)' => '24456',
+ 'Saint-Martin-du-Bois (33910)' => '33445',
+ 'Saint-Martin-du-Clocher (16700)' => '16335',
+ 'Saint-Martin-du-Fouilloux (79420)' => '79278',
+ 'Saint-Martin-du-Puy (33540)' => '33446',
+ 'Saint-Martin-l\'Ars (86350)' => '86234',
+ 'Saint-Martin-l\'Astier (24400)' => '24457',
+ 'Saint-Martin-la-Méanne (19320)' => '19222',
+ 'Saint-Martin-Lacaussade (33390)' => '33441',
+ 'Saint-Martin-le-Mault (87360)' => '87165',
+ 'Saint-Martin-le-Pin (24300)' => '24458',
+ 'Saint-Martin-le-Vieux (87700)' => '87166',
+ 'Saint-Martin-lès-Melle (79500)' => '79279',
+ 'Saint-Martin-Petit (47180)' => '47257',
+ 'Saint-Martin-Sainte-Catherine (23430)' => '23217',
+ 'Saint-Martin-Sepert (19210)' => '19223',
+ 'Saint-Martin-Terressus (87400)' => '87167',
+ 'Saint-Mary (16260)' => '16336',
+ 'Saint-Mathieu (87440)' => '87168',
+ 'Saint-Maurice-de-Lestapel (47290)' => '47259',
+ 'Saint-Maurice-des-Lions (16500)' => '16337',
+ 'Saint-Maurice-la-Clouère (86160)' => '86235',
+ 'Saint-Maurice-la-Souterraine (23300)' => '23219',
+ 'Saint-Maurice-les-Brousses (87800)' => '87169',
+ 'Saint-Maurice-près-Crocq (23260)' => '23218',
+ 'Saint-Maurice-sur-Adour (40270)' => '40275',
+ 'Saint-Maurin (47270)' => '47260',
+ 'Saint-Maxire (79410)' => '79281',
+ 'Saint-Méard (87130)' => '87170',
+ 'Saint-Méard-de-Drône (24600)' => '24460',
+ 'Saint-Méard-de-Gurçon (24610)' => '24461',
+ 'Saint-Médard (16300)' => '16338',
+ 'Saint-Médard (17500)' => '17372',
+ 'Saint-Médard (64370)' => '64491',
+ 'Saint-Médard (79370)' => '79282',
+ 'Saint-Médard-d\'Aunis (17220)' => '17373',
+ 'Saint-Médard-d\'Excideuil (24160)' => '24463',
+ 'Saint-Médard-d\'Eyrans (33650)' => '33448',
+ 'Saint-Médard-de-Guizières (33230)' => '33447',
+ 'Saint-Médard-de-Mussidan (24400)' => '24462',
+ 'Saint-Médard-en-Jalles (33160)' => '33449',
+ 'Saint-Médard-la-Rochette (23200)' => '23220',
+ 'Saint-Même-les-Carrières (16720)' => '16340',
+ 'Saint-Merd-de-Lapleau (19320)' => '19225',
+ 'Saint-Merd-la-Breuille (23100)' => '23221',
+ 'Saint-Merd-les-Oussines (19170)' => '19226',
+ 'Saint-Mesmin (24270)' => '24464',
+ 'Saint-Mexant (19330)' => '19227',
+ 'Saint-Michel (16470)' => '16341',
+ 'Saint-Michel (64220)' => '64492',
+ 'Saint-Michel-de-Castelnau (33840)' => '33450',
+ 'Saint-Michel-de-Double (24400)' => '24465',
+ 'Saint-Michel-de-Fronsac (33126)' => '33451',
+ 'Saint-Michel-de-Lapujade (33190)' => '33453',
+ 'Saint-Michel-de-Montaigne (24230)' => '24466',
+ 'Saint-Michel-de-Rieufret (33720)' => '33452',
+ 'Saint-Michel-de-Veisse (23480)' => '23222',
+ 'Saint-Michel-de-Villadeix (24380)' => '24468',
+ 'Saint-Michel-Escalus (40550)' => '40276',
+ 'Saint-Moreil (23400)' => '23223',
+ 'Saint-Morillon (33650)' => '33454',
+ 'Saint-Nazaire-sur-Charente (17780)' => '17375',
+ 'Saint-Nexans (24520)' => '24472',
+ 'Saint-Nicolas-de-la-Balerme (47220)' => '47262',
+ 'Saint-Oradoux-de-Chirouze (23100)' => '23224',
+ 'Saint-Oradoux-près-Crocq (23260)' => '23225',
+ 'Saint-Ouen-d\'Aunis (17230)' => '17376',
+ 'Saint-Ouen-la-Thène (17490)' => '17377',
+ 'Saint-Ouen-sur-Gartempe (87300)' => '87172',
+ 'Saint-Palais (33820)' => '33456',
+ 'Saint-Palais (64120)' => '64493',
+ 'Saint-Palais-de-Négrignac (17210)' => '17378',
+ 'Saint-Palais-de-Phiolin (17800)' => '17379',
+ 'Saint-Palais-du-Né (16300)' => '16342',
+ 'Saint-Palais-sur-Mer (17420)' => '17380',
+ 'Saint-Pancrace (24530)' => '24474',
+ 'Saint-Pandelon (40180)' => '40277',
+ 'Saint-Pantaléon-de-Lapleau (19160)' => '19228',
+ 'Saint-Pantaléon-de-Larche (19600)' => '19229',
+ 'Saint-Pantaly-d\'Ans (24640)' => '24475',
+ 'Saint-Pantaly-d\'Excideuil (24160)' => '24476',
+ 'Saint-Pardon-de-Conques (33210)' => '33457',
+ 'Saint-Pardoult (17400)' => '17381',
+ 'Saint-Pardoux (79310)' => '79285',
+ 'Saint-Pardoux (87250)' => '87173',
+ 'Saint-Pardoux-Corbier (19210)' => '19230',
+ 'Saint-Pardoux-d\'Arnet (23260)' => '23226',
+ 'Saint-Pardoux-de-Drône (24600)' => '24477',
+ 'Saint-Pardoux-du-Breuil (47200)' => '47263',
+ 'Saint-Pardoux-et-Vielvic (24170)' => '24478',
+ 'Saint-Pardoux-Isaac (47800)' => '47264',
+ 'Saint-Pardoux-l\'Ortigier (19270)' => '19234',
+ 'Saint-Pardoux-la-Croisille (19320)' => '19231',
+ 'Saint-Pardoux-la-Rivière (24470)' => '24479',
+ 'Saint-Pardoux-le-Neuf (19200)' => '19232',
+ 'Saint-Pardoux-le-Neuf (23200)' => '23228',
+ 'Saint-Pardoux-le-Vieux (19200)' => '19233',
+ 'Saint-Pardoux-les-Cards (23150)' => '23229',
+ 'Saint-Pardoux-Morterolles (23400)' => '23227',
+ 'Saint-Pastour (47290)' => '47265',
+ 'Saint-Paul (19150)' => '19235',
+ 'Saint-Paul (33390)' => '33458',
+ 'Saint-Paul (87260)' => '87174',
+ 'Saint-Paul-de-Serre (24380)' => '24480',
+ 'Saint-Paul-en-Born (40200)' => '40278',
+ 'Saint-Paul-en-Gâtine (79240)' => '79286',
+ 'Saint-Paul-la-Roche (24800)' => '24481',
+ 'Saint-Paul-lès-Dax (40990)' => '40279',
+ 'Saint-Paul-Lizonne (24320)' => '24482',
+ 'Saint-Pé-de-Léren (64270)' => '64494',
+ 'Saint-Pé-Saint-Simon (47170)' => '47266',
+ 'Saint-Pée-sur-Nivelle (64310)' => '64495',
+ 'Saint-Perdon (40090)' => '40280',
+ 'Saint-Perdoux (24560)' => '24483',
+ 'Saint-Pey-d\'Armens (33330)' => '33459',
+ 'Saint-Pey-de-Castets (33350)' => '33460',
+ 'Saint-Philippe-d\'Aiguille (33350)' => '33461',
+ 'Saint-Philippe-du-Seignal (33220)' => '33462',
+ 'Saint-Pierre-Bellevue (23460)' => '23232',
+ 'Saint-Pierre-Chérignat (23430)' => '23230',
+ 'Saint-Pierre-d\'Amilly (17700)' => '17382',
+ 'Saint-Pierre-d\'Aurillac (33490)' => '33463',
+ 'Saint-Pierre-d\'Exideuil (86400)' => '86237',
+ 'Saint-Pierre-d\'Eyraud (24130)' => '24487',
+ 'Saint-Pierre-d\'Irube (64990)' => '64496',
+ 'Saint-Pierre-d\'Oléron (17310)' => '17385',
+ 'Saint-Pierre-de-Bat (33760)' => '33464',
+ 'Saint-Pierre-de-Buzet (47160)' => '47267',
+ 'Saint-Pierre-de-Chignac (24330)' => '24484',
+ 'Saint-Pierre-de-Clairac (47270)' => '47269',
+ 'Saint-Pierre-de-Côle (24800)' => '24485',
+ 'Saint-Pierre-de-Frugie (24450)' => '24486',
+ 'Saint-Pierre-de-Fursac (23290)' => '23231',
+ 'Saint-Pierre-de-Juillers (17400)' => '17383',
+ 'Saint-Pierre-de-l\'Isle (17330)' => '17384',
+ 'Saint-Pierre-de-Maillé (86260)' => '86236',
+ 'Saint-Pierre-de-Mons (33210)' => '33465',
+ 'Saint-Pierre-des-Échaubrognes (79700)' => '79289',
+ 'Saint-Pierre-du-Mont (40280)' => '40281',
+ 'Saint-Pierre-du-Palais (17270)' => '17386',
+ 'Saint-Pierre-le-Bost (23600)' => '23233',
+ 'Saint-Pierre-sur-Dropt (47120)' => '47271',
+ 'Saint-Pompain (79160)' => '79290',
+ 'Saint-Pompont (24170)' => '24488',
+ 'Saint-Porchaire (17250)' => '17387',
+ 'Saint-Preuil (16130)' => '16343',
+ 'Saint-Priest (23110)' => '23234',
+ 'Saint-Priest-de-Gimel (19800)' => '19236',
+ 'Saint-Priest-la-Feuille (23300)' => '23235',
+ 'Saint-Priest-la-Plaine (23240)' => '23236',
+ 'Saint-Priest-les-Fougères (24450)' => '24489',
+ 'Saint-Priest-Ligoure (87800)' => '87176',
+ 'Saint-Priest-Palus (23400)' => '23237',
+ 'Saint-Priest-sous-Aixe (87700)' => '87177',
+ 'Saint-Priest-Taurion (87480)' => '87178',
+ 'Saint-Privat (19220)' => '19237',
+ 'Saint-Privat-des-Prés (24410)' => '24490',
+ 'Saint-Projet-Saint-Constant (16110)' => '16344',
+ 'Saint-Quantin-de-Rançanne (17800)' => '17388',
+ 'Saint-Quentin-de-Baron (33750)' => '33466',
+ 'Saint-Quentin-de-Caplong (33220)' => '33467',
+ 'Saint-Quentin-de-Chalais (16210)' => '16346',
+ 'Saint-Quentin-du-Dropt (47330)' => '47272',
+ 'Saint-Quentin-la-Chabanne (23500)' => '23238',
+ 'Saint-Quentin-sur-Charente (16150)' => '16345',
+ 'Saint-Rabier (24210)' => '24491',
+ 'Saint-Raphaël (24160)' => '24493',
+ 'Saint-Rémy (19290)' => '19238',
+ 'Saint-Rémy (24700)' => '24494',
+ 'Saint-Rémy (79410)' => '79293',
+ 'Saint-Rémy-sur-Creuse (86220)' => '86241',
+ 'Saint-Robert (19310)' => '19239',
+ 'Saint-Robert (47340)' => '47273',
+ 'Saint-Rogatien (17220)' => '17391',
+ 'Saint-Romain (16210)' => '16347',
+ 'Saint-Romain (86250)' => '86242',
+ 'Saint-Romain-de-Benet (17600)' => '17393',
+ 'Saint-Romain-de-Monpazier (24540)' => '24495',
+ 'Saint-Romain-et-Saint-Clément (24800)' => '24496',
+ 'Saint-Romain-la-Virvée (33240)' => '33470',
+ 'Saint-Romain-le-Noble (47270)' => '47274',
+ 'Saint-Romain-sur-Gironde (17240)' => '17392',
+ 'Saint-Romans-des-Champs (79230)' => '79294',
+ 'Saint-Romans-lès-Melle (79500)' => '79295',
+ 'Saint-Salvadour (19700)' => '19240',
+ 'Saint-Salvy (47360)' => '47275',
+ 'Saint-Sardos (47360)' => '47276',
+ 'Saint-Saturnin (16290)' => '16348',
+ 'Saint-Saturnin-du-Bois (17700)' => '17394',
+ 'Saint-Saud-Lacoussière (24470)' => '24498',
+ 'Saint-Sauvant (17610)' => '17395',
+ 'Saint-Sauvant (86600)' => '86244',
+ 'Saint-Sauveur (24520)' => '24499',
+ 'Saint-Sauveur (33250)' => '33471',
+ 'Saint-Sauveur-d\'Aunis (17540)' => '17396',
+ 'Saint-Sauveur-de-Meilhan (47180)' => '47277',
+ 'Saint-Sauveur-de-Puynormand (33660)' => '33472',
+ 'Saint-Sauveur-Lalande (24700)' => '24500',
+ 'Saint-Savin (33920)' => '33473',
+ 'Saint-Savin (86310)' => '86246',
+ 'Saint-Savinien (17350)' => '17397',
+ 'Saint-Saviol (86400)' => '86247',
+ 'Saint-Sébastien (23160)' => '23239',
+ 'Saint-Secondin (86350)' => '86248',
+ 'Saint-Selve (33650)' => '33474',
+ 'Saint-Sernin (47120)' => '47278',
+ 'Saint-Setiers (19290)' => '19241',
+ 'Saint-Seurin-de-Bourg (33710)' => '33475',
+ 'Saint-Seurin-de-Cadourne (33180)' => '33476',
+ 'Saint-Seurin-de-Cursac (33390)' => '33477',
+ 'Saint-Seurin-de-Palenne (17800)' => '17398',
+ 'Saint-Seurin-de-Prats (24230)' => '24501',
+ 'Saint-Seurin-sur-l\'Isle (33660)' => '33478',
+ 'Saint-Sève (33190)' => '33479',
+ 'Saint-Sever (40500)' => '40282',
+ 'Saint-Sever-de-Saintonge (17800)' => '17400',
+ 'Saint-Séverin (16390)' => '16350',
+ 'Saint-Séverin-d\'Estissac (24190)' => '24502',
+ 'Saint-Séverin-sur-Boutonne (17330)' => '17401',
+ 'Saint-Sigismond-de-Clermont (17240)' => '17402',
+ 'Saint-Silvain-Bas-le-Roc (23600)' => '23240',
+ 'Saint-Silvain-Bellegarde (23190)' => '23241',
+ 'Saint-Silvain-Montaigut (23320)' => '23242',
+ 'Saint-Silvain-sous-Toulx (23140)' => '23243',
+ 'Saint-Simeux (16120)' => '16351',
+ 'Saint-Simon (16120)' => '16352',
+ 'Saint-Simon-de-Bordes (17500)' => '17403',
+ 'Saint-Simon-de-Pellouaille (17260)' => '17404',
+ 'Saint-Sixte (47220)' => '47279',
+ 'Saint-Solve (19130)' => '19242',
+ 'Saint-Sorlin-de-Conac (17150)' => '17405',
+ 'Saint-Sornin (16220)' => '16353',
+ 'Saint-Sornin (17600)' => '17406',
+ 'Saint-Sornin-la-Marche (87210)' => '87179',
+ 'Saint-Sornin-Lavolps (19230)' => '19243',
+ 'Saint-Sornin-Leulac (87290)' => '87180',
+ 'Saint-Sulpice-d\'Arnoult (17250)' => '17408',
+ 'Saint-Sulpice-d\'Excideuil (24800)' => '24505',
+ 'Saint-Sulpice-de-Cognac (16370)' => '16355',
+ 'Saint-Sulpice-de-Faleyrens (33330)' => '33480',
+ 'Saint-Sulpice-de-Guilleragues (33580)' => '33481',
+ 'Saint-Sulpice-de-Mareuil (24340)' => '24503',
+ 'Saint-Sulpice-de-Pommiers (33540)' => '33482',
+ 'Saint-Sulpice-de-Roumagnac (24600)' => '24504',
+ 'Saint-Sulpice-de-Royan (17200)' => '17409',
+ 'Saint-Sulpice-de-Ruffec (16460)' => '16356',
+ 'Saint-Sulpice-et-Cameyrac (33450)' => '33483',
+ 'Saint-Sulpice-Laurière (87370)' => '87181',
+ 'Saint-Sulpice-le-Dunois (23800)' => '23244',
+ 'Saint-Sulpice-le-Guérétois (23000)' => '23245',
+ 'Saint-Sulpice-les-Bois (19250)' => '19244',
+ 'Saint-Sulpice-les-Champs (23480)' => '23246',
+ 'Saint-Sulpice-les-Feuilles (87160)' => '87182',
+ 'Saint-Sylvain (19380)' => '19245',
+ 'Saint-Sylvestre (87240)' => '87183',
+ 'Saint-Sylvestre-sur-Lot (47140)' => '47280',
+ 'Saint-Symphorien (33113)' => '33484',
+ 'Saint-Symphorien (79270)' => '79298',
+ 'Saint-Symphorien-sur-Couze (87140)' => '87184',
+ 'Saint-Thomas-de-Conac (17150)' => '17410',
+ 'Saint-Trojan (33710)' => '33486',
+ 'Saint-Trojan-les-Bains (17370)' => '17411',
+ 'Saint-Urcisse (47270)' => '47281',
+ 'Saint-Vaize (17100)' => '17412',
+ 'Saint-Vallier (16480)' => '16357',
+ 'Saint-Varent (79330)' => '79299',
+ 'Saint-Vaury (23320)' => '23247',
+ 'Saint-Viance (19240)' => '19246',
+ 'Saint-Victor (24350)' => '24508',
+ 'Saint-Victor-en-Marche (23000)' => '23248',
+ 'Saint-Victour (19200)' => '19247',
+ 'Saint-Victurnien (87420)' => '87185',
+ 'Saint-Vincent (64800)' => '64498',
+ 'Saint-Vincent-de-Connezac (24190)' => '24509',
+ 'Saint-Vincent-de-Cosse (24220)' => '24510',
+ 'Saint-Vincent-de-Lamontjoie (47310)' => '47282',
+ 'Saint-Vincent-de-Paul (33440)' => '33487',
+ 'Saint-Vincent-de-Paul (40990)' => '40283',
+ 'Saint-Vincent-de-Pertignas (33420)' => '33488',
+ 'Saint-Vincent-de-Tyrosse (40230)' => '40284',
+ 'Saint-Vincent-Jalmoutiers (24410)' => '24511',
+ 'Saint-Vincent-la-Châtre (79500)' => '79301',
+ 'Saint-Vincent-le-Paluel (24200)' => '24512',
+ 'Saint-Vincent-sur-l\'Isle (24420)' => '24513',
+ 'Saint-Vite (47500)' => '47283',
+ 'Saint-Vitte-sur-Briance (87380)' => '87186',
+ 'Saint-Vivien (17220)' => '17413',
+ 'Saint-Vivien (24230)' => '24514',
+ 'Saint-Vivien-de-Blaye (33920)' => '33489',
+ 'Saint-Vivien-de-Médoc (33590)' => '33490',
+ 'Saint-Vivien-de-Monségur (33580)' => '33491',
+ 'Saint-Xandre (17138)' => '17414',
+ 'Saint-Yaguen (40400)' => '40285',
+ 'Saint-Ybard (19140)' => '19248',
+ 'Saint-Yrieix-la-Montagne (23460)' => '23249',
+ 'Saint-Yrieix-la-Perche (87500)' => '87187',
+ 'Saint-Yrieix-le-Déjalat (19300)' => '19249',
+ 'Saint-Yrieix-les-Bois (23150)' => '23250',
+ 'Saint-Yrieix-sous-Aixe (87700)' => '87188',
+ 'Saint-Yrieix-sur-Charente (16710)' => '16358',
+ 'Saint-Yzan-de-Soudiac (33920)' => '33492',
+ 'Saint-Yzans-de-Médoc (33340)' => '33493',
+ 'Sainte-Alvère-Saint-Laurent Les Bâtons (24510)' => '24362',
+ 'Sainte-Anne-Saint-Priest (87120)' => '87134',
+ 'Sainte-Bazeille (47180)' => '47233',
+ 'Sainte-Blandine (79370)' => '79240',
+ 'Sainte-Colombe (16230)' => '16309',
+ 'Sainte-Colombe (17210)' => '17319',
+ 'Sainte-Colombe (33350)' => '33390',
+ 'Sainte-Colombe (40700)' => '40252',
+ 'Sainte-Colombe-de-Duras (47120)' => '47236',
+ 'Sainte-Colombe-de-Villeneuve (47300)' => '47237',
+ 'Sainte-Colombe-en-Bruilhois (47310)' => '47238',
+ 'Sainte-Colome (64260)' => '64473',
+ 'Sainte-Croix (24440)' => '24393',
+ 'Sainte-Croix-de-Mareuil (24340)' => '24394',
+ 'Sainte-Croix-du-Mont (33410)' => '33392',
+ 'Sainte-Eanne (79800)' => '79246',
+ 'Sainte-Engrâce (64560)' => '64475',
+ 'Sainte-Eulalie (33560)' => '33397',
+ 'Sainte-Eulalie-d\'Ans (24640)' => '24401',
+ 'Sainte-Eulalie-d\'Eymet (24500)' => '24402',
+ 'Sainte-Eulalie-en-Born (40200)' => '40257',
+ 'Sainte-Féréole (19270)' => '19202',
+ 'Sainte-Feyre (23000)' => '23193',
+ 'Sainte-Feyre-la-Montagne (23500)' => '23194',
+ 'Sainte-Florence (33350)' => '33401',
+ 'Sainte-Fortunade (19490)' => '19203',
+ 'Sainte-Foy (40190)' => '40258',
+ 'Sainte-Foy-de-Belvès (24170)' => '24406',
+ 'Sainte-Foy-de-Longas (24510)' => '24407',
+ 'Sainte-Foy-la-Grande (33220)' => '33402',
+ 'Sainte-Foy-la-Longue (33490)' => '33403',
+ 'Sainte-Gemme (17250)' => '17330',
+ 'Sainte-Gemme (33580)' => '33404',
+ 'Sainte-Gemme (79330)' => '79250',
+ 'Sainte-Gemme-Martaillac (47250)' => '47244',
+ 'Sainte-Hélène (33480)' => '33417',
+ 'Sainte-Innocence (24500)' => '24423',
+ 'Sainte-Lheurine (17520)' => '17355',
+ 'Sainte-Livrade-sur-Lot (47110)' => '47252',
+ 'Sainte-Marie-de-Chignac (24330)' => '24447',
+ 'Sainte-Marie-de-Gosse (40390)' => '40271',
+ 'Sainte-Marie-de-Ré (17740)' => '17360',
+ 'Sainte-Marie-de-Vaux (87420)' => '87162',
+ 'Sainte-Marie-Lapanouze (19160)' => '19219',
+ 'Sainte-Marthe (47430)' => '47253',
+ 'Sainte-Maure-de-Peyriac (47170)' => '47258',
+ 'Sainte-Même (17770)' => '17374',
+ 'Sainte-Mondane (24370)' => '24470',
+ 'Sainte-Nathalène (24200)' => '24471',
+ 'Sainte-Néomaye (79260)' => '79283',
+ 'Sainte-Orse (24210)' => '24473',
+ 'Sainte-Ouenne (79220)' => '79284',
+ 'Sainte-Radegonde (17250)' => '17389',
+ 'Sainte-Radegonde (24560)' => '24492',
+ 'Sainte-Radegonde (33350)' => '33468',
+ 'Sainte-Radegonde (79100)' => '79292',
+ 'Sainte-Radégonde (86300)' => '86239',
+ 'Sainte-Ramée (17240)' => '17390',
+ 'Sainte-Sévère (16200)' => '16349',
+ 'Sainte-Soline (79120)' => '79297',
+ 'Sainte-Souline (16480)' => '16354',
+ 'Sainte-Soulle (17220)' => '17407',
+ 'Sainte-Terre (33350)' => '33485',
+ 'Sainte-Trie (24160)' => '24507',
+ 'Sainte-Verge (79100)' => '79300',
+ 'Saintes (17100)' => '17415',
+ 'Saires (86420)' => '86249',
+ 'Saivres (79400)' => '79302',
+ 'Saix (86120)' => '86250',
+ 'Salagnac (24160)' => '24515',
+ 'Salaunes (33160)' => '33494',
+ 'Saleignes (17510)' => '17416',
+ 'Salies-de-Béarn (64270)' => '64499',
+ 'Salignac-de-Mirambeau (17130)' => '17417',
+ 'Salignac-Eyvigues (24590)' => '24516',
+ 'Salignac-sur-Charente (17800)' => '17418',
+ 'Salleboeuf (33370)' => '33496',
+ 'Salles (33770)' => '33498',
+ 'Salles (47150)' => '47284',
+ 'Salles (79800)' => '79303',
+ 'Salles-d\'Angles (16130)' => '16359',
+ 'Salles-de-Barbezieux (16300)' => '16360',
+ 'Salles-de-Belvès (24170)' => '24517',
+ 'Salles-de-Villefagnan (16700)' => '16361',
+ 'Salles-Lavalette (16190)' => '16362',
+ 'Salles-Mongiscard (64300)' => '64500',
+ 'Salles-sur-Mer (17220)' => '17420',
+ 'Sallespisse (64300)' => '64501',
+ 'Salon (24380)' => '24518',
+ 'Salon-la-Tour (19510)' => '19250',
+ 'Samadet (40320)' => '40286',
+ 'Samazan (47250)' => '47285',
+ 'Sames (64520)' => '64502',
+ 'Sammarçolles (86200)' => '86252',
+ 'Samonac (33710)' => '33500',
+ 'Samsons-Lion (64350)' => '64503',
+ 'Sanguinet (40460)' => '40287',
+ 'Sannat (23110)' => '23167',
+ 'Sansais (79270)' => '79304',
+ 'Sanxay (86600)' => '86253',
+ 'Sarbazan (40120)' => '40288',
+ 'Sardent (23250)' => '23168',
+ 'Sare (64310)' => '64504',
+ 'Sarlande (24270)' => '24519',
+ 'Sarlat-la-Canéda (24200)' => '24520',
+ 'Sarliac-sur-l\'Isle (24420)' => '24521',
+ 'Sarpourenx (64300)' => '64505',
+ 'Sarran (19800)' => '19251',
+ 'Sarrance (64490)' => '64506',
+ 'Sarrazac (24800)' => '24522',
+ 'Sarraziet (40500)' => '40289',
+ 'Sarron (40800)' => '40290',
+ 'Sarroux (19110)' => '19252',
+ 'Saubion (40230)' => '40291',
+ 'Saubole (64420)' => '64507',
+ 'Saubrigues (40230)' => '40292',
+ 'Saubusse (40180)' => '40293',
+ 'Saucats (33650)' => '33501',
+ 'Saucède (64400)' => '64508',
+ 'Saugnac-et-Cambran (40180)' => '40294',
+ 'Saugnacq-et-Muret (40410)' => '40295',
+ 'Saugon (33920)' => '33502',
+ 'Sauguis-Saint-Étienne (64470)' => '64509',
+ 'Saujon (17600)' => '17421',
+ 'Saulgé (86500)' => '86254',
+ 'Saulgond (16420)' => '16363',
+ 'Sault-de-Navailles (64300)' => '64510',
+ 'Sauméjan (47420)' => '47286',
+ 'Saumont (47600)' => '47287',
+ 'Saumos (33680)' => '33503',
+ 'Saurais (79200)' => '79306',
+ 'Saussignac (24240)' => '24523',
+ 'Sauternes (33210)' => '33504',
+ 'Sauvagnac (16310)' => '16364',
+ 'Sauvagnas (47340)' => '47288',
+ 'Sauvagnon (64230)' => '64511',
+ 'Sauvelade (64150)' => '64512',
+ 'Sauveterre-de-Béarn (64390)' => '64513',
+ 'Sauveterre-de-Guyenne (33540)' => '33506',
+ 'Sauveterre-la-Lémance (47500)' => '47292',
+ 'Sauveterre-Saint-Denis (47220)' => '47293',
+ 'Sauviac (33430)' => '33507',
+ 'Sauviat-sur-Vige (87400)' => '87190',
+ 'Sauvignac (16480)' => '16365',
+ 'Sauzé-Vaussais (79190)' => '79307',
+ 'Savennes (23000)' => '23170',
+ 'Savignac (33124)' => '33508',
+ 'Savignac-de-Duras (47120)' => '47294',
+ 'Savignac-de-l\'Isle (33910)' => '33509',
+ 'Savignac-de-Miremont (24260)' => '24524',
+ 'Savignac-de-Nontron (24300)' => '24525',
+ 'Savignac-Lédrier (24270)' => '24526',
+ 'Savignac-les-Églises (24420)' => '24527',
+ 'Savignac-sur-Leyze (47150)' => '47295',
+ 'Savigné (86400)' => '86255',
+ 'Savigny-Lévescault (86800)' => '86256',
+ 'Savigny-sous-Faye (86140)' => '86257',
+ 'Sceau-Saint-Angel (24300)' => '24528',
+ 'Sciecq (79000)' => '79308',
+ 'Scillé (79240)' => '79309',
+ 'Scorbé-Clairvaux (86140)' => '86258',
+ 'Séby (64410)' => '64514',
+ 'Secondigné-sur-Belle (79170)' => '79310',
+ 'Secondigny (79130)' => '79311',
+ 'Sedze-Maubecq (64160)' => '64515',
+ 'Sedzère (64160)' => '64516',
+ 'Ségalas (47410)' => '47296',
+ 'Segonzac (16130)' => '16366',
+ 'Segonzac (19310)' => '19253',
+ 'Segonzac (24600)' => '24529',
+ 'Ségur-le-Château (19230)' => '19254',
+ 'Seigné (17510)' => '17422',
+ 'Seignosse (40510)' => '40296',
+ 'Seilhac (19700)' => '19255',
+ 'Séligné (79170)' => '79312',
+ 'Sembas (47360)' => '47297',
+ 'Séméacq-Blachon (64350)' => '64517',
+ 'Semens (33490)' => '33510',
+ 'Semillac (17150)' => '17423',
+ 'Semoussac (17150)' => '17424',
+ 'Semussac (17120)' => '17425',
+ 'Sencenac-Puy-de-Fourches (24310)' => '24530',
+ 'Sendets (33690)' => '33511',
+ 'Sendets (64320)' => '64518',
+ 'Sénestis (47430)' => '47298',
+ 'Senillé-Saint-Sauveur (86100)' => '86245',
+ 'Sepvret (79120)' => '79313',
+ 'Sérandon (19160)' => '19256',
+ 'Séreilhac (87620)' => '87191',
+ 'Sergeac (24290)' => '24531',
+ 'Sérignac-Péboudou (47410)' => '47299',
+ 'Sérignac-sur-Garonne (47310)' => '47300',
+ 'Sérigny (86230)' => '86260',
+ 'Sérilhac (19190)' => '19257',
+ 'Sermur (23700)' => '23171',
+ 'Séron (65320)' => '65422',
+ 'Serres-Castet (64121)' => '64519',
+ 'Serres-et-Montguyard (24500)' => '24532',
+ 'Serres-Gaston (40700)' => '40298',
+ 'Serres-Morlaàs (64160)' => '64520',
+ 'Serres-Sainte-Marie (64170)' => '64521',
+ 'Serreslous-et-Arribans (40700)' => '40299',
+ 'Sers (16410)' => '16368',
+ 'Servanches (24410)' => '24533',
+ 'Servières-le-Château (19220)' => '19258',
+ 'Sévignacq (64160)' => '64523',
+ 'Sévignacq-Meyracq (64260)' => '64522',
+ 'Sèvres-Anxaumont (86800)' => '86261',
+ 'Sexcles (19430)' => '19259',
+ 'Seyches (47350)' => '47301',
+ 'Seyresse (40180)' => '40300',
+ 'Siecq (17490)' => '17427',
+ 'Siest (40180)' => '40301',
+ 'Sigalens (33690)' => '33512',
+ 'Sigogne (16200)' => '16369',
+ 'Sigoulès (24240)' => '24534',
+ 'Sillars (86320)' => '86262',
+ 'Sillas (33690)' => '33513',
+ 'Simacourbe (64350)' => '64524',
+ 'Simeyrols (24370)' => '24535',
+ 'Sindères (40110)' => '40302',
+ 'Singleyrac (24500)' => '24536',
+ 'Sioniac (19120)' => '19260',
+ 'Siorac-de-Ribérac (24600)' => '24537',
+ 'Siorac-en-Périgord (24170)' => '24538',
+ 'Sireuil (16440)' => '16370',
+ 'Siros (64230)' => '64525',
+ 'Smarves (86240)' => '86263',
+ 'Solférino (40210)' => '40303',
+ 'Solignac (87110)' => '87192',
+ 'Sommières-du-Clain (86160)' => '86264',
+ 'Sompt (79110)' => '79314',
+ 'Sonnac (17160)' => '17428',
+ 'Soorts-Hossegor (40150)' => '40304',
+ 'Sorbets (40320)' => '40305',
+ 'Sorde-l\'Abbaye (40300)' => '40306',
+ 'Sore (40430)' => '40307',
+ 'Sorges et Ligueux en Périgord (24420)' => '24540',
+ 'Sornac (19290)' => '19261',
+ 'Sort-en-Chalosse (40180)' => '40308',
+ 'Sos (47170)' => '47302',
+ 'Sossais (86230)' => '86265',
+ 'Soubise (17780)' => '17429',
+ 'Soubran (17150)' => '17430',
+ 'Soubrebost (23250)' => '23173',
+ 'Soudaine-Lavinadière (19370)' => '19262',
+ 'Soudan (79800)' => '79316',
+ 'Soudat (24360)' => '24541',
+ 'Soudeilles (19300)' => '19263',
+ 'Souffrignac (16380)' => '16372',
+ 'Soulac-sur-Mer (33780)' => '33514',
+ 'Soulaures (24540)' => '24542',
+ 'Soulignac (33760)' => '33515',
+ 'Soulignonne (17250)' => '17431',
+ 'Soumans (23600)' => '23174',
+ 'Soumensac (47120)' => '47303',
+ 'Souméras (17130)' => '17432',
+ 'Soumoulou (64420)' => '64526',
+ 'Souprosse (40250)' => '40309',
+ 'Souraïde (64250)' => '64527',
+ 'Soursac (19550)' => '19264',
+ 'Sourzac (24400)' => '24543',
+ 'Sous-Parsat (23150)' => '23175',
+ 'Sousmoulins (17130)' => '17433',
+ 'Soussac (33790)' => '33516',
+ 'Soussans (33460)' => '33517',
+ 'Soustons (40140)' => '40310',
+ 'Soutiers (79310)' => '79318',
+ 'Souvigné (16240)' => '16373',
+ 'Souvigné (79800)' => '79319',
+ 'Soyaux (16800)' => '16374',
+ 'Suaux (16260)' => '16375',
+ 'Suhescun (64780)' => '64528',
+ 'Surdoux (87130)' => '87193',
+ 'Surgères (17700)' => '17434',
+ 'Surin (79220)' => '79320',
+ 'Surin (86250)' => '86266',
+ 'Suris (16270)' => '16376',
+ 'Sus (64190)' => '64529',
+ 'Susmiou (64190)' => '64530',
+ 'Sussac (87130)' => '87194',
+ 'Tabaille-Usquain (64190)' => '64531',
+ 'Tabanac (33550)' => '33518',
+ 'Tadousse-Ussau (64330)' => '64532',
+ 'Taillant (17350)' => '17435',
+ 'Taillebourg (17350)' => '17436',
+ 'Taillebourg (47200)' => '47304',
+ 'Taillecavat (33580)' => '33520',
+ 'Taizé (79100)' => '79321',
+ 'Taizé-Aizie (16700)' => '16378',
+ 'Talais (33590)' => '33521',
+ 'Talence (33400)' => '33522',
+ 'Taller (40260)' => '40311',
+ 'Talmont-sur-Gironde (17120)' => '17437',
+ 'Tamniès (24620)' => '24544',
+ 'Tanzac (17260)' => '17438',
+ 'Taponnat-Fleurignac (16110)' => '16379',
+ 'Tardes (23170)' => '23251',
+ 'Tardets-Sorholus (64470)' => '64533',
+ 'Targon (33760)' => '33523',
+ 'Tarnac (19170)' => '19265',
+ 'Tarnès (33240)' => '33524',
+ 'Tarnos (40220)' => '40312',
+ 'Taron-Sadirac-Viellenave (64330)' => '64534',
+ 'Tarsacq (64360)' => '64535',
+ 'Tartas (40400)' => '40313',
+ 'Taugon (17170)' => '17439',
+ 'Tauriac (33710)' => '33525',
+ 'Tayac (33570)' => '33526',
+ 'Tayrac (47270)' => '47305',
+ 'Teillots (24390)' => '24545',
+ 'Temple-Laguyon (24390)' => '24546',
+ 'Tercé (86800)' => '86268',
+ 'Tercillat (23350)' => '23252',
+ 'Tercis-les-Bains (40180)' => '40314',
+ 'Ternant (17400)' => '17440',
+ 'Ternay (86120)' => '86269',
+ 'Terrasson-Lavilledieu (24120)' => '24547',
+ 'Tersannes (87360)' => '87195',
+ 'Tesson (17460)' => '17441',
+ 'Tessonnière (79600)' => '79325',
+ 'Téthieu (40990)' => '40315',
+ 'Teuillac (33710)' => '33530',
+ 'Teyjat (24300)' => '24548',
+ 'Thaims (17120)' => '17442',
+ 'Thairé (17290)' => '17443',
+ 'Thalamy (19200)' => '19266',
+ 'Thauron (23250)' => '23253',
+ 'Theil-Rabier (16240)' => '16381',
+ 'Thénac (17460)' => '17444',
+ 'Thénac (24240)' => '24549',
+ 'Thénezay (79390)' => '79326',
+ 'Thenon (24210)' => '24550',
+ 'Thézac (17600)' => '17445',
+ 'Thézac (47370)' => '47307',
+ 'Thèze (64450)' => '64536',
+ 'Thiat (87320)' => '87196',
+ 'Thiviers (24800)' => '24551',
+ 'Thollet (86290)' => '86270',
+ 'Thonac (24290)' => '24552',
+ 'Thorigné (79370)' => '79327',
+ 'Thorigny-sur-le-Mignon (79360)' => '79328',
+ 'Thors (17160)' => '17446',
+ 'Thouars (79100)' => '79329',
+ 'Thouars-sur-Garonne (47230)' => '47308',
+ 'Thouron (87140)' => '87197',
+ 'Thurageau (86110)' => '86271',
+ 'Thuré (86540)' => '86272',
+ 'Tilh (40360)' => '40316',
+ 'Tillou (79110)' => '79330',
+ 'Tizac-de-Curton (33420)' => '33531',
+ 'Tizac-de-Lapouyade (33620)' => '33532',
+ 'Tocane-Saint-Apre (24350)' => '24553',
+ 'Tombeboeuf (47380)' => '47309',
+ 'Tonnay-Boutonne (17380)' => '17448',
+ 'Tonnay-Charente (17430)' => '17449',
+ 'Tonneins (47400)' => '47310',
+ 'Torsac (16410)' => '16382',
+ 'Torxé (17380)' => '17450',
+ 'Tosse (40230)' => '40317',
+ 'Toulenne (33210)' => '33533',
+ 'Toulouzette (40250)' => '40318',
+ 'Toulx-Sainte-Croix (23600)' => '23254',
+ 'Tourliac (47210)' => '47311',
+ 'Tournon-d\'Agenais (47370)' => '47312',
+ 'Tourriers (16560)' => '16383',
+ 'Tourtenay (79100)' => '79331',
+ 'Tourtoirac (24390)' => '24555',
+ 'Tourtrès (47380)' => '47313',
+ 'Touvérac (16360)' => '16384',
+ 'Touvre (16600)' => '16385',
+ 'Touzac (16120)' => '16386',
+ 'Toy-Viam (19170)' => '19268',
+ 'Trayes (79240)' => '79332',
+ 'Treignac (19260)' => '19269',
+ 'Trélissac (24750)' => '24557',
+ 'Trémolat (24510)' => '24558',
+ 'Trémons (47140)' => '47314',
+ 'Trensacq (40630)' => '40319',
+ 'Trentels (47140)' => '47315',
+ 'Tresses (33370)' => '33535',
+ 'Triac-Lautrait (16200)' => '16387',
+ 'Trizay (17250)' => '17453',
+ 'Troche (19230)' => '19270',
+ 'Trois-Fonds (23230)' => '23255',
+ 'Trois-Palis (16730)' => '16388',
+ 'Trois-Villes (64470)' => '64537',
+ 'Tudeils (19120)' => '19271',
+ 'Tugéras-Saint-Maurice (17130)' => '17454',
+ 'Tulle (19000)' => '19272',
+ 'Turenne (19500)' => '19273',
+ 'Turgon (16350)' => '16389',
+ 'Tursac (24620)' => '24559',
+ 'Tusson (16140)' => '16390',
+ 'Tuzie (16700)' => '16391',
+ 'Uchacq-et-Parentis (40090)' => '40320',
+ 'Uhart-Cize (64220)' => '64538',
+ 'Uhart-Mixe (64120)' => '64539',
+ 'Urcuit (64990)' => '64540',
+ 'Urdès (64370)' => '64541',
+ 'Urdos (64490)' => '64542',
+ 'Urepel (64430)' => '64543',
+ 'Urgons (40320)' => '40321',
+ 'Urost (64160)' => '64544',
+ 'Urrugne (64122)' => '64545',
+ 'Urt (64240)' => '64546',
+ 'Urval (24480)' => '24560',
+ 'Ussac (19270)' => '19274',
+ 'Usseau (79210)' => '79334',
+ 'Usseau (86230)' => '86275',
+ 'Ussel (19200)' => '19275',
+ 'Usson-du-Poitou (86350)' => '86276',
+ 'Ustaritz (64480)' => '64547',
+ 'Uza (40170)' => '40322',
+ 'Uzan (64370)' => '64548',
+ 'Uzein (64230)' => '64549',
+ 'Uzerche (19140)' => '19276',
+ 'Uzeste (33730)' => '33537',
+ 'Uzos (64110)' => '64550',
+ 'Val d\'Issoire (87330)' => '87097',
+ 'Val de Virvée (33240)' => '33018',
+ 'Val des Vignes (16250)' => '16175',
+ 'Valdivienne (86300)' => '86233',
+ 'Valence (16460)' => '16392',
+ 'Valeuil (24310)' => '24561',
+ 'Valeyrac (33340)' => '33538',
+ 'Valiergues (19200)' => '19277',
+ 'Vallans (79270)' => '79335',
+ 'Vallereuil (24190)' => '24562',
+ 'Vallière (23120)' => '23257',
+ 'Valojoulx (24290)' => '24563',
+ 'Vançais (79120)' => '79336',
+ 'Vandré (17700)' => '17457',
+ 'Vanxains (24600)' => '24564',
+ 'Vanzac (17500)' => '17458',
+ 'Vanzay (79120)' => '79338',
+ 'Varaignes (24360)' => '24565',
+ 'Varaize (17400)' => '17459',
+ 'Vareilles (23300)' => '23258',
+ 'Varennes (24150)' => '24566',
+ 'Varennes (86110)' => '86277',
+ 'Varès (47400)' => '47316',
+ 'Varetz (19240)' => '19278',
+ 'Vars (16330)' => '16393',
+ 'Vars-sur-Roseix (19130)' => '19279',
+ 'Varzay (17460)' => '17460',
+ 'Vasles (79340)' => '79339',
+ 'Vaulry (87140)' => '87198',
+ 'Vaunac (24800)' => '24567',
+ 'Vausseroux (79420)' => '79340',
+ 'Vautebis (79420)' => '79341',
+ 'Vaux (86700)' => '86278',
+ 'Vaux-Lavalette (16320)' => '16394',
+ 'Vaux-Rouillac (16170)' => '16395',
+ 'Vaux-sur-Mer (17640)' => '17461',
+ 'Vaux-sur-Vienne (86220)' => '86279',
+ 'Vayres (33870)' => '33539',
+ 'Vayres (87600)' => '87199',
+ 'Végennes (19120)' => '19280',
+ 'Veix (19260)' => '19281',
+ 'Vélines (24230)' => '24568',
+ 'Vellèches (86230)' => '86280',
+ 'Vendays-Montalivet (33930)' => '33540',
+ 'Vendeuvre-du-Poitou (86380)' => '86281',
+ 'Vendoire (24320)' => '24569',
+ 'Vénérand (17100)' => '17462',
+ 'Vensac (33590)' => '33541',
+ 'Ventouse (16460)' => '16396',
+ 'Vérac (33240)' => '33542',
+ 'Verdelais (33490)' => '33543',
+ 'Verdets (64400)' => '64551',
+ 'Verdille (16140)' => '16397',
+ 'Verdon (24520)' => '24570',
+ 'Vergeroux (17300)' => '17463',
+ 'Vergné (17330)' => '17464',
+ 'Vergt (24380)' => '24571',
+ 'Vergt-de-Biron (24540)' => '24572',
+ 'Vérines (17540)' => '17466',
+ 'Verneiges (23170)' => '23259',
+ 'Verneuil (16310)' => '16398',
+ 'Verneuil-Moustiers (87360)' => '87200',
+ 'Verneuil-sur-Vienne (87430)' => '87201',
+ 'Vernon (86340)' => '86284',
+ 'Vernoux-en-Gâtine (79240)' => '79342',
+ 'Vernoux-sur-Boutonne (79170)' => '79343',
+ 'Verrières (16130)' => '16399',
+ 'Verrières (86410)' => '86285',
+ 'Verrue (86420)' => '86286',
+ 'Verruyes (79310)' => '79345',
+ 'Vert (40420)' => '40323',
+ 'Verteillac (24320)' => '24573',
+ 'Verteuil-d\'Agenais (47260)' => '47317',
+ 'Verteuil-sur-Charente (16510)' => '16400',
+ 'Vertheuil (33180)' => '33545',
+ 'Vervant (16330)' => '16401',
+ 'Vervant (17400)' => '17467',
+ 'Veyrac (87520)' => '87202',
+ 'Veyrières (19200)' => '19283',
+ 'Veyrignac (24370)' => '24574',
+ 'Veyrines-de-Domme (24250)' => '24575',
+ 'Veyrines-de-Vergt (24380)' => '24576',
+ 'Vézac (24220)' => '24577',
+ 'Vézières (86120)' => '86287',
+ 'Vialer (64330)' => '64552',
+ 'Viam (19170)' => '19284',
+ 'Vianne (47230)' => '47318',
+ 'Vibrac (16120)' => '16402',
+ 'Vibrac (17130)' => '17468',
+ 'Vicq-d\'Auribat (40380)' => '40324',
+ 'Vicq-sur-Breuilh (87260)' => '87203',
+ 'Vicq-sur-Gartempe (86260)' => '86288',
+ 'Vidaillat (23250)' => '23260',
+ 'Videix (87600)' => '87204',
+ 'Vielle-Saint-Girons (40560)' => '40326',
+ 'Vielle-Soubiran (40240)' => '40327',
+ 'Vielle-Tursan (40320)' => '40325',
+ 'Viellenave-d\'Arthez (64170)' => '64554',
+ 'Viellenave-de-Navarrenx (64190)' => '64555',
+ 'Vielleségure (64150)' => '64556',
+ 'Viennay (79200)' => '79347',
+ 'Viersat (23170)' => '23261',
+ 'Vieux-Boucau-les-Bains (40480)' => '40328',
+ 'Vieux-Mareuil (24340)' => '24579',
+ 'Vieux-Ruffec (16350)' => '16404',
+ 'Vigeois (19410)' => '19285',
+ 'Vigeville (23140)' => '23262',
+ 'Vignes (64410)' => '64557',
+ 'Vignolles (16300)' => '16405',
+ 'Vignols (19130)' => '19286',
+ 'Vignonet (33330)' => '33546',
+ 'Vilhonneur (16220)' => '16406',
+ 'Villac (24120)' => '24580',
+ 'Villamblard (24140)' => '24581',
+ 'Villandraut (33730)' => '33547',
+ 'Villard (23800)' => '23263',
+ 'Villars (24530)' => '24582',
+ 'Villars-en-Pons (17260)' => '17469',
+ 'Villars-les-Bois (17770)' => '17470',
+ 'Villebois-Lavalette (16320)' => '16408',
+ 'Villebramar (47380)' => '47319',
+ 'Villedoux (17230)' => '17472',
+ 'Villefagnan (16240)' => '16409',
+ 'Villefavard (87190)' => '87206',
+ 'Villefollet (79170)' => '79348',
+ 'Villefranche-de-Lonchat (24610)' => '24584',
+ 'Villefranche-du-Périgord (24550)' => '24585',
+ 'Villefranche-du-Queyran (47160)' => '47320',
+ 'Villefranque (64990)' => '64558',
+ 'Villegats (16700)' => '16410',
+ 'Villegouge (33141)' => '33548',
+ 'Villejésus (16140)' => '16411',
+ 'Villejoubert (16560)' => '16412',
+ 'Villemain (79110)' => '79349',
+ 'Villemorin (17470)' => '17473',
+ 'Villemort (86310)' => '86291',
+ 'Villenave (40110)' => '40330',
+ 'Villenave-d\'Ornon (33140)' => '33550',
+ 'Villenave-de-Rions (33550)' => '33549',
+ 'Villenave-près-Béarn (65500)' => '65476',
+ 'Villeneuve (33710)' => '33551',
+ 'Villeneuve-de-Duras (47120)' => '47321',
+ 'Villeneuve-de-Marsan (40190)' => '40331',
+ 'Villeneuve-la-Comtesse (17330)' => '17474',
+ 'Villeneuve-sur-Lot (47300)' => '47323',
+ 'Villeréal (47210)' => '47324',
+ 'Villeton (47400)' => '47325',
+ 'Villetoureix (24600)' => '24586',
+ 'Villexavier (17500)' => '17476',
+ 'Villiers (86190)' => '86292',
+ 'Villiers-Couture (17510)' => '17477',
+ 'Villiers-en-Bois (79360)' => '79350',
+ 'Villiers-en-Plaine (79160)' => '79351',
+ 'Villiers-le-Roux (16240)' => '16413',
+ 'Villiers-sur-Chizé (79170)' => '79352',
+ 'Villognon (16230)' => '16414',
+ 'Vinax (17510)' => '17478',
+ 'Vindelle (16430)' => '16415',
+ 'Viodos-Abense-de-Bas (64130)' => '64559',
+ 'Virazeil (47200)' => '47326',
+ 'Virelade (33720)' => '33552',
+ 'Virollet (17260)' => '17479',
+ 'Virsac (33240)' => '33553',
+ 'Virson (17290)' => '17480',
+ 'Vitrac (24200)' => '24587',
+ 'Vitrac-Saint-Vincent (16310)' => '16416',
+ 'Vitrac-sur-Montane (19800)' => '19287',
+ 'Viven (64450)' => '64560',
+ 'Viville (16120)' => '16417',
+ 'Vivonne (86370)' => '86293',
+ 'Voeuil-et-Giget (16400)' => '16418',
+ 'Voissay (17400)' => '17481',
+ 'Vouharte (16330)' => '16419',
+ 'Vouhé (17700)' => '17482',
+ 'Vouhé (79310)' => '79354',
+ 'Vouillé (79230)' => '79355',
+ 'Vouillé (86190)' => '86294',
+ 'Voulême (86400)' => '86295',
+ 'Voulgézac (16250)' => '16420',
+ 'Voulmentin (79150)' => '79242',
+ 'Voulon (86700)' => '86296',
+ 'Vouneuil-sous-Biard (86580)' => '86297',
+ 'Vouneuil-sur-Vienne (86210)' => '86298',
+ 'Voutezac (19130)' => '19288',
+ 'Vouthon (16220)' => '16421',
+ 'Vouzailles (86170)' => '86299',
+ 'Vouzan (16410)' => '16422',
+ 'Xaintrailles (47230)' => '47327',
+ 'Xaintray (79220)' => '79357',
+ 'Xambes (16330)' => '16423',
+ 'Ychoux (40160)' => '40332',
+ 'Ygos-Saint-Saturnin (40110)' => '40333',
+ 'Yssandon (19310)' => '19289',
+ 'Yversay (86170)' => '86300',
+ 'Yves (17340)' => '17483',
+ 'Yviers (16210)' => '16424',
+ 'Yvrac (33370)' => '33554',
+ 'Yvrac-et-Malleyrand (16110)' => '16425',
+ 'Yzosse (40180)' => '40334'
+ );
+}
diff --git a/bridges/AutoJMBridge.php b/bridges/AutoJMBridge.php
index 598f043..25fb2cb 100644
--- a/bridges/AutoJMBridge.php
+++ b/bridges/AutoJMBridge.php
@@ -3,63 +3,184 @@
class AutoJMBridge extends BridgeAbstract {
const NAME = 'AutoJM';
- const URI = 'http://www.autojm.fr/';
+ const URI = 'https://www.autojm.fr/';
const DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
'Afficher les offres de véhicules disponible en fonction des critères du site AutoJM' => array(
'url' => array(
- 'name' => 'URL de la recherche',
+ 'name' => 'URL du modèle',
'type' => 'text',
'required' => true,
'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/',
- 'exampleValue' => 'gammes/index/398?order_by=finition_asc&energie[]=3&transmission[]=2&dispo=all'
+ 'exampleValue' => 'achat-voitures-neuves-peugeot-nouvelle-308-5p'
+ ),
+ 'energy' => array(
+ 'name' => 'Carburant',
+ 'type' => 'list',
+ 'values' => array(
+ '-' => '',
+ 'Diesel' => 1,
+ 'Essence' => 3,
+ 'Hybride' => 5
+ ),
+ 'title' => 'Carburant'
+ ),
+ 'transmission' => array(
+ 'name' => 'Transmission',
+ 'type' => 'list',
+ 'values' => array(
+ '-' => '',
+ 'Automatique' => 1,
+ 'Manuelle' => 2
+ ),
+ 'title' => 'Transmission'
+ ),
+ 'priceMin' => array(
+ 'name' => 'Prix minimum',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Prix minimum du véhicule',
+ 'exampleValue' => '10000',
+ 'defaultValue' => '0'
+ ),
+ 'priceMax' => array(
+ 'name' => 'Prix maximum',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Prix maximum du véhicule',
+ 'exampleValue' => '15000',
+ 'defaultValue' => '150000'
)
)
);
const CACHE_TIMEOUT = 3600;
public function getIcon() {
- return self::URI . 'assets/images/favicon.ico';
+ return self::URI . 'favicon.ico';
+ }
+
+ public function getName() {
+ switch($this->queriedContext) {
+ case 'Afficher les offres de véhicules disponible en fonction des critères du site AutoJM':
+ $html = getSimpleHTMLDOMCached(self::URI . $this->getInput('url'), 86400);
+ $name = html_entity_decode($html->find('title', 0)->plaintext);
+ return $name;
+ break;
+ default:
+ return parent::getName();
+ }
+
}
public function collectData() {
- $html = getSimpleHTMLDOM(self::URI . $this->getInput('url'))
+
+ $model_url = self::URI . $this->getInput('url');
+
+ // Get the session cookies and the form token
+ $this->getInitialParameters($model_url);
+
+ // Build the form
+ $post_data = array(
+ 'form[energy]' => $this->getInput('energy'),
+ 'form[transmission]' => $this->getInput('transmission'),
+ 'form[priceMin]' => $this->getInput('priceMin'),
+ 'form[priceMin]' => $this->getInput('priceMin'),
+ 'form[_token]' => $this->token
+ );
+
+ // Set the Form request content type
+ $header = array(
+ 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
+ );
+
+ // Set the curl options (POST query and content, and session cookies
+ $curl_opts = array(
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => http_build_query($post_data),
+ CURLOPT_COOKIE => $this->cookies
+ );
+
+ // Get the JSON content of the form
+ $json = getContents($model_url, $header, $curl_opts)
or returnServerError('Could not request AutoJM.');
- $list = $html->find('div[class*=ligne_modele]');
- foreach($list as $element) {
- $image = $element->find('img[class=width-100]', 0)->src;
- $serie = $element->find('div[class=serie]', 0)->find('span', 0)->plaintext;
- $url = $element->find('div[class=serie]', 0)->find('a[class=btn_ligne color-black]', 0)->href;
- if($element->find('div[class*=hasStock-info]', 0) != null) {
- $dispo = 'Disponible';
- } else {
- $dispo = 'Sur commande';
+
+ // Extract the HTML content from the JSON result
+ $data = json_decode($json);
+ $html = str_get_html($data->content);
+
+ // Go through every finisha of the model
+ $list = $html->find('h3');
+ foreach ($list as $finish) {
+ $finish_name = $finish->plaintext;
+ $motorizations = $finish->next_sibling()->find('li');
+ foreach ($motorizations as $element) {
+ $image = $element->find('div[class=block-product-image]', 0)->{'data-ga-banner'};
+ $serie = $element->find('span[class=model]', 0)->plaintext;
+ $url = self::URI . substr($element->find('a', 0)->href, 1);
+ if ($element->find('span[class*=block-product-nbModel]', 0) != null) {
+ $availability = 'En Stock';
+ } else {
+ $availability = 'Sur commande';
+ }
+ $discount_html = $element->find('span[class*=tag--promo]', 0);
+ if ($discount_html != null) {
+ $discount = $discount_html->plaintext;
+ } else {
+ $discount = 'inconnue';
+ }
+ $price = $element->find('span[class=price red h1]', 0)->plaintext;
+ $item = array();
+ $item['title'] = $finish_name . ' ' . $serie;
+ $item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />'
+ . $finish_name . ' ' . $serie . '</p>';
+ $item['content'] .= '<ul><li>Disponibilité : ' . $availability . '</li>';
+ $item['content'] .= '<li>Série : ' . $serie . '</li>';
+ $item['content'] .= '<li>Remise : ' . $discount . '</li>';
+ $item['content'] .= '<li>Prix : ' . $price . '</li></ul>';
+
+ // Add a fictionnal anchor to the RSS element URL, based on the item content ;
+ // As the URL could be identical even if the price change, some RSS reader will not show those offers as new items
+ $item['uri'] = $url . '#' . md5($item['content']);
+
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ /**
+ * Gets the session cookie and the form token
+ *
+ * @param string $pageURL The URL from which to get the values
+ */
+ private function getInitialParameters($pageURL) {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $pageURL);
+ curl_setopt($ch, CURLOPT_HEADER, true);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ $data = curl_exec($ch);
+
+ // Separate the response header and the content
+ $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+ $header = substr($data, 0, $headerSize);
+ $content = substr($data, $headerSize);
+ curl_close($ch);
+
+ // Extract the cookies from the headers
+ $cookies = '';
+ $http_response_header = explode("\r\n", $header);
+ foreach ($http_response_header as $hdr) {
+ if (strpos($hdr, 'Set-Cookie') !== false) {
+ $cLine = explode(':', $hdr)[1];
+ $cLine = explode(';', $cLine)[0];
+ $cookies .= ';' . $cLine;
}
- $carburant = str_replace('dispo |', '', $element->find('div[class=carburant]', 0)->plaintext);
- $transmission = $element->find('div[class*=bv]', 0)->plaintext;
- $places = $element->find('div[class*=places]', 0)->plaintext;
- $portes = $element->find('div[class*=nb_portes]', 0)->plaintext;
- $carosserie = $element->find('div[class*=coloris]', 0)->plaintext;
- $remise = $element->find('div[class*=remise]', 0)->plaintext;
- $prix = $element->find('div[class*=prixjm]', 0)->plaintext;
-
- $item = array();
- $item['uri'] = $url;
- $item['title'] = $serie;
- $item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />' . $serie . '</p>';
- $item['content'] .= '<ul><li>Disponibilité : ' . $dispo . '</li>';
- $item['content'] .= '<li>Carburant : ' . $carburant . '</li>';
- $item['content'] .= '<li>Transmission : ' . $transmission . '</li>';
- $item['content'] .= '<li>Nombre de places : ' . $places . '</li>';
- $item['content'] .= '<li>Nombre de portes : ' . $portes . '</li>';
- $item['content'] .= '<li>Série : ' . $serie . '</li>';
- $item['content'] .= '<li>Carosserie : ' . $carosserie . '</li>';
- $item['content'] .= '<li>Remise : ' . $remise . '</li>';
- $item['content'] .= '<li>Prix : ' . $prix . '</li></ul>';
-
- $this->items[] = $item;
}
+ $this->cookies = trim(substr($cookies, 1));
+ // Get the token from the content
+ $html = str_get_html($content);
+ $token = $html->find('input[type=hidden][id=form__token]', 0);
+ $this->token = $token->value;
}
}
diff --git a/bridges/BAEBridge.php b/bridges/BAEBridge.php
index caa2cf7..6c5d8ba 100644
--- a/bridges/BAEBridge.php
+++ b/bridges/BAEBridge.php
@@ -55,9 +55,7 @@ class BAEBridge extends BridgeAbstract {
$content .= '<hr>';
$content .= $htmlDetail->find('section', 0)->innertext;
- $content = str_replace('src="/', 'src="' . parent::getURI() . '/', $content);
- $content = str_replace('href="/', 'href="' . parent::getURI() . '/', $content);
- $item['content'] = $content;
+ $item['content'] = defaultLinkTo($content, parent::getURI());
$image = $htmlDetail->find('#zoom', 0);
if ($image) {
$item['enclosures'] = array(parent::getURI() . $image->getAttribute('src'));
diff --git a/bridges/BadDragonBridge.php b/bridges/BadDragonBridge.php
new file mode 100644
index 0000000..d606c4e
--- /dev/null
+++ b/bridges/BadDragonBridge.php
@@ -0,0 +1,435 @@
+<?php
+class BadDragonBridge extends BridgeAbstract {
+ const NAME = 'Bad Dragon Bridge';
+ const URI = 'https://bad-dragon.com/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns sales or new clearance items';
+ const MAINTAINER = 'Roliga';
+ const PARAMETERS = array(
+ 'Sales' => array(
+ ),
+ 'Clearance' => array(
+ 'ready_made' => array(
+ 'name' => 'Ready Made',
+ 'type' => 'checkbox'
+ ),
+ 'flop' => array(
+ 'name' => 'Flops',
+ 'type' => 'checkbox'
+ ),
+ 'skus' => array(
+ 'name' => 'Products',
+ 'exampleValue' => 'chanceflared, crackers',
+ 'title' => 'Comma separated list of product SKUs'
+ ),
+ 'onesize' => array(
+ 'name' => 'One-Size',
+ 'type' => 'checkbox'
+ ),
+ 'mini' => array(
+ 'name' => 'Mini',
+ 'type' => 'checkbox'
+ ),
+ 'small' => array(
+ 'name' => 'Small',
+ 'type' => 'checkbox'
+ ),
+ 'medium' => array(
+ 'name' => 'Medium',
+ 'type' => 'checkbox'
+ ),
+ 'large' => array(
+ 'name' => 'Large',
+ 'type' => 'checkbox'
+ ),
+ 'extralarge' => array(
+ 'name' => 'Extra Large',
+ 'type' => 'checkbox'
+ ),
+ 'category' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => array(
+ 'All' => 'all',
+ 'Accessories' => 'accessories',
+ 'Merchandise' => 'merchandise',
+ 'Dildos' => 'insertable',
+ 'Masturbators' => 'penetrable',
+ 'Packers' => 'packer',
+ 'Lil\' Squirts' => 'shooter',
+ 'Lil\' Vibes' => 'vibrator',
+ 'Wearables' => 'wearable'
+ ),
+ 'defaultValue' => 'all',
+ ),
+ 'soft' => array(
+ 'name' => 'Soft Firmness',
+ 'type' => 'checkbox'
+ ),
+ 'med_firm' => array(
+ 'name' => 'Medium Firmness',
+ 'type' => 'checkbox'
+ ),
+ 'firm' => array(
+ 'name' => 'Firm',
+ 'type' => 'checkbox'
+ ),
+ 'split' => array(
+ 'name' => 'Split Firmness',
+ 'type' => 'checkbox'
+ ),
+ 'maxprice' => array(
+ 'name' => 'Max Price',
+ 'type' => 'number',
+ 'required' => true,
+ 'defaultValue' => 300
+ ),
+ 'minprice' => array(
+ 'name' => 'Min Price',
+ 'type' => 'number',
+ 'defaultValue' => 0
+ ),
+ 'cumtube' => array(
+ 'name' => 'Cumtube',
+ 'type' => 'checkbox'
+ ),
+ 'suctionCup' => array(
+ 'name' => 'Suction Cup',
+ 'type' => 'checkbox'
+ ),
+ 'noAccessories' => array(
+ 'name' => 'No Accessories',
+ 'type' => 'checkbox'
+ )
+ )
+ );
+
+ /*
+ * This sets index $strFrom (or $strTo if set) in $outArr to 'on' if
+ * $inArr[$param] contains $strFrom.
+ * It is used for translating BD's shop filter URLs into something we can use.
+ *
+ * For the query '?type[]=ready_made&type[]=flop' we would have an array like:
+ * Array (
+ * [type] => Array (
+ * [0] => ready_made
+ * [1] => flop
+ * )
+ * )
+ * which could be translated into:
+ * Array (
+ * [ready_made] => on
+ * [flop] => on
+ * )
+ * */
+ private function setParam($inArr, &$outArr, $param, $strFrom, $strTo = null) {
+ if(isset($inArr[$param]) && in_array($strFrom, $inArr[$param])) {
+ $outArr[($strTo ?: $strFrom)] = 'on';
+ }
+ }
+
+ public function detectParameters($url) {
+ $params = array();
+
+ // Sale
+ $regex = '/^(https?:\/\/)?bad-dragon\.com\/sales/';
+ if(preg_match($regex, $url, $matches) > 0) {
+ return $params;
+ }
+
+ // Clearance
+ $regex = '/^(https?:\/\/)?bad-dragon\.com\/shop\/clearance/';
+ if(preg_match($regex, $url, $matches) > 0) {
+ parse_str(parse_url($url, PHP_URL_QUERY), $urlParams);
+
+ $this->setParam($urlParams, $params, 'type', 'ready_made');
+ $this->setParam($urlParams, $params, 'type', 'flop');
+
+ if(isset($urlParams['skus'])) {
+ $skus = array();
+ foreach($urlParams['skus'] as $sku) {
+ is_string($sku) && $skus[] = $sku;
+ is_array($sku) && $skus[] = $sku[0];
+ }
+ $params['skus'] = implode(',', $skus);
+ }
+
+ $this->setParam($urlParams, $params, 'sizes', 'onesize');
+ $this->setParam($urlParams, $params, 'sizes', 'mini');
+ $this->setParam($urlParams, $params, 'sizes', 'small');
+ $this->setParam($urlParams, $params, 'sizes', 'medium');
+ $this->setParam($urlParams, $params, 'sizes', 'large');
+ $this->setParam($urlParams, $params, 'sizes', 'extralarge');
+
+ if(isset($urlParams['category'])) {
+ $params['category'] = strtolower($urlParams['category']);
+ } else{
+ $params['category'] = 'all';
+ }
+
+ $this->setParam($urlParams, $params, 'firmnessValues', 'soft');
+ $this->setParam($urlParams, $params, 'firmnessValues', 'medium', 'med_firm');
+ $this->setParam($urlParams, $params, 'firmnessValues', 'firm');
+ $this->setParam($urlParams, $params, 'firmnessValues', 'split');
+
+ if(isset($urlParams['price'])) {
+ isset($urlParams['price']['max'])
+ && $params['maxprice'] = $urlParams['price']['max'];
+ isset($urlParams['price']['min'])
+ && $params['minprice'] = $urlParams['price']['min'];
+ }
+
+ isset($urlParams['cumtube'])
+ && $urlParams['cumtube'] === '1'
+ && $params['cumtube'] = 'on';
+ isset($urlParams['suctionCup'])
+ && $urlParams['suctionCup'] === '1'
+ && $params['suctionCup'] = 'on';
+ isset($urlParams['noAccessories'])
+ && $urlParams['noAccessories'] === '1'
+ && $params['noAccessories'] = 'on';
+
+ return $params;
+ }
+
+ return null;
+ }
+
+ public function getName() {
+ switch($this->queriedContext) {
+ case 'Sales':
+ return 'Bad Dragon Sales';
+ case 'Clearance':
+ return 'Bad Dragon Clearance Search';
+ default:
+ return parent::getName();
+ }
+ }
+
+ public function getURI() {
+ switch($this->queriedContext) {
+ case 'Sales':
+ return self::URI . 'sales';
+ case 'Clearance':
+ return $this->inputToURL();
+ default:
+ return parent::getURI();
+ }
+ }
+
+ public function collectData() {
+ switch($this->queriedContext) {
+ case 'Sales':
+ $sales = json_decode(getContents(self::URI . 'api/sales'))
+ or returnServerError('Failed to query BD API');
+
+ foreach($sales as $sale) {
+ $item = array();
+
+ $item['title'] = $sale->title;
+ $item['timestamp'] = strtotime($sale->startDate);
+
+ $item['uri'] = $this->getURI() . '/' . $sale->slug;
+
+ $contentHTML = '<p><img src="' . $sale->image->url . '"></p>';
+ if(isset($sale->endDate)) {
+ $contentHTML .= '<p><b>This promotion ends on '
+ . gmdate('M j, Y \a\t g:i A T', strtotime($sale->endDate))
+ . '</b></p>';
+ } else {
+ $contentHTML .= '<p><b>This promotion never ends</b></p>';
+ }
+ $ul = false;
+ $content = json_decode($sale->content);
+ foreach($content->blocks as $block) {
+ switch($block->type) {
+ case 'header-one':
+ $contentHTML .= '<h1>' . $block->text . '</h1>';
+ break;
+ case 'header-two':
+ $contentHTML .= '<h2>' . $block->text . '</h2>';
+ break;
+ case 'header-three':
+ $contentHTML .= '<h3>' . $block->text . '</h3>';
+ break;
+ case 'unordered-list-item':
+ if(!$ul) {
+ $contentHTML .= '<ul>';
+ $ul = true;
+ }
+ $contentHTML .= '<li>' . $block->text . '</li>';
+ break;
+ default:
+ if($ul) {
+ $contentHTML .= '</ul>';
+ $ul = false;
+ }
+ $contentHTML .= '<p>' . $block->text . '</p>';
+ break;
+ }
+ }
+ $item['content'] = $contentHTML;
+
+ $this->items[] = $item;
+ }
+ break;
+ case 'Clearance':
+ $toyData = json_decode(getContents($this->inputToURL(true)))
+ or returnServerError('Failed to query BD API');
+
+ $productList = json_decode(getContents(self::URI
+ . 'api/inventory-toy/product-list'))
+ or returnServerError('Failed to query BD API');
+
+ foreach($toyData->toys as $toy) {
+ $item = array();
+
+ $item['uri'] = $this->getURI()
+ . '#'
+ . $toy->id;
+ $item['timestamp'] = strtotime($toy->created);
+
+ foreach($productList as $product) {
+ if($product->sku == $toy->sku) {
+ $item['title'] = $product->name;
+ break;
+ }
+ }
+
+ // images
+ $content = '<p>';
+ foreach($toy->images as $image) {
+ $content .= '<a href="'
+ . $image->fullFilename
+ . '"><img src="'
+ . $image->thumbFilename
+ . '" /></a>';
+ }
+ // price
+ $content .= '</p><p><b>Price:</b> $'
+ . $toy->price
+ // size
+ . '<br /><b>Size:</b> '
+ . $toy->size
+ // color
+ . '<br /><b>Color:</b> '
+ . $toy->color
+ // features
+ . '<br /><b>Features:</b> '
+ . ($toy->suction_cup ? 'Suction cup' : '')
+ . ($toy->suction_cup && $toy->cumtube ? ', ' : '')
+ . ($toy->cumtube ? 'Cumtube' : '')
+ . ($toy->suction_cup || $toy->cumtube ? '' : 'None');
+ // firmness
+ $firmnessTexts = array(
+ '2' => 'Extra soft',
+ '3' => 'Soft',
+ '5' => 'Medium',
+ '8' => 'Firm'
+ );
+ $firmnesses = explode('/', $toy->firmness);
+ if(count($firmnesses) === 2) {
+ $content .= '<br /><b>Firmness:</b> '
+ . $firmnessTexts[$firmnesses[0]]
+ . ', '
+ . $firmnessTexts[$firmnesses[1]];
+ } else{
+ $content .= '<br /><b>Firmness:</b> '
+ . $firmnessTexts[$firmnesses[0]];
+ }
+ // flop
+ if($toy->type === 'flop') {
+ $content .= '<br /><b>Flop reason:</b> '
+ . $toy->flop_reason;
+ }
+ $content .= '</p>';
+ $item['content'] = $content;
+
+ $enclosures = array();
+ foreach($toy->images as $image) {
+ $enclosures[] = $image->fullFilename;
+ }
+ $item['enclosures'] = $enclosures;
+
+ $categories = array();
+ $categories[] = $toy->sku;
+ $categories[] = $toy->type;
+ $categories[] = $toy->size;
+ if($toy->cumtube) {
+ $categories[] = 'cumtube';
+ }
+ if($toy->suction_cup) {
+ $categories[] = 'suction_cup';
+ }
+ $item['categories'] = $categories;
+
+ $this->items[] = $item;
+ }
+ break;
+ }
+ }
+
+ private function inputToURL($api = false) {
+ $url = self::URI;
+ $url .= ($api ? 'api/inventory-toys?' : 'shop/clearance?');
+
+ // Default parameters
+ $url .= 'limit=60';
+ $url .= '&page=1';
+ $url .= '&sort[field]=created';
+ $url .= '&sort[direction]=desc';
+
+ // Product types
+ $url .= ($this->getInput('ready_made') ? '&type[]=ready_made' : '');
+ $url .= ($this->getInput('flop') ? '&type[]=flop' : '');
+
+ // Product names
+ foreach(array_filter(explode(',', $this->getInput('skus'))) as $sku) {
+ $url .= '&skus[]=' . urlencode(trim($sku));
+ }
+
+ // Size
+ $url .= ($this->getInput('onesize') ? '&sizes[]=onesize' : '');
+ $url .= ($this->getInput('mini') ? '&sizes[]=mini' : '');
+ $url .= ($this->getInput('small') ? '&sizes[]=small' : '');
+ $url .= ($this->getInput('medium') ? '&sizes[]=medium' : '');
+ $url .= ($this->getInput('large') ? '&sizes[]=large' : '');
+ $url .= ($this->getInput('extralarge') ? '&sizes[]=extralarge' : '');
+
+ // Category
+ $url .= ($this->getInput('category') ? '&category='
+ . urlencode($this->getInput('category')) : '');
+
+ // Firmness
+ if($api) {
+ $url .= ($this->getInput('soft') ? '&firmnessValues[]=3' : '');
+ $url .= ($this->getInput('med_firm') ? '&firmnessValues[]=5' : '');
+ $url .= ($this->getInput('firm') ? '&firmnessValues[]=8' : '');
+ if($this->getInput('split')) {
+ $url .= '&firmnessValues[]=3/5';
+ $url .= '&firmnessValues[]=3/8';
+ $url .= '&firmnessValues[]=8/3';
+ $url .= '&firmnessValues[]=5/8';
+ $url .= '&firmnessValues[]=8/5';
+ }
+ } else{
+ $url .= ($this->getInput('soft') ? '&firmnessValues[]=soft' : '');
+ $url .= ($this->getInput('med_firm') ? '&firmnessValues[]=medium' : '');
+ $url .= ($this->getInput('firm') ? '&firmnessValues[]=firm' : '');
+ $url .= ($this->getInput('split') ? '&firmnessValues[]=split' : '');
+ }
+
+ // Price
+ $url .= ($this->getInput('maxprice') ? '&price[max]='
+ . $this->getInput('maxprice') : '&price[max]=300');
+ $url .= ($this->getInput('minprice') ? '&price[min]='
+ . $this->getInput('minprice') : '&price[min]=0');
+
+ // Features
+ $url .= ($this->getInput('cumtube') ? '&cumtube=1' : '');
+ $url .= ($this->getInput('suctionCup') ? '&suctionCup=1' : '');
+ $url .= ($this->getInput('noAccessories') ? '&noAccessories=1' : '');
+
+ return $url;
+ }
+}
diff --git a/bridges/BakaUpdatesMangaReleasesBridge.php b/bridges/BakaUpdatesMangaReleasesBridge.php
new file mode 100644
index 0000000..27eca28
--- /dev/null
+++ b/bridges/BakaUpdatesMangaReleasesBridge.php
@@ -0,0 +1,103 @@
+<?php
+class BakaUpdatesMangaReleasesBridge extends BridgeAbstract {
+ const NAME = 'Baka Updates Manga Releases';
+ const URI = 'https://www.mangaupdates.com/';
+ const DESCRIPTION = 'Get the latest series releases';
+ const MAINTAINER = 'fulmeek';
+ const PARAMETERS = array(array(
+ 'series_id' => array(
+ 'name' => 'Series ID',
+ 'type' => 'number',
+ 'required' => true,
+ 'exampleValue' => '12345'
+ )
+ ));
+ const LIMIT_COLS = 5;
+ const LIMIT_ITEMS = 10;
+
+ private $feedName = '';
+
+ public function collectData() {
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Series not found');
+
+ // content is an unstructured pile of divs, ugly to parse
+ $cols = $html->find('div#main_content div.row > div.text');
+ if (!$cols)
+ returnServerError('No releases');
+
+ $rows = array_slice(
+ array_chunk($cols, self::LIMIT_COLS), 0, self::LIMIT_ITEMS
+ );
+
+ if (isset($rows[0][1])) {
+ $this->feedName = $this->filterHTML($rows[0][1]->plaintext);
+ }
+
+ foreach($rows as $cols) {
+ if (count($cols) < self::LIMIT_COLS) continue;
+
+ $item = array();
+ $title = array();
+
+ $item['content'] = '';
+
+ $objDate = $cols[0];
+ if ($objDate)
+ $item['timestamp'] = strtotime($objDate->plaintext);
+
+ $objTitle = $cols[1];
+ if ($objTitle) {
+ $title[] = $this->filterHTML($objTitle->plaintext);
+ $item['content'] .= '<p>Series: ' . $this->filterText($objTitle->innertext) . '</p>';
+ }
+
+ $objVolume = $cols[2];
+ if ($objVolume && !empty($objVolume->plaintext))
+ $title[] = 'Vol.' . $objVolume->plaintext;
+
+ $objChapter = $cols[3];
+ if ($objChapter && !empty($objChapter->plaintext))
+ $title[] = 'Chp.' . $objChapter->plaintext;
+
+ $objAuthor = $cols[4];
+ if ($objAuthor && !empty($objAuthor->plaintext)) {
+ $item['author'] = $this->filterHTML($objAuthor->plaintext);
+ $item['content'] .= '<p>Groups: ' . $this->filterText($objAuthor->innertext) . '</p>';
+ }
+
+ $item['title'] = implode(' ', $title);
+ $item['uri'] = $this->getURI();
+ $item['uid'] = $this->getSanitizedHash($item['title']);
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI(){
+ $series_id = $this->getInput('series_id');
+ if (!empty($series_id)) {
+ return self::URI . 'releases.html?search=' . $series_id . '&stype=series';
+ }
+ return self::URI;
+ }
+
+ public function getName(){
+ if(!empty($this->feedName)) {
+ return $this->feedName . ' - ' . self::NAME;
+ }
+ return parent::getName();
+ }
+
+ private function getSanitizedHash($string) {
+ return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string))));
+ }
+
+ private function filterText($text) {
+ return rtrim($text, '* ');
+ }
+
+ private function filterHTML($text) {
+ return $this->filterText(html_entity_decode($text));
+ }
+}
diff --git a/bridges/BandcampBridge.php b/bridges/BandcampBridge.php
index 9c8d436..6c75ed5 100644
--- a/bridges/BandcampBridge.php
+++ b/bridges/BandcampBridge.php
@@ -13,48 +13,72 @@ class BandcampBridge extends BridgeAbstract {
'required' => true
)
));
+ const IMGURI = 'https://f4.bcbits.com/';
+ const IMGSIZE_300PX = 23;
+ const IMGSIZE_700PX = 16;
public function getIcon() {
return 'https://s4.bcbits.com/img/bc_favicon.ico';
}
public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI())
- or returnServerError('No results for this query.');
+ $url = self::URI . 'api/hub/1/dig_deeper';
+ $data = $this->buildRequestJson();
+ $header = array(
+ 'Content-Type: application/json',
+ 'Content-Length: ' . strlen($data)
+ );
+ $opts = array(
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_POSTFIELDS => $data
+ );
+ $content = getContents($url, $header, $opts)
+ or returnServerError('Could not complete request to: ' . $url);
- foreach($html->find('li.item') as $release) {
- $script = $release->find('div.art', 0)->getAttribute('onclick');
- $uri = ltrim($script, "return 'url(");
- $uri = rtrim($uri, "')");
+ $json = json_decode($content);
- $item = array();
- $item['author'] = $release->find('div.itemsubtext', 0)->plaintext
- . ' - '
- . $release->find('div.itemtext', 0)->plaintext;
+ if ($json->ok !== true) {
+ returnServerError('Invalid response');
+ }
- $item['title'] = $release->find('div.itemsubtext', 0)->plaintext
- . ' - '
- . $release->find('div.itemtext', 0)->plaintext;
+ foreach ($json->items as $entry) {
+ $url = $entry->tralbum_url;
+ $artist = $entry->artist;
+ $title = $entry->title;
+ // e.g. record label is the releaser, but not the artist
+ $releaser = $entry->band_name !== $entry->artist ? $entry->band_name : null;
- $item['content'] = '<img src="'
- . $uri
- . '"/><br/>'
- . $release->find('div.itemsubtext', 0)->plaintext
- . ' - '
- . $release->find('div.itemtext', 0)->plaintext;
+ $full_title = $artist . ' - ' . $title;
+ $full_artist = $artist;
+ if (isset($releaser)) {
+ $full_title .= ' (' . $releaser . ')';
+ $full_artist .= ' (' . $releaser . ')';
+ }
+ $small_img = $this->getImageUrl($entry->art_id, self::IMGSIZE_300PX);
+ $img = $this->getImageUrl($entry->art_id, self::IMGSIZE_700PX);
- $item['id'] = $release->find('a', 0)->getAttribute('href');
- $item['uri'] = $release->find('a', 0)->getAttribute('href');
+ $item = array(
+ 'uri' => $url,
+ 'author' => $full_artist,
+ 'title' => $full_title
+ );
+ $item['content'] = "<img src='$small_img' /><br/>$full_title";
+ $item['enclosures'] = array($img);
$this->items[] = $item;
}
}
- public function getURI(){
- if(!is_null($this->getInput('tag'))) {
- return self::URI . 'tag/' . urlencode($this->getInput('tag')) . '?sort_field=date';
- }
+ private function buildRequestJson(){
+ $requestJson = array(
+ 'tag' => $this->getInput('tag'),
+ 'page' => 1,
+ 'sort' => 'date'
+ );
+ return json_encode($requestJson);
+ }
- return parent::getURI();
+ private function getImageUrl($id, $size){
+ return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg';
}
public function getName(){
diff --git a/bridges/BinanceBridge.php b/bridges/BinanceBridge.php
new file mode 100644
index 0000000..9653ab7
--- /dev/null
+++ b/bridges/BinanceBridge.php
@@ -0,0 +1,103 @@
+<?php
+class BinanceBridge extends BridgeAbstract {
+ const NAME = 'Binance';
+ const URI = 'https://www.binance.com';
+ const DESCRIPTION = 'Subscribe to the Binance blog or the Binance Zendesk announcements.';
+ const MAINTAINER = 'thefranke';
+ const CACHE_TIMEOUT = 3600; // 1h
+
+ const PARAMETERS = array( array(
+ 'category' => array(
+ 'name' => 'category',
+ 'type' => 'list',
+ 'exampleValue' => 'Blog',
+ 'title' => 'Select a category',
+ 'values' => array(
+ 'Blog' => 'Blog',
+ 'Announcements' => 'Announcements'
+ )
+ )
+ ));
+
+ public function getIcon() {
+ return 'https://bin.bnbstatic.com/static/images/common/favicon.ico';
+ }
+
+ public function getName() {
+ return self::NAME . ' ' . $this->getInput('category');
+ }
+
+ public function getURI() {
+ if ($this->getInput('category') == 'Blog')
+ return self::URI . '/en/blog';
+ else
+ return 'https://binance.zendesk.com/hc/en-us/categories/115000056351-Announcements';
+ }
+
+ protected function collectBlogData() {
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not fetch Binance blog data.');
+
+ foreach($html->find('div[direction="row"]') as $element) {
+
+ $date = $element->find('div[direction="column"]', 0);
+ $day = $date->find('div', 0)->innertext;
+ $month = $date->find('div', 1)->innertext;
+ $extractedDate = $day . ' ' . $month;
+
+ $abstract = $element->find('div[direction="column"]', 1);
+ $a = $abstract->find('a', 0);
+ $uri = self::URI . $a->href;
+ $title = $a->innertext;
+
+ $full = getSimpleHTMLDOMCached($uri);
+ $content = $full->find('div.desc', 1);
+
+ $item = array();
+ $item['title'] = $title;
+ $item['uri'] = $uri;
+ $item['timestamp'] = strtotime($extractedDate);
+ $item['author'] = 'Binance';
+ $item['content'] = $content;
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10)
+ break;
+ }
+ }
+
+ protected function collectAnnouncementData() {
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not fetch Zendesk announcement data.');
+
+ foreach($html->find('a.article-list-link') as $a) {
+ $title = $a->innertext;
+ $uri = 'https://binance.zendesk.com' . $a->href;
+
+ $full = getSimpleHTMLDOMCached($uri);
+ $content = $full->find('div.article-body', 0);
+ $date = $full->find('time', 0)->getAttribute('datetime');
+
+ $item = array();
+
+ $item['title'] = $title;
+ $item['uri'] = $uri;
+ $item['timestamp'] = strtotime($date);
+ $item['author'] = 'Binance';
+ $item['content'] = $content;
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10)
+ break;
+ }
+ }
+
+ public function collectData() {
+ if ($this->getInput('category') == 'Blog')
+ $this->collectBlogData();
+ else
+ $this->collectAnnouncementData();
+ }
+}
diff --git a/bridges/BingSearchBridge.php b/bridges/BingSearchBridge.php
new file mode 100644
index 0000000..eb8a5fc
--- /dev/null
+++ b/bridges/BingSearchBridge.php
@@ -0,0 +1,119 @@
+<?php
+
+class BingSearchBridge extends BridgeAbstract
+{
+ const NAME = 'Bing search';
+ const URI = 'https://www.bing.com/';
+ const DESCRIPTION = 'Return images from bing search discover';
+ const MAINTAINER = 'DnAp';
+ const PARAMETERS = array(
+ 'Image Discover' => array(
+ 'category' => array(
+ 'name' => 'Categories',
+ 'type' => 'list',
+ 'values' => self::IMAGE_DISCOVER_CATEGORIES
+ ),
+ 'image_size' => array(
+ 'name' => 'Image size',
+ 'type' => 'list',
+ 'values' => array(
+ 'Small' => 'turl',
+ 'Full size' => 'imgurl'
+ )
+ )
+ )
+ );
+
+ const IMAGE_DISCOVER_CATEGORIES = array(
+ 'Abstract' => 'abstract',
+ 'Animals' => 'animals',
+ 'Anime' => 'anime',
+ 'Architecture' => 'architecture',
+ 'Arts and Crafts' => 'arts-and-crafts',
+ 'Beauty' => 'beauty',
+ 'Cars and Motorcycles' => 'cars-and-motorcycles',
+ 'Cats' => 'cats',
+ 'Celebrities' => 'celebrities',
+ 'Comics' => 'comics',
+ 'DIY' => 'diy',
+ 'Dogs' => 'dogs',
+ 'Fitness' => 'fitness',
+ 'Food and Drink' => 'food-and-drink',
+ 'Funny' => 'funny',
+ 'Gadgets' => 'gadgets',
+ 'Gardening' => 'gardening',
+ 'Geeky' => 'geeky',
+ 'Hairstyles' => 'hairstyles',
+ 'Home Decor' => 'home-decor',
+ 'Marine Life' => 'marine-life',
+ 'Men\'s Fashion' => 'men%27s-fashion',
+ 'Nature' => 'nature',
+ 'Outdoors' => 'outdoors',
+ 'Parenting' => 'parenting',
+ 'Phone Wallpapers' => 'phone-wallpapers',
+ 'Photography' => 'photography',
+ 'Quotes' => 'quotes',
+ 'Recipes' => 'recipes',
+ 'Snow' => 'snow',
+ 'Tattoos' => 'tattoos',
+ 'Travel' => 'travel',
+ 'Video Games' => 'video-games',
+ 'Weddings' => 'weddings',
+ 'Women\'s Fashion' => 'women%27s-fashion',
+ );
+
+ public function getIcon()
+ {
+ return 'https://www.bing.com/sa/simg/bing_p_rr_teal_min.ico';
+ }
+
+ public function collectData()
+ {
+ $this->items = $this->imageDiscover($this->getInput('category'));
+ }
+
+ public function getName()
+ {
+ if ($this->getInput('category')) {
+ if (self::IMAGE_DISCOVER_CATEGORIES[$this->getInput('categories')] !== null) {
+ $category = self::IMAGE_DISCOVER_CATEGORIES[$this->getInput('categories')];
+ } else {
+ $category = 'Unknown';
+ }
+
+ return 'Best ' . $category . ' - Bing Image Discover';
+ }
+ return parent::getName();
+ }
+
+ private function imageDiscover($category)
+ {
+ $html = getSimpleHTMLDOM(self::URI . '/discover/' . $category)
+ or returnServerError('Could not request ' . self::NAME);
+ $sizeKey = $this->getInput('image_size');
+
+ $items = [];
+ foreach ($html->find('a.iusc') as $element) {
+ $data = json_decode(htmlspecialchars_decode($element->getAttribute('m')), true);
+
+ $item = array();
+ $item['title'] = basename(rtrim($data['imgurl'], '/'));
+ $item['uri'] = $data['imgurl'];
+ $item['content'] = '<a href="' . $data['imgurl'] . '">
+ <img src="' . $data[$sizeKey] . '" alt="' . $item['title'] . '"></a>
+ <p>Source: <a href="' . $this->curUrl($data['surl']) . '"> </a></p>';
+ $item['enclosures'] = $data['imgurl'];
+
+ $items[] = $item;
+ }
+ return $items;
+ }
+
+ private function curUrl($url)
+ {
+ if (strlen($url) <= 80) {
+ return $url;
+ }
+ return substr($url, 0, 80) . '...';
+ }
+}
diff --git a/bridges/BrutBridge.php b/bridges/BrutBridge.php
new file mode 100644
index 0000000..32265b6
--- /dev/null
+++ b/bridges/BrutBridge.php
@@ -0,0 +1,157 @@
+<?php
+class BrutBridge extends BridgeAbstract {
+ const NAME = 'Brut Bridge';
+ const URI = 'https://www.brut.media';
+ const DESCRIPTION = 'Returns 5 newest videos by category and edition';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = array(array(
+ 'category' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => array(
+ 'News' => 'news',
+ 'International' => 'international',
+ 'Economy' => 'economy',
+ 'Science and Technology' => 'science-and-technology',
+ 'Entertainment' => 'entertainment',
+ 'Sports' => 'sport',
+ 'Nature' => 'nature',
+ ),
+ 'defaultValue' => 'news',
+ ),
+ 'edition' => array(
+ 'name' => ' Edition',
+ 'type' => 'list',
+ 'values' => array(
+ 'United States' => 'us',
+ 'United Kingdom' => 'uk',
+ 'France' => 'fr',
+ 'India' => 'in',
+ 'Mexico' => 'mx',
+ ),
+ 'defaultValue' => 'us',
+ )
+ )
+ );
+
+ const CACHE_TIMEOUT = 1800; // 30 mins
+
+ private $videoId = '';
+ private $videoType = '';
+ private $videoImage = '';
+
+ public function collectData() {
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request: ' . $this->getURI());
+
+ $results = $html->find('div.results', 0);
+
+ foreach($results->find('li.col-6.col-sm-4.col-md-3.col-lg-2.px-2.pb-4') as $index => $li) {
+ $item = array();
+
+ $videoPath = self::URI . $li->children(0)->href;
+
+ $videoPageHtml = getSimpleHTMLDOMCached($videoPath, 3600)
+ or returnServerError('Could not request: ' . $videoPath);
+
+ $this->videoImage = $videoPageHtml->find('meta[name="twitter:image"]', 0)->content;
+
+ $this->processTwitterImage();
+
+ $description = $videoPageHtml->find('div.description', 0);
+
+ $item['uri'] = $videoPath;
+ $item['title'] = $description->find('h1', 0)->plaintext;
+
+ if ($description->find('div.date', 0)->children(0)) {
+ $description->find('div.date', 0)->children(0)->outertext = '';
+ }
+
+ $item['content'] = $this->processContent(
+ $description
+ );
+
+ $item['timestamp'] = $this->processDate($description);
+ $item['enclosures'][] = $this->videoImage;
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 5) {
+ break;
+ }
+ }
+ }
+
+ public function getURI() {
+
+ if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
+ return self::URI . '/' . $this->getInput('edition') . '/' . $this->getInput('category');
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName() {
+
+ if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
+ $parameters = $this->getParameters();
+
+ $editionValues = array_flip($parameters[0]['edition']['values']);
+ $categoryValues = array_flip($parameters[0]['category']['values']);
+
+ return $categoryValues[$this->getInput('category')] . ' - ' .
+ $editionValues[$this->getInput('edition')] . ' - Brut.';
+ }
+
+ return parent::getName();
+ }
+
+ private function processDate($description) {
+
+ if ($this->getInput('edition') === 'uk') {
+ $date = DateTime::createFromFormat('d/m/Y H:i', $description->find('div.date', 0)->innertext);
+ return strtotime($date->format('Y-m-d H:i:s'));
+ }
+
+ return strtotime($description->find('div.date', 0)->innertext);
+ }
+
+ private function processContent($description) {
+
+ $content = '<video controls poster="' . $this->videoImage . '" preload="none">
+ <source src="https://content.brut.media/video/' . $this->videoId . '-' . $this->videoType . '-web.mp4"
+ type="video/mp4">
+ </video>';
+ $content .= '<p>' . $description->find('h2.mb-1', 0)->innertext . '</p>';
+
+ if ($description->find('div.text.pb-3', 0)->children(1)->class != 'date') {
+ $content .= '<p>' . $description->find('div.text.pb-3', 0)->children(1)->innertext . '</p>';
+ }
+
+ return $content;
+ }
+
+ private function processTwitterImage() {
+ /**
+ * Extract video ID + type from twitter image
+ *
+ * Example (wrapped):
+ * https://img.brut.media/thumbnail/
+ * the-life-of-rita-moreno-2cce75b5-d448-44d2-a97c-ca50d6470dd4-square.jpg
+ * ?ts=1559337892
+ */
+ $fpath = parse_url($this->videoImage, PHP_URL_PATH);
+ $fname = basename($fpath);
+ $fname = substr($fname, 0, strrpos($fname, '.'));
+ $parts = explode('-', $fname);
+
+ if (end($parts) === 'auto') {
+ $key = array_search('auto', $parts);
+ unset($parts[$key]);
+ }
+
+ $this->videoId = implode('-', array_splice($parts, -6, 5));
+ $this->videoType = end($parts);
+ }
+}
diff --git a/bridges/BundesbankBridge.php b/bridges/BundesbankBridge.php
index d21f22b..b64a642 100644
--- a/bridges/BundesbankBridge.php
+++ b/bridges/BundesbankBridge.php
@@ -17,7 +17,6 @@ class BundesbankBridge extends BridgeAbstract {
self::PARAM_LANG => array(
'name' => 'Language',
'type' => 'list',
- 'required' => true,
'defaultValue' => self::LANG_DE,
'values' => array(
'English' => self::LANG_EN,
diff --git a/bridges/CNETFranceBridge.php b/bridges/CNETFranceBridge.php
new file mode 100644
index 0000000..222c8b9
--- /dev/null
+++ b/bridges/CNETFranceBridge.php
@@ -0,0 +1,63 @@
+<?php
+class CNETFranceBridge extends FeedExpander
+{
+ const MAINTAINER = 'leomaradan';
+ const NAME = 'CNET France';
+ const URI = 'https://www.cnetfrance.fr/';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'CNET France RSS with filters';
+ const PARAMETERS = array(
+ 'filters' => array(
+ 'title' => array(
+ 'name' => 'Exclude by title',
+ 'required' => false,
+ 'title' => 'Title term, separated by semicolon (;)',
+ 'defaultValue' => 'bon plan;bons plans;au meilleur prix;des meilleures offres;Amazon Prime Day;RED by SFR ou B&You'
+ ),
+ 'url' => array(
+ 'name' => 'Exclude by url',
+ 'required' => false,
+ 'title' => 'URL term, separated by semicolon (;)',
+ 'defaultValue' => 'bon-plan;bons-plans'
+ )
+ )
+ );
+
+ private $bannedTitle = [];
+ private $bannedURL = [];
+
+ public function collectData()
+ {
+ $title = $this->getInput('title');
+ $url = $this->getInput('url');
+
+ if ($title !== null) {
+ $this->bannedTitle = explode(';', $title);
+ }
+
+ if ($url !== null) {
+ $this->bannedURL = explode(';', $url);
+ }
+
+ $this->collectExpandableDatas('https://www.cnetfrance.fr/feeds/rss/news/');
+ }
+
+ protected function parseItem($feedItem)
+ {
+ $item = parent::parseItem($feedItem);
+
+ foreach ($this->bannedTitle as $term) {
+ if (preg_match('/' . $term . '/mi', $item['title']) === 1) {
+ return null;
+ }
+ }
+
+ foreach ($this->bannedURL as $term) {
+ if (preg_match('/' . $term . '/mi', $item['uri']) === 1) {
+ return null;
+ }
+ }
+
+ return $item;
+ }
+}
diff --git a/bridges/CachetBridge.php b/bridges/CachetBridge.php
new file mode 100644
index 0000000..a60b8f7
--- /dev/null
+++ b/bridges/CachetBridge.php
@@ -0,0 +1,134 @@
+<?php
+
+class CachetBridge extends BridgeAbstract {
+ const NAME = 'Cachet Bridge';
+ const URI = 'https://cachethq.io/';
+ const DESCRIPTION = 'Returns status updates from any Cachet installation';
+ const MAINTAINER = 'klimplant';
+ const PARAMETERS = array(
+ array(
+ 'host' => array(
+ 'name' => 'Cachet installation',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'The URL of the Cachet installation',
+ 'exampleValue' => 'https://demo.cachethq.io/',
+ ), 'additional_info' => array(
+ 'name' => 'Additional Timestamps',
+ 'type' => 'checkbox',
+ 'title' => 'Whether to include the given timestamps'
+ )
+ )
+ );
+ const CACHE_TIMEOUT = 300;
+
+ private $componentCache = [];
+
+ public function getURI() {
+ return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host');
+ }
+
+ /**
+ * Validates the ping request to the cache API
+ *
+ * @param string $ping
+ * @return boolean
+ */
+ private function validatePing($ping) {
+ $ping = json_decode($ping);
+ if ($ping === null) {
+ return false;
+ }
+ return $ping->data === 'Pong!';
+ }
+
+ /**
+ * Returns the component name of a cachat component
+ *
+ * @param integer $id
+ * @return string
+ */
+ private function getComponentName($id) {
+ if ($id === 0) {
+ return '';
+ }
+ if (array_key_exists($id, $this->componentCache)) {
+ return $this->componentCache[$id];
+ }
+
+ $component = getContents($this->getURI() . '/api/v1/components/' . $id);
+ $component = json_decode($component);
+ if ($component === null) {
+ return '';
+ }
+ return $component->data->name;
+ }
+
+ public function collectData() {
+ $ping = getContents(urljoin($this->getURI(), '/api/v1/ping'));
+ if (!$this->validatePing($ping)) {
+ returnClientError('Provided URI is invalid!');
+ }
+
+ $url = urljoin($this->getURI(), '/api/v1/incidents?sort=id&order=desc');
+ $incidents = getContents($url);
+ $incidents = json_decode($incidents);
+ if ($incidents === null) {
+ returnClientError('/api/v1/incidents returned no valid json');
+ }
+
+ usort($incidents->data, function ($a, $b) {
+ $timeA = strtotime($a->updated_at);
+ $timeB = strtotime($b->updated_at);
+ return $timeA > $timeB ? -1 : 1;
+ });
+
+ foreach ($incidents->data as $incident) {
+
+ if (isset($incident->permalink)) {
+ $permalink = $incident->permalink;
+ } else {
+ $permalink = urljoin($this->getURI(), '/incident/' . $incident->id);
+ }
+
+ $title = $incident->human_status . ': ' . $incident->name;
+ $message = '';
+ if ($this->getInput('additional_info')) {
+ if (isset($incident->occurred_at)) {
+ $message .= 'Occurred at: ' . $incident->occurred_at . "\r\n";
+ }
+ if (isset($incident->scheduled_at)) {
+ $message .= 'Scheduled at: ' . $incident->scheduled_at . "\r\n";
+ }
+ if (isset($incident->created_at)) {
+ $message .= 'Created at: ' . $incident->created_at . "\r\n";
+ }
+ if (isset($incident->updated_at)) {
+ $message .= 'Updated at: ' . $incident->updated_at . "\r\n\r\n";
+ }
+ }
+
+ $message .= $incident->message;
+ $content = nl2br($message);
+ $componentName = $this->getComponentName($incident->component_id);
+ $uidOrig = $permalink . $incident->created_at;
+ $uid = hash('sha512', $uidOrig);
+ $timestamp = strtotime($incident->created_at);
+ $categories = [];
+ $categories[] = $incident->human_status;
+ if ($componentName !== '') {
+ $categories[] = $componentName;
+ }
+
+ $item = [];
+ $item['uri'] = $permalink;
+ $item['title'] = $title;
+ $item['timestamp'] = $timestamp;
+ $item['content'] = $content;
+ $item['uid'] = $uid;
+ $item['categories'] = $categories;
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/CastorusBridge.php b/bridges/CastorusBridge.php
index 3ed1331..c394283 100644
--- a/bridges/CastorusBridge.php
+++ b/bridges/CastorusBridge.php
@@ -83,7 +83,7 @@ class CastorusBridge extends BridgeAbstract {
if(!$html)
returnServerError('Could not load data from ' . self::URI . '!');
- $activities = $html->find('div#activite/li');
+ $activities = $html->find('div#activite > li');
if(!$activities)
returnServerError('Failed to find activities!');
diff --git a/bridges/ComboiosDePortugalBridge.php b/bridges/ComboiosDePortugalBridge.php
new file mode 100644
index 0000000..610e23b
--- /dev/null
+++ b/bridges/ComboiosDePortugalBridge.php
@@ -0,0 +1,22 @@
+<?php
+class ComboiosDePortugalBridge extends BridgeAbstract {
+ const NAME = 'CP | Avisos';
+ const BASE_URI = 'https://www.cp.pt';
+ const URI = self::BASE_URI . '/passageiros/pt';
+ const DESCRIPTION = 'Comboios de Portugal | Avisos';
+ const MAINTAINER = 'somini';
+
+ public function collectData() {
+ $html = getSimpleHTMLDOM($this->getURI() . '/consultar-horarios/avisos')
+ or returnServerError('Could not load content');
+
+ foreach($html->find('.warnings-table a') as $element) {
+ $item = array();
+
+ $item['title'] = $element->innertext;
+ $item['uri'] = self::BASE_URI . implode('/', array_map('urlencode', explode('/', $element->href)));
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/ContainerLinuxReleasesBridge.php b/bridges/ContainerLinuxReleasesBridge.php
index ae43888..d2f6325 100644
--- a/bridges/ContainerLinuxReleasesBridge.php
+++ b/bridges/ContainerLinuxReleasesBridge.php
@@ -15,7 +15,6 @@ class ContainerLinuxReleasesBridge extends BridgeAbstract {
'channel' => [
'name' => 'Release Channel',
'type' => 'list',
- 'required' => true,
'defaultValue' => self::STABLE,
'values' => [
'Stable' => self::STABLE,
diff --git a/bridges/CourrierInternationalBridge.php b/bridges/CourrierInternationalBridge.php
index 1e7c93e..1b754e3 100644
--- a/bridges/CourrierInternationalBridge.php
+++ b/bridges/CourrierInternationalBridge.php
@@ -3,7 +3,7 @@ class CourrierInternationalBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'Courrier International Bridge';
- const URI = 'http://CourrierInternational.com/';
+ const URI = 'https://www.courrierinternational.com/';
const CACHE_TIMEOUT = 300; // 5 min
const DESCRIPTION = 'Courrier International bridge';
diff --git a/bridges/CuriousCatBridge.php b/bridges/CuriousCatBridge.php
new file mode 100644
index 0000000..0ebc8bd
--- /dev/null
+++ b/bridges/CuriousCatBridge.php
@@ -0,0 +1,109 @@
+<?php
+class CuriousCatBridge extends BridgeAbstract {
+ const NAME = 'Curious Cat Bridge';
+ const URI = 'https://curiouscat.me';
+ const DESCRIPTION = 'Returns list of newest questions and answers for a user profile';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = array(array(
+ 'username' => array(
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'koethekoethe',
+ )
+ ));
+
+ const CACHE_TIMEOUT = 3600;
+
+ public function collectData() {
+
+ $url = self::URI . '/api/v2/profile?username=' . urlencode($this->getInput('username'));
+
+ $apiJson = getContents($url)
+ or returnServerError('Could not request: ' . $url);
+
+ $apiData = json_decode($apiJson, true);
+
+ foreach($apiData['posts'] as $post) {
+ $item = array();
+
+ $item['author'] = 'Anonymous';
+
+ if ($post['senderData']['id'] !== false) {
+ $item['author'] = $post['senderData']['username'];
+ }
+
+ $item['uri'] = $this->getURI() . '/post/' . $post['id'];
+ $item['title'] = $this->ellipsisTitle($post['comment']);
+
+ $item['content'] = $this->processContent($post);
+ $item['timestamp'] = $post['timestamp'];
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI() {
+
+ if (!is_null($this->getInput('username'))) {
+ return self::URI . '/' . $this->getInput('username');
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName() {
+
+ if (!is_null($this->getInput('username'))) {
+ return $this->getInput('username') . ' - Curious Cat';
+ }
+
+ return parent::getName();
+ }
+
+ private function processContent($post) {
+
+ $author = 'Anonymous';
+
+ if ($post['senderData']['id'] !== false) {
+ $authorUrl = self::URI . '/' . $post['senderData']['username'];
+
+ $author = <<<EOD
+<a href="{$authorUrl}">{$post['senderData']['username']}</a>
+EOD;
+ }
+
+ $question = $this->formatUrls($post['comment']);
+ $answer = $this->formatUrls($post['reply']);
+
+ $content = <<<EOD
+<p>{$author} asked:</p>
+<blockquote>{$question}</blockquote><br/>
+<p>{$post['addresseeData']['username']} answered:</p>
+<blockquote>{$answer}</blockquote>
+EOD;
+
+ return $content;
+ }
+
+ private function ellipsisTitle($text) {
+ $length = 150;
+
+ if (strlen($text) > $length) {
+ $text = explode('<br>', wordwrap($text, $length, '<br>'));
+ return $text[0] . '...';
+ }
+
+ return $text;
+ }
+
+ private function formatUrls($content) {
+
+ return preg_replace(
+ '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims',
+ '<a target="_blank" href="$1" target="_blank">$1</a> ',
+ $content
+ );
+
+ }
+}
diff --git a/bridges/DailymotionBridge.php b/bridges/DailymotionBridge.php
index ff8d482..dc4f5d3 100644
--- a/bridges/DailymotionBridge.php
+++ b/bridges/DailymotionBridge.php
@@ -4,7 +4,7 @@ class DailymotionBridge extends BridgeAbstract {
const MAINTAINER = 'mitsukarenai';
const NAME = 'Dailymotion Bridge';
const URI = 'https://www.dailymotion.com/';
- const CACHE_TIMEOUT = 10800; // 3h
+ const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns the 5 newest videos by username/playlist or search';
const PARAMETERS = array (
@@ -27,74 +27,99 @@ class DailymotionBridge extends BridgeAbstract {
),
'pa' => array(
'name' => 'Page',
- 'type' => 'number'
+ 'type' => 'number',
+ 'defaultValue' => 1,
)
)
);
- protected function getMetadata($id){
- $metadata = array();
- $html2 = getSimpleHTMLDOM(self::URI . 'video/' . $id);
- if(!$html2) {
- return $metadata;
- }
+ private $feedName = '';
- $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;
- }
+ private $apiUrl = 'https://api.dailymotion.com';
+ private $apiFields = 'created_time,description,id,owner.screenname,tags,thumbnail_url,title,url';
public function getIcon() {
return 'https://static1-ssl.dmcdn.net/images/neon/favicons/android-icon-36x36.png.vf806ca4ed0deed812';
}
- public function collectData(){
- $html = '';
- $limit = 5;
- $count = 0;
+ public function collectData() {
+
+ if ($this->queriedContext === 'By username' || $this->queriedContext === 'By playlist id') {
+
+ $apiJson = getContents($this->getApiUrl())
+ or returnServerError('Could not request: ' . $this->getApiUrl());
+
+ $apiData = json_decode($apiJson, true);
+
+ $this->feedName = $this->getPlaylistTitle($this->getInput('p'));
+
+ foreach ($apiData['list'] as $apiItem) {
+ $item = array();
+
+ $item['uri'] = $apiItem['url'];
+ $item['uid'] = $apiItem['id'];
+ $item['title'] = $apiItem['title'];
+ $item['timestamp'] = $apiItem['created_time'];
+ $item['author'] = $apiItem['owner.screenname'];
+ $item['content'] = '<p><a href="' . $apiItem['url'] . '">
+ <img src="' . $apiItem['thumbnail_url'] . '"></a></p><p>' . $apiItem['description'] . '</p>';
+ $item['categories'] = $apiItem['tags'];
+ $item['enclosures'][] = $apiItem['thumbnail_url'];
- $html = getSimpleHTMLDOM($this->getURI())
- or returnServerError('Could not request Dailymotion.');
+ $this->items[] = $item;
+ }
+ }
- foreach($html->find('div.media a.preview_link') as $element) {
- if($count < $limit) {
+ if ($this->queriedContext === 'From search results') {
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request Dailymotion.');
+
+ foreach($html->find('div.media a.preview_link') as $element) {
$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>';
+ . $item['uri']
+ . '"><img src="'
+ . $metadata['thumbnailUri']
+ . '" /></a><br><a href="'
+ . $item['uri']
+ . '">'
+ . $item['title']
+ . '</a>';
$this->items[] = $item;
- $count++;
+
+ if (count($this->items) >= 5) {
+ break;
+ }
}
}
}
- public function getName(){
+ public function getName() {
switch($this->queriedContext) {
case 'By username':
$specific = $this->getInput('u');
break;
case 'By playlist id':
$specific = strtok($this->getInput('p'), '_');
+
+ if ($this->feedName) {
+ $specific = $this->feedName;
+ }
+
break;
case 'From search results':
$specific = $this->getInput('s');
@@ -102,26 +127,77 @@ class DailymotionBridge extends BridgeAbstract {
default: return parent::getName();
}
- return $specific . ' : Dailymotion Bridge';
+ return $specific . ' : Dailymotion';
}
public function getURI(){
$uri = self::URI;
switch($this->queriedContext) {
case 'By username':
- $uri .= 'user/' . urlencode($this->getInput('u')) . '/1';
+ $uri .= 'user/' . urlencode($this->getInput('u'));
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');
+
+ if(!is_null($this->getInput('pa'))) {
+ $pa = $this->getInput('pa');
+
+ if ($this->getInput('pa') < 1) {
+ $pa = 1;
+ }
+
+ $uri .= '/' . $pa;
}
break;
default: return parent::getURI();
}
return $uri;
}
+
+ private function getMetadata($id) {
+ $metadata = array();
+
+ $html = getSimpleHTMLDOM(self::URI . 'video/' . $id);
+
+ if(!$html) {
+ return $metadata;
+ }
+
+ $metadata['title'] = $html->find('meta[property=og:title]', 0)->getAttribute('content');
+ $metadata['timestamp'] = strtotime(
+ $html->find('meta[property=video:release_date]', 0)->getAttribute('content')
+ );
+ $metadata['thumbnailUri'] = $html->find('meta[property=og:image]', 0)->getAttribute('content');
+ $metadata['uri'] = $html->find('meta[property=og:url]', 0)->getAttribute('content');
+ return $metadata;
+ }
+
+ private function getPlaylistTitle($id) {
+ $title = '';
+
+ $url = self::URI . 'playlist/' . $id;
+
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Could not request: ' . $url);
+
+ $title = $html->find('meta[property=og:title]', 0)->getAttribute('content');
+ return $title;
+ }
+
+ private function getApiUrl() {
+
+ switch($this->queriedContext) {
+ case 'By username':
+ return $this->apiUrl . '/user/' . $this->getInput('u')
+ . '/videos?fields=' . urlencode($this->apiFields) . '&availability=1&sort=recent&limit=5';
+ break;
+ case 'By playlist id':
+ return $this->apiUrl . '/playlist/' . $this->getInput('p')
+ . '/videos?fields=' . urlencode($this->apiFields) . '&limit=5';
+ break;
+ }
+ }
}
diff --git a/bridges/DanbooruBridge.php b/bridges/DanbooruBridge.php
index 755399f..ea4b2be 100644
--- a/bridges/DanbooruBridge.php
+++ b/bridges/DanbooruBridge.php
@@ -40,7 +40,7 @@ class DanbooruBridge extends BridgeAbstract {
defaultLinkTo($element, $this->getURI());
$item = array();
- $item['uri'] = $element->find('a', 0)->href;
+ $item['uri'] = html_entity_decode($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;
diff --git a/bridges/DavesTrailerPageBridge.php b/bridges/DavesTrailerPageBridge.php
new file mode 100644
index 0000000..90afec4
--- /dev/null
+++ b/bridges/DavesTrailerPageBridge.php
@@ -0,0 +1,27 @@
+<?php
+class DavesTrailerPageBridge extends BridgeAbstract {
+ const MAINTAINER = 'johnnygroovy';
+ const NAME = 'Daves Trailer Page Bridge';
+ const URI = 'https://www.davestrailerpage.co.uk/';
+ const DESCRIPTION = 'Last trailers in HD thanks to Dave.';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(static::URI)
+ or returnClientError('No results for this query.');
+
+ foreach ($html->find('tr[!align]') as $tr) {
+ $item = array();
+
+ // title
+ $item['title'] = $tr->find('td', 0)->find('b', 0)->plaintext;
+
+ // content
+ $item['content'] = $tr->find('ul', 1);
+
+ // uri
+ $item['uri'] = $tr->find('a', 3)->getAttribute('href');
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/DealabsBridge.php b/bridges/DealabsBridge.php
index 89183ed..1657b8b 100644
--- a/bridges/DealabsBridge.php
+++ b/bridges/DealabsBridge.php
@@ -15,13 +15,11 @@ class DealabsBridge extends PepperBridgeAbstract {
'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',
@@ -41,7 +39,6 @@ class DealabsBridge extends PepperBridgeAbstract {
'group' => array(
'name' => 'Groupe',
'type' => 'list',
- 'required' => true,
'title' => 'Groupe dont il faut afficher les deals',
'values' => array(
'Abonnements internet' => 'abonnements-internet',
@@ -957,7 +954,6 @@ class DealabsBridge extends PepperBridgeAbstract {
'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' => '',
@@ -1149,7 +1145,7 @@ class PepperBridgeAbstract extends BridgeAbstract {
} else {
foreach ($list as $deal) {
$item = array();
- $item['uri'] = $deal->find('div[class=threadGrid-title]', 0)->find('a', 0)->href;
+ $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;
@@ -1380,8 +1376,11 @@ class PepperBridgeAbstract extends BridgeAbstract {
// Add the Hour and minutes
$date_str .= ' 00:00';
-
$date = DateTime::createFromFormat('j F Y H:i', $date_str);
+ // In some case, the date is not recognized : as a workaround the actual date is taken
+ if($date === false) {
+ $date = new DateTime();
+ }
return $date->getTimestamp();
}
diff --git a/bridges/DemoBridge.php b/bridges/DemoBridge.php
deleted file mode 100644
index f48b451..0000000
--- a/bridges/DemoBridge.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?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
deleted file mode 100644
index 842b421..0000000
--- a/bridges/DemonoidBridge.php
+++ /dev/null
@@ -1,169 +0,0 @@
-<?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(
- 'Keywords' => 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
- )
- )
- ),
- 'Category Only' => 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
- )
- )
- ),
- 'User ID' => 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/DesoutterBridge.php b/bridges/DesoutterBridge.php
index 14e26c2..0aae41a 100644
--- a/bridges/DesoutterBridge.php
+++ b/bridges/DesoutterBridge.php
@@ -15,7 +15,6 @@ class DesoutterBridge extends BridgeAbstract {
'news_lang' => array(
'name' => 'Language',
'type' => 'list',
- 'required' => true,
'title' => 'Select your language',
'defaultValue' => 'Corporate',
'values' => array(
@@ -66,7 +65,6 @@ class DesoutterBridge extends BridgeAbstract {
'industry_lang' => array(
'name' => 'Language',
'type' => 'list',
- 'required' => true,
'title' => 'Select your language',
'defaultValue' => 'Corporate',
'values' => array(
@@ -117,7 +115,6 @@ class DesoutterBridge extends BridgeAbstract {
'full' => array(
'name' => 'Load full articles',
'type' => 'checkbox',
- 'required' => false,
'title' => 'Enable to load the full article for each item'
)
)
@@ -162,13 +159,13 @@ class DesoutterBridge extends BridgeAbstract {
foreach($html->find('article') as $article) {
$item = array();
- $item['uri'] = $article->find('[itemprop="name"]', 0)->href;
- $item['title'] = $article->find('[itemprop="name"]', 0)->title;
+ $item['uri'] = $article->find('a', 0)->href;
+ $item['title'] = $article->find('a[title]', 0)->title;
if($this->getInput('full')) {
$item['content'] = $this->getFullNewsArticle($item['uri']);
} else {
- $item['content'] = $article->find('[itemprop="description"]', 0)->plaintext;
+ $item['content'] = $article->find('div.tile-body p', 0)->plaintext;
}
$this->items[] = $item;
diff --git a/bridges/DollbooruBridge.php b/bridges/DollbooruBridge.php
deleted file mode 100644
index 5ed4119..0000000
--- a/bridges/DollbooruBridge.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?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/EconomistBridge.php b/bridges/EconomistBridge.php
new file mode 100644
index 0000000..1256be4
--- /dev/null
+++ b/bridges/EconomistBridge.php
@@ -0,0 +1,63 @@
+<?php
+class EconomistBridge extends BridgeAbstract {
+ const NAME = 'The Economist: Latest Updates';
+ const URI = 'https://www.economist.com';
+ const DESCRIPTION = 'Fetches the latest updates from the Economist.';
+ const MAINTAINER = 'thefranke';
+ const CACHE_TIMEOUT = 3600; // 1h
+
+ public function getIcon() {
+ return 'https://www.economist.com/sites/default/files/econfinal_favicon.ico';
+ }
+
+ public function collectData() {
+ $html = getSimpleHTMLDOM(self::URI . '/latest/')
+ or returnServerError('Could not fetch latest updates form The Economist.');
+
+ foreach($html->find('article') as $element) {
+
+ $a = $element->find('a', 0);
+ $href = self::URI . $a->href;
+ $full = getSimpleHTMLDOMCached($href);
+ $article = $full->find('article', 0);
+
+ $header = $article->find('h1', 0);
+ $author = $article->find('span[itemprop="author"]', 0);
+ $time = $article->find('time[itemprop="dateCreated"]', 0);
+ $content = $article->find('div[itemprop="description"]', 0);
+
+ // Remove newsletter subscription box
+ $newsletter = $content->find('div[class="newsletter-form__message"]', 0);
+ if ($newsletter)
+ $newsletter->outertext = '';
+
+ $newsletterForm = $content->find('form', 0);
+ if ($newsletterForm)
+ $newsletterForm->outertext = '';
+
+ // Remove next and previous article URLs at the bottom
+ $nextprev = $content->find('div[class="blog-post__next-previous-wrapper"]', 0);
+ if ($nextprev)
+ $nextprev->outertext = '';
+
+ $section = [ $article->find('h3[itemprop="articleSection"]', 0)->plaintext ];
+
+ $item = array();
+ $item['title'] = $header->find('span', 0)->innertext . ': '
+ . $header->find('span', 1)->innertext;
+
+ $item['uri'] = $href;
+ $item['timestamp'] = strtotime($time->datetime);
+ $item['author'] = $author->innertext;
+ $item['categories'] = $section;
+
+ $item['content'] = '<img style="max-width: 100%" src="'
+ . $a->find('img', 0)->src . '">' . $content->innertext;
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10)
+ break;
+ }
+ }
+}
diff --git a/bridges/EliteDangerousGalnetBridge.php b/bridges/EliteDangerousGalnetBridge.php
index dc6077b..1afa042 100644
--- a/bridges/EliteDangerousGalnetBridge.php
+++ b/bridges/EliteDangerousGalnetBridge.php
@@ -47,5 +47,8 @@ class EliteDangerousGalnetBridge extends BridgeAbstract {
$this->items[] = $item;
}
+
+ //Remove duplicates that sometimes show up on the website
+ $this->items = array_unique($this->items, SORT_REGULAR);
}
}
diff --git a/bridges/ElloBridge.php b/bridges/ElloBridge.php
index 22f5d30..3de167e 100644
--- a/bridges/ElloBridge.php
+++ b/bridges/ElloBridge.php
@@ -120,9 +120,11 @@ class ElloBridge extends BridgeAbstract {
}
private function getAPIKey() {
- $cache = Cache::create('FileCache');
- $cache->setPath(PATH_CACHE);
- $cache->setParameters(['key']);
+ $cacheFac = new CacheFactory();
+ $cacheFac->setWorkingDir(PATH_LIB_CACHES);
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope(get_called_class());
+ $cache->setKey(['key']);
$key = $cache->loadData();
if($key == null) {
diff --git a/bridges/EngadgetBridge.php b/bridges/EngadgetBridge.php
new file mode 100644
index 0000000..cf200fa
--- /dev/null
+++ b/bridges/EngadgetBridge.php
@@ -0,0 +1,26 @@
+<?php
+class EngadgetBridge extends FeedExpander {
+
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'Engadget Bridge';
+ const URI = 'https://www.engadget.com/';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'Article content for Engadget.';
+
+ public function collectData(){
+ $this->collectExpandableDatas(static::URI . 'rss.xml', 15);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+ // figure contain's the main article image
+ $article = $articlePage->find('figure', 0);
+ // .article-text has the actual article
+ foreach($articlePage->find('.article-text') as $element)
+ $article = $article . $element;
+ $item['content'] = $article;
+ return $item;
+ }
+}
diff --git a/bridges/ExtremeDownloadBridge.php b/bridges/ExtremeDownloadBridge.php
index 5272997..acdf630 100644
--- a/bridges/ExtremeDownloadBridge.php
+++ b/bridges/ExtremeDownloadBridge.php
@@ -15,7 +15,6 @@ class ExtremeDownloadBridge extends BridgeAbstract {
'filter' => array(
'name' => 'Type de contenu',
'type' => 'list',
- 'required' => true,
'title' => 'Type de contenu à suivre : Téléchargement, Streaming ou les deux',
'values' => array(
'Streaming et Téléchargement' => 'both',
diff --git a/bridges/FB2Bridge.php b/bridges/FB2Bridge.php
index 29df755..2faa321 100644
--- a/bridges/FB2Bridge.php
+++ b/bridges/FB2Bridge.php
@@ -72,15 +72,15 @@ class FB2Bridge extends BridgeAbstract {
$pageInfo = $this->getPageInfos($page, $cookies);
if($pageInfo['userId'] === null) {
- echo <<<EOD
+ returnClientError(<<<EOD
Unable to get the page id. You should consider getting the ID by hand, then importing it into FB2Bridge
-EOD;
- die();
+EOD
+ );
} elseif($pageInfo['userId'] == -1) {
- echo <<<EOD
+ returnClientError(<<<EOD
This page is not accessible without being logged in.
-EOD;
- die();
+EOD
+ );
}
}
@@ -95,7 +95,7 @@ EOD;
foreach($html->find('article') as $content) {
$item = array();
- //echo $content; die();
+
preg_match('/publish_time\\\":([0-9]+),/', $content->getAttribute('data-store', 0), $match);
if(isset($match[1]))
$timestamp = $match[1];
diff --git a/bridges/FDroidBridge.php b/bridges/FDroidBridge.php
index b606cec..7f54735 100644
--- a/bridges/FDroidBridge.php
+++ b/bridges/FDroidBridge.php
@@ -11,7 +11,6 @@ class FDroidBridge extends BridgeAbstract {
'u' => array(
'name' => 'Widget selection',
'type' => 'list',
- 'required' => true,
'values' => array(
'Latest added apps' => 'added',
'Latest updated apps' => 'updated'
@@ -29,14 +28,14 @@ class FDroidBridge extends BridgeAbstract {
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
+ // "updated" is the 5th widget on the page, "added" is the 6th
switch($this->getInput('u')) {
case 'updated':
- $html_widget = $html->find('div.sidebar-widget', 4);
+ $html_widget = $html->find('div.sidebar-widget', 5);
break;
default:
- $html_widget = $html->find('div.sidebar-widget', 5);
+ $html_widget = $html->find('div.sidebar-widget', 6);
break;
}
diff --git a/bridges/FabriceBellardBridge.php b/bridges/FabriceBellardBridge.php
new file mode 100644
index 0000000..2c24b5e
--- /dev/null
+++ b/bridges/FabriceBellardBridge.php
@@ -0,0 +1,36 @@
+<?php
+class FabriceBellardBridge extends BridgeAbstract {
+ const NAME = 'Fabrice Bellard';
+ const URI = 'https://bellard.org/';
+ const DESCRIPTION = "Fabrice Bellard's Home Page";
+ const MAINTAINER = 'somini';
+
+ public function collectData() {
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not load content');
+
+ foreach ($html->find('p') as $obj) {
+ $item = array();
+
+ $html = defaultLinkTo($html, $this->getURI());
+
+ $links = $obj->find('a');
+ if (count($links) > 0) {
+ $link_uri = $links[0]->href;
+ } else {
+ $link_uri = $this->getURI();
+ }
+
+ /* try to make sure the link is valid */
+ if ($link_uri[-1] !== '/' && strpos($link_uri, '/') === false) {
+ $link_uri = $link_uri . '/';
+ }
+
+ $item['title'] = strip_tags($obj->innertext);
+ $item['uri'] = $link_uri;
+ $item['content'] = $obj->innertext;
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/FacebookBridge.php b/bridges/FacebookBridge.php
index 7b61705..08b3a38 100644
--- a/bridges/FacebookBridge.php
+++ b/bridges/FacebookBridge.php
@@ -142,7 +142,11 @@ class FacebookBridge extends BridgeAbstract {
private function collectGroupData() {
- $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n");
+ if(getEnv('HTTP_ACCEPT_LANGUAGE')) {
+ $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE'));
+ } else {
+ $header = array();
+ }
$html = getSimpleHTMLDOM($this->getURI(), $header)
or returnServerError('Failed loading facebook page: ' . $this->getURI());
@@ -219,8 +223,7 @@ class FacebookBridge extends BridgeAbstract {
$ogtitle = $html->find('meta[property="og:title"]', 0)
or returnServerError('Unable to find group title!');
- return htmlspecialchars_decode($ogtitle->content, ENT_QUOTES);
-
+ return html_entity_decode($ogtitle->content, ENT_QUOTES);
}
private function extractGroupURI($post) {
@@ -506,7 +509,11 @@ EOD;
// Retrieve page contents
if(is_null($html)) {
- $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE'));
+ if(getEnv('HTTP_ACCEPT_LANGUAGE')) {
+ $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE'));
+ } else {
+ $header = array();
+ }
$html = getSimpleHTMLDOM($this->getURI(), $header)
or returnServerError('No results for this query.');
@@ -581,6 +588,8 @@ EOD;
'._5mly', // Remove embedded videos (the preview image remains)
'._2ezg', // Remove "Views ..."
'.hidden_elem', // Remove hidden elements (they are hidden anyway)
+ '.timestampContent', // Remove relative timestamp
+ '._6spk', // Remove redundant separator
);
foreach($content_filters as $filter) {
diff --git a/bridges/FeedExpanderExampleBridge.php b/bridges/FeedExpanderExampleBridge.php
deleted file mode 100644
index 537a635..0000000
--- a/bridges/FeedExpanderExampleBridge.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-class FeedExpanderExampleBridge extends FeedExpander {
-
- const MAINTAINER = 'logmanoriginal';
- const NAME = 'FeedExpander Example';
- const URI = 'http://github.com/RSS-Bridge/rss-bridge/';
- 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/FicbookBridge.php b/bridges/FicbookBridge.php
new file mode 100644
index 0000000..8b8a57f
--- /dev/null
+++ b/bridges/FicbookBridge.php
@@ -0,0 +1,164 @@
+<?php
+class FicbookBridge extends BridgeAbstract {
+
+ const NAME = 'Ficbook Bridge';
+ const URI = 'https://ficbook.net/';
+ const DESCRIPTION = 'No description provided';
+ const MAINTAINER = 'logmanoriginal';
+
+ const PARAMETERS = array(
+ 'Site News' => array(),
+ 'Fiction Updates' => array(
+ 'fiction_id' => array(
+ 'name' => 'Fanfiction ID',
+ 'type' => 'text',
+ 'pattern' => '[0-9]+',
+ 'required' => true,
+ 'title' => 'Insert fanfiction ID',
+ 'exampleValue' => '5783919',
+ ),
+ 'include_contents' => array(
+ 'name' => 'Include contents',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to include contents in the feed',
+ ),
+ ),
+ 'Fiction Comments' => array(
+ 'fiction_id' => array(
+ 'name' => 'Fanfiction ID',
+ 'type' => 'text',
+ 'pattern' => '[0-9]+',
+ 'required' => true,
+ 'title' => 'Insert fanfiction ID',
+ 'exampleValue' => '5783919',
+ ),
+ ),
+ );
+
+ public function getURI() {
+ switch($this->queriedContext) {
+ case 'Site News': {
+ // For some reason this is not HTTPS
+ return 'http://ficbook.net/sitenews';
+ }
+ case 'Fiction Updates': {
+ return self::URI
+ . 'readfic/'
+ . urlencode($this->getInput('fiction_id'));
+ }
+ case 'Fiction Comments': {
+ return self::URI
+ . 'readfic/'
+ . urlencode($this->getInput('fiction_id'))
+ . '/comments#content';
+ }
+ default: return parent::getURI();
+ }
+ }
+
+ public function collectData() {
+
+ $header = array('Accept-Language: en-US');
+
+ $html = getSimpleHTMLDOM($this->getURI(), $header)
+ or returnServerError('Could not request ' . $this->getURI());
+
+ $html = defaultLinkTo($html, self::URI);
+
+ switch($this->queriedContext) {
+ case 'Site News': return $this->collectSiteNews($html);
+ case 'Fiction Updates': return $this->collectUpdatesData($html);
+ case 'Fiction Comments': return $this->collectCommentsData($html);
+ }
+
+ }
+
+ private function collectSiteNews($html) {
+ foreach($html->find('.news_view') as $news) {
+ $this->items[] = array(
+ 'title' => $news->find('h1.title', 0)->plaintext,
+ 'timestamp' => strtotime($this->fixDate($news->find('span[title]', 0)->title)),
+ 'content' => $news->find('.news_text', 0),
+ );
+ }
+ }
+
+ private function collectCommentsData($html) {
+ foreach($html->find('article.post') as $article) {
+ $this->items[] = array(
+ 'uri' => $article->find('.comment_link_to_fic > a', 0)->href,
+ 'title' => $article->find('.comment_author', 0)->plaintext,
+ 'author' => $article->find('.comment_author', 0)->plaintext,
+ 'timestamp' => strtotime($this->fixDate($article->find('time[datetime]', 0)->datetime)),
+ 'content' => $article->find('.comment_message', 0),
+ 'enclosures' => array($article->find('img', 0)->src),
+ );
+ }
+ }
+
+ private function collectUpdatesData($html) {
+ foreach($html->find('ul.table-of-contents > li') as $chapter) {
+ $item = array(
+ 'uri' => $chapter->find('a', 0)->href,
+ 'title' => $chapter->find('a', 0)->plaintext,
+ 'timestamp' => strtotime($this->fixDate($chapter->find('span[title]', 0)->title)),
+ );
+
+ if($this->getInput('include_contents')) {
+ $content = getSimpleHTMLDOMCached($item['uri']);
+ $item['content'] = $content->find('#content', 0);
+ }
+
+ $this->items[] = $item;
+
+ // Sort by time, descending
+ usort($this->items, function($a, $b){ return $b['timestamp'] - $a['timestamp']; });
+ }
+ }
+
+ private function fixDate($date) {
+
+ // FIXME: This list was generated using Google tranlator. Someone who
+ // actually knows russian should check this list! Please keep in mind
+ // that month names must match exactly the names returned by Ficbook.
+ $ru_month = array(
+ 'января',
+ 'февраля',
+ 'марта',
+ 'апреля',
+ 'мая',
+ 'июня',
+ 'июля',
+ 'августа',
+ 'Сентября',
+ 'октября',
+ 'Ноября',
+ 'Декабря',
+ );
+
+ $en_month = array(
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+ );
+
+ $fixed_date = str_replace($ru_month, $en_month, $date);
+
+ if($fixed_date === $date) {
+ Debug::log('Unable to fix date: ' . $date);
+ return null;
+ }
+
+ return $fixed_date;
+
+ }
+}
diff --git a/bridges/FindACrewBridge.php b/bridges/FindACrewBridge.php
index c245c84..abab6e1 100644
--- a/bridges/FindACrewBridge.php
+++ b/bridges/FindACrewBridge.php
@@ -62,11 +62,16 @@ class FindACrewBridge extends BridgeAbstract {
foreach ($annonces as $annonce) {
$item = array();
- $img = parent::getURI() . $annonce->find('.css_LstPic img', 0)->getAttribute('src');
- $item['title'] = $annonce->find('.css_LstCtrls span', 0)->plaintext;
- $item['uri'] = parent::getURI() . $annonce->find('.css_PnlCtrls a', 0)->href;
- $content = $annonce->find('.css_LstDtl div', 2)->innertext;
- $item['content'] = "<img src='$img' /><br>$content";
+ $link = parent::getURI() . $annonce->find('.lst-ctrls a', 0)->href;
+ $htmlDetail = getSimpleHTMLDOMCached($link . '?mdl=2'); // add ?mdl=2 for xhr content not full html page
+
+ $img = parent::getURI() . $htmlDetail->find('img.img-responsive', 0)->getAttribute('src');
+ $item['title'] = $annonce->find('.lst-tags span', 0)->plaintext;
+ $item['uri'] = $link;
+ $content = $htmlDetail->find('.panel-body div.clearfix.row > div', 1)->innertext;
+ $content .= $htmlDetail->find('.panel-body > div', 1)->innertext;
+ $content = defaultLinkTo($content, parent::getURI());
+ $item['content'] = $content;
$item['enclosures'] = array($img);
$item['categories'] = array($annonce->find('.css_AccLocCur', 0)->plaintext);
$this->items[] = $item;
diff --git a/bridges/FurAffinityBridge.php b/bridges/FurAffinityBridge.php
new file mode 100644
index 0000000..2f78ee4
--- /dev/null
+++ b/bridges/FurAffinityBridge.php
@@ -0,0 +1,918 @@
+<?php
+class FurAffinityBridge extends BridgeAbstract {
+ const NAME = 'FurAffinity Bridge';
+ const URI = 'https://www.furaffinity.net';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns posts from various sections of FurAffinity';
+ const MAINTAINER = 'Roliga';
+ const PARAMETERS = array(
+ 'Search' => array(
+ 'q' => array(
+ 'name' => 'Query',
+ 'required' => true
+ ),
+ 'rating-general' => array(
+ 'name' => 'General',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'rating-mature' => array(
+ 'name' => 'Mature',
+ 'type' => 'checkbox',
+ ),
+ 'rating-adult' => array(
+ 'name' => 'Adult',
+ 'type' => 'checkbox',
+ ),
+ 'range' => array(
+ 'name' => 'Time range',
+ 'type' => 'list',
+ 'values' => array(
+ 'A Day' => 'day',
+ '3 Days' => '3days',
+ 'A Week' => 'week',
+ 'A Month' => 'month',
+ 'All time' => 'all'
+ ),
+ 'defaultValue' => 'all'
+ ),
+ 'type-art' => array(
+ 'name' => 'Art',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'type-flash' => array(
+ 'name' => 'Flash',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'type-photo' => array(
+ 'name' => 'Photography',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'type-music' => array(
+ 'name' => 'Music',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'type-story' => array(
+ 'name' => 'Story',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'type-poetry' => array(
+ 'name' => 'Poetry',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'mode' => array(
+ 'name' => 'Match mode',
+ 'type' => 'list',
+ 'values' => array(
+ 'All of the words' => 'all',
+ 'Any of the words' => 'any',
+ 'Extended' => 'extended'
+ ),
+ 'defaultValue' => 'extended'
+ ),
+ 'limit' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ),
+ 'full' => array(
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'cache' => array(
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ )
+ ),
+ 'Browse' => array(
+ 'cat' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => array(
+ 'Visual Art' => array(
+ 'All' => 1,
+ 'Artwork (Digital)' => 2,
+ 'Artwork (Traditional)' => 3,
+ 'Cellshading' => 4,
+ 'Crafting' => 5,
+ 'Designs' => 6,
+ 'Flash' => 7,
+ 'Fursuiting' => 8,
+ 'Icons' => 9,
+ 'Mosaics' => 10,
+ 'Photography' => 11,
+ 'Sculpting' => 12
+ ),
+ 'Readable Art' => array(
+ 'Story' => 13,
+ 'Poetry' => 14,
+ 'Prose' => 15
+ ),
+ 'Audio Art' => array(
+ 'Music' => 16,
+ 'Podcasts' => 17
+ ),
+ 'Downloadable' => array(
+ 'Skins' => 18,
+ 'Handhelds' => 19,
+ 'Resources' => 20
+ ),
+ 'Other Stuff' => array(
+ 'Adoptables' => 21,
+ 'Auctions' => 22,
+ 'Contests' => 23,
+ 'Current Events' => 24,
+ 'Desktops' => 25,
+ 'Stockart' => 26,
+ 'Screenshots' => 27,
+ 'Scraps' => 28,
+ 'Wallpaper' => 29,
+ 'YCH / Sale' => 30,
+ 'Other' => 31
+ )
+ ),
+ 'defaultValue' => 1
+ ),
+ 'atype' => array(
+ 'name' => 'Type',
+ 'type' => 'list',
+ 'values' => array(
+ 'General Things' => array(
+ 'All' => 1,
+ 'Abstract' => 2,
+ 'Animal related (non-anthro)' => 3,
+ 'Anime' => 4,
+ 'Comics' => 5,
+ 'Doodle' => 6,
+ 'Fanart' => 7,
+ 'Fantasy' => 8,
+ 'Human' => 9,
+ 'Portraits' => 10,
+ 'Scenery' => 11,
+ 'Still Life' => 12,
+ 'Tutorials' => 13,
+ 'Miscellaneous' => 14
+ ),
+ 'Fetish / Furry specialty' => array(
+ 'Baby fur' => 101,
+ 'Bondage' => 102,
+ 'Digimon' => 103,
+ 'Fat Furs' => 104,
+ 'Fetish Other' => 105,
+ 'Fursuit' => 106,
+ 'Gore / Macabre Art' => 119,
+ 'Hyper' => 107,
+ 'Inflation' => 108,
+ 'Macro / Micro' => 109,
+ 'Muscle' => 110,
+ 'My Little Pony / Brony' => 111,
+ 'Paw' => 112,
+ 'Pokemon' => 113,
+ 'Pregnancy' => 114,
+ 'Sonic' => 115,
+ 'Transformation' => 116,
+ 'Vore' => 117,
+ 'Water Sports' => 118,
+ 'General Furry Art' => 100
+ ),
+ 'Music' => array(
+ 'Techno' => 201,
+ 'Trance' => 202,
+ 'House' => 203,
+ '90s' => 204,
+ '80s' => 205,
+ '70s' => 206,
+ '60s' => 207,
+ 'Pre-60s' => 208,
+ 'Classical' => 209,
+ 'Game Music' => 210,
+ 'Rock' => 211,
+ 'Pop' => 212,
+ 'Rap' => 213,
+ 'Industrial' => 214,
+ 'Other Music' => 200
+ )
+ ),
+ 'defaultValue' => 1
+ ),
+ 'species' => array(
+ 'name' => 'Species',
+ 'type' => 'list',
+ 'values' => array(
+ 'Unspecified / Any' => 1,
+ 'Amphibian' => array(
+ 'Frog' => 1001,
+ 'Newt' => 1002,
+ 'Salamander' => 1003,
+ 'Amphibian (Other)' => 1000
+ ),
+ 'Aquatic' => array(
+ 'Cephalopod' => 2001,
+ 'Dolphin' => 2002,
+ 'Fish' => 2005,
+ 'Porpoise' => 2004,
+ 'Seal' => 6068,
+ 'Shark' => 2006,
+ 'Whale' => 2003,
+ 'Aquatic (Other)' => 2000
+ ),
+ 'Avian' => array(
+ 'Corvid' => 3001,
+ 'Crow' => 3002,
+ 'Duck' => 3003,
+ 'Eagle' => 3004,
+ 'Falcon' => 3005,
+ 'Goose' => 3006,
+ 'Gryphon' => 3007,
+ 'Hawk' => 3008,
+ 'Owl' => 3009,
+ 'Phoenix' => 3010,
+ 'Swan' => 3011,
+ 'Avian (Other)' => 3000
+ ),
+ 'Bears &amp; Ursines' => array(
+ 'Bear' => 6002
+ ),
+ 'Camelids' => array(
+ 'Camel' => 6074,
+ 'Llama' => 6036
+ ),
+ 'Canines &amp; Lupines' => array(
+ 'Coyote' => 6008,
+ 'Doberman' => 6009,
+ 'Dog' => 6010,
+ 'Dingo' => 6011,
+ 'German Shepherd' => 6012,
+ 'Jackal' => 6013,
+ 'Husky' => 6014,
+ 'Wolf' => 6016,
+ 'Canine (Other)' => 6017
+ ),
+ 'Cervines' => array(
+ 'Cervine (Other)' => 6018
+ ),
+ 'Cows &amp; Bovines' => array(
+ 'Antelope' => 6004,
+ 'Cows' => 6003,
+ 'Gazelle' => 6005,
+ 'Goat' => 6006,
+ 'Bovines (General)' => 6007
+ ),
+ 'Dragons' => array(
+ 'Eastern Dragon' => 4001,
+ 'Hydra' => 4002,
+ 'Serpent' => 4003,
+ 'Western Dragon' => 4004,
+ 'Wyvern' => 4005,
+ 'Dragon (Other)' => 4000
+ ),
+ 'Equestrians' => array(
+ 'Donkey' => 6019,
+ 'Horse' => 6034,
+ 'Pony' => 6073,
+ 'Zebra' => 6071
+ ),
+ 'Exotic &amp; Mythicals' => array(
+ 'Argonian' => 5002,
+ 'Chakat' => 5003,
+ 'Chocobo' => 5004,
+ 'Citra' => 5005,
+ 'Crux' => 5006,
+ 'Daemon' => 5007,
+ 'Digimon' => 5008,
+ 'Dracat' => 5009,
+ 'Draenei' => 5010,
+ 'Elf' => 5011,
+ 'Gargoyle' => 5012,
+ 'Iksar' => 5013,
+ 'Kaiju/Monster' => 5015,
+ 'Langurhali' => 5014,
+ 'Moogle' => 5017,
+ 'Naga' => 5016,
+ 'Orc' => 5018,
+ 'Pokemon' => 5019,
+ 'Satyr' => 5020,
+ 'Sergal' => 5021,
+ 'Tanuki' => 5022,
+ 'Unicorn' => 5023,
+ 'Xenomorph' => 5024,
+ 'Alien (Other)' => 5001,
+ 'Exotic (Other)' => 5000
+ ),
+ 'Felines' => array(
+ 'Domestic Cat' => 6020,
+ 'Cheetah' => 6021,
+ 'Cougar' => 6022,
+ 'Jaguar' => 6023,
+ 'Leopard' => 6024,
+ 'Lion' => 6025,
+ 'Lynx' => 6026,
+ 'Ocelot' => 6027,
+ 'Panther' => 6028,
+ 'Tiger' => 6029,
+ 'Feline (Other)' => 6030
+ ),
+ 'Insects' => array(
+ 'Arachnid' => 8000,
+ 'Mantid' => 8004,
+ 'Scorpion' => 8005,
+ 'Insect (Other)' => 8003
+ ),
+ 'Mammals (Other)' => array(
+ 'Bat' => 6001,
+ 'Giraffe' => 6031,
+ 'Hedgehog' => 6032,
+ 'Hippopotamus' => 6033,
+ 'Hyena' => 6035,
+ 'Panda' => 6052,
+ 'Pig/Swine' => 6053,
+ 'Rabbit/Hare' => 6059,
+ 'Raccoon' => 6060,
+ 'Red Panda' => 6062,
+ 'Meerkat' => 6043,
+ 'Mongoose' => 6044,
+ 'Rhinoceros' => 6063,
+ 'Mammals (Other)' => 6000
+ ),
+ 'Marsupials' => array(
+ 'Opossum' => 6037,
+ 'Kangaroo' => 6038,
+ 'Koala' => 6039,
+ 'Quoll' => 6040,
+ 'Wallaby' => 6041,
+ 'Marsupial (Other)' => 6042
+ ),
+ 'Mustelids' => array(
+ 'Badger' => 6045,
+ 'Ferret' => 6046,
+ 'Mink' => 6048,
+ 'Otter' => 6047,
+ 'Skunk' => 6069,
+ 'Weasel' => 6049,
+ 'Mustelid (Other)' => 6051
+ ),
+ 'Primates' => array(
+ 'Gorilla' => 6054,
+ 'Human' => 6055,
+ 'Lemur' => 6056,
+ 'Monkey' => 6057,
+ 'Primate (Other)' => 6058
+ ),
+ 'Reptillian' => array(
+ 'Alligator &amp; Crocodile' => 7001,
+ 'Gecko' => 7003,
+ 'Iguana' => 7004,
+ 'Lizard' => 7005,
+ 'Snakes &amp; Serpents' => 7006,
+ 'Turtle' => 7007,
+ 'Reptilian (Other)' => 7000
+ ),
+ 'Rodents' => array(
+ 'Beaver' => 6064,
+ 'Mouse' => 6065,
+ 'Rat' => 6061,
+ 'Squirrel' => 6070,
+ 'Rodent (Other)' => 6067
+ ),
+ 'Vulpines' => array(
+ 'Fennec' => 6072,
+ 'Fox' => 6075,
+ 'Vulpine (Other)' => 6015
+ ),
+ 'Other' => array(
+ 'Dinosaur' => 8001,
+ 'Wolverine' => 6050
+ )
+ ),
+ 'defaultValue' => 1
+ ),
+ 'gender' => array(
+ 'name' => 'Gender',
+ 'type' => 'list',
+ 'values' => array(
+ 'Any' => 0,
+ 'Male' => 2,
+ 'Female' => 3,
+ 'Herm' => 4,
+ 'Transgender' => 5,
+ 'Multiple characters' => 6,
+ 'Other / Not Specified' => 7
+ ),
+ 'defaultValue' => 0
+ ),
+ 'rating_general' => array(
+ 'name' => 'General',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'rating_mature' => array(
+ 'name' => 'Mature',
+ 'type' => 'checkbox',
+ ),
+ 'rating_adult' => array(
+ 'name' => 'Adult',
+ 'type' => 'checkbox',
+ ),
+ 'limit-browse' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => true,
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ),
+ 'full' => array(
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'cache' => array(
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ )
+
+ ),
+ 'Journals' => array(
+ 'username-journals' => array(
+ 'name' => 'Username',
+ 'required' => true,
+ 'title' => 'Lowercase username as seen in URLs'
+ ),
+ 'limit' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'defaultValue' => -1,
+ 'title' => 'Limit number of journals to return. -1 for unlimited.'
+ )
+
+ ),
+ 'Single Journal' => array(
+ 'journal-id' => array(
+ 'name' => 'Journal ID',
+ 'required' => true,
+ 'type' => 'number',
+ 'title' => 'Number seen in journal URL'
+ )
+ ),
+ 'Gallery' => array(
+ 'username-gallery' => array(
+ 'name' => 'Username',
+ 'required' => true,
+ 'title' => 'Lowercase username as seen in URLs'
+ ),
+ 'limit' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ),
+ 'full' => array(
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'cache' => array(
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ )
+ ),
+ 'Scraps' => array(
+ 'username-scraps' => array(
+ 'name' => 'Username',
+ 'required' => true,
+ 'title' => 'Lowercase username as seen in URLs'
+ ),
+ 'limit' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ),
+ 'full' => array(
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'cache' => array(
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ )
+ ),
+ 'Favorites' => array(
+ 'username-favorites' => array(
+ 'name' => 'Username',
+ 'required' => true,
+ 'title' => 'Lowercase username as seen in URLs'
+ ),
+ 'limit' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ),
+ 'full' => array(
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'cache' => array(
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ )
+ ),
+ 'Gallery Folder' => array(
+ 'username-folder' => array(
+ 'name' => 'Username',
+ 'required' => true,
+ 'title' => 'Lowercase username as seen in URLs'
+ ),
+ 'folder-id' => array(
+ 'name' => 'Folder ID',
+ 'required' => true,
+ 'type' => 'number',
+ 'title' => 'Number seen in folder URL'
+ ),
+ 'limit' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'defaultValue' => 10,
+ 'title' => 'Limit number of submissions to return. -1 for unlimited.'
+ ),
+ 'full' => array(
+ 'name' => 'Full view',
+ 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ ),
+ 'cache' => array(
+ 'name' => 'Cache submission pages',
+ 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
+ 'type' => 'checkbox',
+ 'defaultValue' => 'checked'
+ )
+ )
+ );
+
+ /*
+ * This was aquired by creating a new user on FA then
+ * extracting the cookie from the browsers dev console.
+ */
+ const FA_AUTH_COOKIE = 'b=4ce65691-b50f-4742-a990-bf28d6de16ee; a=ca6e4566-9d81-4263-9444-653b142e35f8';
+
+ public function detectParameters($url) {
+ $params = array();
+
+ // Single journal
+ $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journal\/(\d+)/';
+ if(preg_match($regex, $url, $matches) > 0) {
+ $params['journal-id'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ // Journals
+ $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journals\/([^\/&?\n]+)/';
+ if(preg_match($regex, $url, $matches) > 0) {
+ $params['username-journals'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ // Gallery folder
+ $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/gallery\/([^\/&?\n]+)\/folder\/(\d+)/';
+ if(preg_match($regex, $url, $matches) > 0) {
+ $params['username-folder'] = urldecode($matches[3]);
+ $params['folder-id'] = urldecode($matches[4]);
+ $params['full'] = 'on';
+ return $params;
+ }
+
+ // Gallery (must be after gallery folder)
+ $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/(gallery|scraps|favorites)\/([^\/&?\n]+)/';
+ if(preg_match($regex, $url, $matches) > 0) {
+ $params['username-' . $matches[3]] = urldecode($matches[4]);
+ $params['full'] = 'on';
+ return $params;
+ }
+
+ return null;
+ }
+
+ public function getName() {
+ switch($this->queriedContext) {
+ case 'Search':
+ return 'Search For '
+ . $this->getInput('q');
+ case 'Browse':
+ return 'Browse';
+ case 'Journals':
+ return $this->getInput('username-journals');
+ case 'Single Journal':
+ return 'Journal '
+ . $this->getInput('journal-id');
+ case 'Gallery':
+ return $this->getInput('username-gallery');
+ case 'Scraps':
+ return $this->getInput('username-scraps');
+ case 'Favorites':
+ return $this->getInput('username-favorites');
+ case 'Gallery Folder':
+ return $this->getInput('username-folder')
+ . '\'s Folder '
+ . $this->getInput('folder-id');
+ default: return parent::getName();
+ }
+ }
+
+ public function getDescription() {
+ switch($this->queriedContext) {
+ case 'Search':
+ return 'FurAffinity Search For '
+ . $this->getInput('q');
+ case 'Browse':
+ return 'FurAffinity Browse';
+ case 'Journals':
+ return 'FurAffinity Journals By '
+ . $this->getInput('username-journals');
+ case 'Single Journal':
+ return 'FurAffinity Journal '
+ . $this->getInput('journal-id');
+ case 'Gallery':
+ return 'FurAffinity Gallery By '
+ . $this->getInput('username-gallery');
+ case 'Scraps':
+ return 'FurAffinity Scraps By '
+ . $this->getInput('username-scraps');
+ case 'Favorites':
+ return 'FurAffinity Favorites By '
+ . $this->getInput('username-favorites');
+ case 'Gallery Folder':
+ return 'FurAffinity Gallery Folder '
+ . $this->getInput('folder-id')
+ . ' By '
+ . $this->getInput('username-folder');
+ default: return parent::getDescription();
+ }
+ }
+
+ public function getURI() {
+ switch($this->queriedContext) {
+ case 'Search':
+ return SELF::URI
+ . '/search';
+ case 'Browse':
+ return SELF::URI
+ . '/browse';
+ case 'Journals':
+ return SELF::URI
+ . '/journals/'
+ . $this->getInput('username-journals');
+ case 'Single Journal':
+ return SELF::URI
+ . '/journal/'
+ . $this->getInput('journal-id');
+ case 'Gallery':
+ return SELF::URI
+ . '/gallery/'
+ . $this->getInput('username-gallery');
+ case 'Scraps':
+ return SELF::URI
+ . '/scraps/'
+ . $this->getInput('username-scraps');
+ case 'Favorites':
+ return SELF::URI
+ . '/favorites/'
+ . $this->getInput('username-favorites');
+ case 'Gallery Folder':
+ return SELF::URI
+ . '/gallery/'
+ . $this->getInput('username-folder')
+ . '/folder/'
+ . $this->getInput('folder-id');
+ default: return parent::getURI();
+ }
+ }
+
+ public function collectData() {
+ switch($this->queriedContext) {
+ case 'Search':
+ $data = array(
+ 'q' => $this->getInput('q'),
+ 'perpage' => 72,
+ 'rating-general' => ($this->getInput('rating-general') === true ? 'on' : 0),
+ 'rating-mature' => ($this->getInput('rating-mature') === true ? 'on' : 0),
+ 'rating-adult' => ($this->getInput('rating-adult') === true ? 'on' : 0),
+ 'range' => $this->getInput('range'),
+ 'type-art' => ($this->getInput('type-art') === true ? 'on' : 0),
+ 'type-flash' => ($this->getInput('type-flash') === true ? 'on' : 0),
+ 'type-photo' => ($this->getInput('type-photo') === true ? 'on' : 0),
+ 'type-music' => ($this->getInput('type-music') === true ? 'on' : 0),
+ 'type-story' => ($this->getInput('type-story') === true ? 'on' : 0),
+ 'type-poetry' => ($this->getInput('type-poetry') === true ? 'on' : 0),
+ 'mode' => $this->getInput('mode')
+ );
+ $html = $this->postFASimpleHTMLDOM($data);
+ $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10);
+ $this->itemsFromSubmissionList($html, $limit);
+ break;
+ case 'Browse':
+ $data = array(
+ 'cat' => $this->getInput('cat'),
+ 'atype' => $this->getInput('atype'),
+ 'species' => $this->getInput('species'),
+ 'gender' => $this->getInput('gender'),
+ 'perpage' => 72,
+ 'rating_general' => ($this->getInput('rating_general') === true ? 'on' : 0),
+ 'rating_mature' => ($this->getInput('rating_mature') === true ? 'on' : 0),
+ 'rating_adult' => ($this->getInput('rating_adult') === true ? 'on' : 0)
+ );
+ $html = $this->postFASimpleHTMLDOM($data);
+ $limit = (is_int($this->getInput('limit-browse')) ? $this->getInput('limit-browse') : 10);
+ $this->itemsFromSubmissionList($html, $limit);
+ break;
+ case 'Journals':
+ $html = $this->getFASimpleHTMLDOM($this->getURI());
+ $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : -1);
+ $this->itemsFromJournalList($html, $limit);
+ break;
+ case 'Single Journal':
+ $html = $this->getFASimpleHTMLDOM($this->getURI());
+ $this->itemsFromJournal($html);
+ break;
+ case 'Gallery':
+ case 'Scraps':
+ case 'Favorites':
+ case 'Gallery Folder':
+ $html = $this->getFASimpleHTMLDOM($this->getURI());
+ $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10);
+ $this->itemsFromSubmissionList($html, $limit);
+ break;
+ }
+ }
+
+ private function postFASimpleHTMLDOM($data) {
+ $opts = array(
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_POSTFIELDS => http_build_query($data)
+ );
+ $header = array(
+ 'Host: ' . parse_url(self::URI, PHP_URL_HOST),
+ 'Content-Type: application/x-www-form-urlencoded',
+ 'Cookie: ' . self::FA_AUTH_COOKIE
+ );
+
+ $html = getSimpleHTMLDOM($this->getURI(), $header, $opts);
+ $html = defaultLinkTo($html, $this->getURI());
+
+ return $html;
+ }
+
+ private function getFASimpleHTMLDOM($url, $cache = false) {
+ $header = array(
+ 'Cookie: ' . self::FA_AUTH_COOKIE
+ );
+
+ if($cache) {
+ $html = getSimpleHTMLDOMCached($url, 86400, $header); // 24 hours
+ } else {
+ $html = getSimpleHTMLDOM($url, $header);
+ }
+
+ $html = defaultLinkTo($html, $url);
+
+ return $html;
+ }
+
+ private function itemsFromJournalList($html, $limit) {
+ foreach($html->find('table[id^=jid:]') as $journal) {
+ # allows limit = -1 to mean 'unlimited'
+ if($limit-- === 0) break;
+
+ $item = array();
+
+ $this->setReferrerPolicy($journal);
+
+ $item['uri'] = $journal->find('a', 0)->href;
+ $item['title'] = html_entity_decode($journal->find('a', 0)->plaintext);
+ $item['author'] = $this->getInput('username-journals');
+ $item['timestamp'] = strtotime(
+ $journal->find('span.popup_date', 0)->plaintext);
+ $item['content'] = $journal
+ ->find('.alt1 table div.no_overflow', 0)
+ ->innertext;
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function itemsFromJournal($html) {
+ $this->setReferrerPolicy($html);
+ $item = array();
+
+ $item['uri'] = $this->getURI();
+
+ $title = $html->find('.journal-title-box .no_overflow', 0)->plaintext;
+ $title = html_entity_decode($title);
+ $title = trim($title, " \t\n\r\0\x0B" . chr(0xC2) . chr(0xA0));
+ $item['title'] = $title;
+
+ $item['author'] = $html->find('.journal-title-box a', 0)->plaintext;
+ $item['timestamp'] = strtotime(
+ $html->find('.journal-title-box span.popup_date', 0)->plaintext);
+ $item['content'] = $html->find('.journal-body', 0)->innertext;
+
+ $this->items[] = $item;
+ }
+
+ private function itemsFromSubmissionList($html, $limit) {
+ $cache = ($this->getInput('cache') === true);
+
+ foreach($html->find('section.gallery figure') as $figure) {
+ # allows limit = -1 to mean 'unlimited'
+ if($limit-- === 0) break;
+
+ $item = array();
+
+ $submissionURL = $figure->find('b u a', 0)->href;
+ $imgURL = 'https:' . $figure->find('b u a img', 0)->src;
+
+ $item['uri'] = $submissionURL;
+ $item['title'] = html_entity_decode(
+ $figure->find('figcaption p a[href*=/view/]', 0)->title);
+ $item['author'] = $figure->find('figcaption p a[href*=/user/]', 0)->title;
+
+ if($this->getInput('full') === true) {
+ $submissionHTML = $this->getFASimpleHTMLDOM($submissionURL, $cache);
+
+ $stats = $submissionHTML->find('.stats-container', 0);
+ $item['timestamp'] = strtotime($stats->find('.popup_date', 0)->title);
+ $item['enclosures'] = array(
+ $submissionHTML->find('.actions a[href^=https://d.facdn]', 0)->href
+ );
+ foreach($stats->find('#keywords a') as $keyword) {
+ $item['categories'][] = $keyword->plaintext;
+ }
+
+ $previewSrc = $submissionHTML->find('#submissionImg', 0)
+ ->{'data-preview-src'};
+ if($previewSrc) {
+ $imgURL = 'https:' . $previewSrc;
+ }
+
+ $description = $submissionHTML
+ ->find('.maintable .maintable tr td.alt1', -1);
+ $this->setReferrerPolicy($description);
+ $description = $description->innertext;
+
+ $item['content'] = <<<EOD
+<a href="$submissionURL">
+ <img src="{$imgURL}" referrerpolicy="no-referrer" />
+</a>
+<p>
+{$description}
+</p>
+EOD;
+ } else {
+ $item['content'] = <<<EOD
+<a href="$submissionURL">
+ <img src="$imgURL" referrerpolicy="no-referrer" />
+</a>
+EOD;
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function setReferrerPolicy(&$html) {
+ foreach($html->find('img') as $img) {
+ /*
+ * Note: Without the no-referrer policy their CDN sometimes denies requests.
+ * We can't control this for enclosures sadly.
+ * At least tt-rss adds the referrerpolicy on its own.
+ * Alternatively we could not use https for images, but that's not ideal.
+ */
+ $img->referrerpolicy = 'no-referrer';
+ }
+ }
+}
diff --git a/bridges/GBAtempBridge.php b/bridges/GBAtempBridge.php
index 9383be7..48a7f85 100644
--- a/bridges/GBAtempBridge.php
+++ b/bridges/GBAtempBridge.php
@@ -10,7 +10,6 @@ class GBAtempBridge extends BridgeAbstract {
'type' => array(
'name' => 'Type',
'type' => 'list',
- 'required' => true,
'values' => array(
'News' => 'N',
'Reviews' => 'R',
diff --git a/bridges/GOGBridge.php b/bridges/GOGBridge.php
index 669332f..09f47b4 100644
--- a/bridges/GOGBridge.php
+++ b/bridges/GOGBridge.php
@@ -8,8 +8,8 @@ class GOGBridge extends BridgeAbstract {
public function collectData() {
- $values = getContents('https://www.gog.com/games/ajax/filtered?limit=25&sort=new') or
- die('Unable to get the news pages from GOG !');
+ $values = getContents('https://www.gog.com/games/ajax/filtered?limit=25&sort=new')
+ or returnServerError('Unable to get the news pages from GOG !');
$decodedValues = json_decode($values);
$limit = 0;
@@ -38,8 +38,8 @@ class GOGBridge extends BridgeAbstract {
private function buildGameContentPage($game) {
- $gameDescriptionText = getContents('https://api.gog.com/products/' . $game->id . '?expand=description') or
- die('Unable to get game description from GOG !');
+ $gameDescriptionText = getContents('https://api.gog.com/products/' . $game->id . '?expand=description')
+ or returnServerError('Unable to get game description from GOG !');
$gameDescriptionValue = json_decode($gameDescriptionText);
diff --git a/bridges/GQMagazineBridge.php b/bridges/GQMagazineBridge.php
index 961b3a0..2884ab6 100644
--- a/bridges/GQMagazineBridge.php
+++ b/bridges/GQMagazineBridge.php
@@ -40,6 +40,11 @@ class GQMagazineBridge extends BridgeAbstract
'data-original' => 'src'
);
+ const POSSIBLE_TITLES = array(
+ 'h2',
+ 'h3'
+ );
+
private function getDomain() {
$domain = $this->getInput('domain');
if (empty($domain))
@@ -54,6 +59,17 @@ class GQMagazineBridge extends BridgeAbstract
return $this->getDomain() . '/' . $this->getInput('page');
}
+ private function findTitleOf($link) {
+ foreach (self::POSSIBLE_TITLES as $tag) {
+ $title = $link->parent()->find($tag, 0);
+ if($title !== null) {
+ if($title->plaintext !== null) {
+ return $title->plaintext;
+ }
+ }
+ }
+ }
+
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI()) or returnServerError('Could not request ' . $this->getURI());
@@ -61,31 +77,36 @@ class GQMagazineBridge extends BridgeAbstract
// Since GQ don't want simple class scrapping, let's do it the hard way and ... discover content !
$main = $html->find('main', 0);
foreach ($main->find('a') as $link) {
+ if(strpos($link, $this->getInput('page')))
+ continue;
$uri = $link->href;
- $title = $link->find('h2', 0);
- $date = $link->find('time', 0);
+ $date = $link->parent()->find('time', 0);
$item = array();
- $author = $link->find('span[itemprop=name]', 0);
- $item['author'] = $author->plaintext;
- $item['title'] = $title->plaintext;
- if(substr($uri, 0, 1) === 'h') { // absolute uri
- $item['uri'] = $uri;
- } else if(substr($uri, 0, 1) === '/') { // domain relative url
- $item['uri'] = $this->getDomain() . $uri;
- } else {
- $item['uri'] = $this->getDomain() . '/' . $uri;
- }
-
- $article = $this->loadFullArticle($item['uri']);
- if($article) {
- $item['content'] = $this->replaceUriInHtmlElement($article);
- } else {
- $item['content'] = "<strong>Article body couldn't be loaded</strong>. It must be a bug!";
+ $author = $link->parent()->find('span[itemprop=name]', 0);
+ if($author !== null) {
+ $item['author'] = $author->plaintext;
+ $item['title'] = $this->findTitleOf($link);
+ switch(substr($uri, 0, 1)) {
+ case 'h': // absolute uri
+ $item['uri'] = $uri;
+ break;
+ case '/': // domain relative uri
+ $item['uri'] = $this->getDomain() . $uri;
+ break;
+ default:
+ $item['uri'] = $this->getDomain() . '/' . $uri;
+ }
+ $article = $this->loadFullArticle($item['uri']);
+ if($article) {
+ $item['content'] = $this->replaceUriInHtmlElement($article);
+ } else {
+ $item['content'] = "<strong>Article body couldn't be loaded</strong>. It must be a bug!";
+ }
+ $short_date = $date->datetime;
+ $item['timestamp'] = strtotime($short_date);
+ $this->items[] = $item;
}
- $short_date = $date->datetime;
- $item['timestamp'] = strtotime($short_date);
- $this->items[] = $item;
}
}
@@ -96,16 +117,7 @@ class GQMagazineBridge extends BridgeAbstract
*/
private function loadFullArticle($uri){
$html = getSimpleHTMLDOMCached($uri);
- // Once again, that generated css classes madness is an obstacle ... which i can go over easily
- foreach($html->find('div') as $div) {
- // List the CSS classes of that div
- $classes = $div->class;
- // I can't directly lookup that class since GQ since to generate random names like "ArticleBodySection-fkggUW"
- if(strpos($classes, 'ArticleBodySection') !== false) {
- return $div;
- }
- }
- return null;
+ return $html->find('section[data-test-id=ArticleBodyContent]', 0);
}
/**
diff --git a/bridges/GiteaBridge.php b/bridges/GiteaBridge.php
new file mode 100644
index 0000000..3324787
--- /dev/null
+++ b/bridges/GiteaBridge.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Gitea is a fork of Gogs which may diverge in the future.
+ * https://docs.gitea.io/en-us/
+ */
+require_once 'GogsBridge.php';
+
+class GiteaBridge extends GogsBridge {
+
+ const NAME = 'Gitea';
+ const URI = 'https://gitea.io';
+ const DESCRIPTION = 'Returns the latest issues, commits or releases';
+ const MAINTAINER = 'logmanoriginal';
+ const CACHE_TIMEOUT = 300; // 5 minutes
+
+ protected function collectReleasesData($html) {
+ $releases = $html->find('#release-list > li')
+ or returnServerError('Unable to find releases');
+
+ foreach($releases as $release) {
+ $this->items[] = array(
+ 'uri' => $release->find('a', 0)->href,
+ 'title' => 'Release ' . $release->find('h3', 0)->plaintext,
+ );
+ }
+ }
+}
diff --git a/bridges/GithubIssueBridge.php b/bridges/GithubIssueBridge.php
index 91dd45e..2eddeb2 100644
--- a/bridges/GithubIssueBridge.php
+++ b/bridges/GithubIssueBridge.php
@@ -66,10 +66,21 @@ class GithubIssueBridge extends BridgeAbstract {
return parent::getURI();
}
- protected function extractIssueEvent($issueNbr, $title, $comment){
- $comment = $comment->firstChild();
- $uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p')
- . '/issues/' . $issueNbr . '#' . $comment->getAttribute('id');
+ private function buildGitHubIssueCommentUri($issue_number, $comment_id) {
+ // https://github.com/<user>/<project>/issues/<issue-number>#<id>
+ return static::URI
+ . $this->getInput('u')
+ . '/'
+ . $this->getInput('p')
+ . '/issues/'
+ . $issue_number
+ . '#'
+ . $comment_id;
+ }
+
+ private function extractIssueEvent($issueNbr, $title, $comment){
+
+ $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
$author = $comment->find('.author', 0)->plaintext;
@@ -94,22 +105,21 @@ class GithubIssueBridge extends BridgeAbstract {
return $item;
}
- protected function extractIssueComment($issueNbr, $title, $comment){
- $uri = static::URI . $this->getInput('u') . '/'
- . $this->getInput('p') . '/issues/' . $issueNbr;
+ private function extractIssueComment($issueNbr, $title, $comment){
+
+ $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->parent->id);
$author = $comment->find('.author', 0)->plaintext;
$title .= ' / ' . trim(
- $comment->find('.comment .timeline-comment-header-text', 0)->plaintext
+ $comment->find('.timeline-comment-header-text', 0)->plaintext
);
$content = $comment->find('.comment-body', 0)->innertext;
$item = array();
$item['author'] = $author;
- $item['uri'] = $uri
- . '#' . $comment->firstChild()->nextSibling()->getAttribute('id');
+ $item['uri'] = $uri;
$item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
$item['timestamp'] = strtotime(
$comment->find('relative-time', 0)->getAttribute('datetime')
@@ -118,25 +128,32 @@ class GithubIssueBridge extends BridgeAbstract {
return $item;
}
- protected function extractIssueComments($issue){
+ private 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) {
+
+ $comments = $issue->find('
+ [id^="issue-"] > .comment,
+ [id^="issuecomment-"] > .comment,
+ [id^="event-"],
+ [id^="ref-"]
+ ');
+ foreach($comments as $comment) {
+
if (!$comment->hasChildNodes()) {
continue;
}
- $comment = $comment->firstChild();
- $classes = explode(' ', $comment->getAttribute('class'));
- if (in_array('timeline-comment-wrapper', $classes)) {
+
+ if (!$comment->hasClass('discussion-item-header')) {
$item = $this->extractIssueComment($issueNbr, $title, $comment);
$items[] = $item;
continue;
}
- while (in_array('discussion-item', $classes)) {
+
+ while ($comment->hasClass('discussion-item-header')) {
$item = $this->extractIssueEvent($issueNbr, $title, $comment);
$items[] = $item;
$comment = $comment->nextSibling();
@@ -145,6 +162,7 @@ class GithubIssueBridge extends BridgeAbstract {
}
$classes = explode(' ', $comment->getAttribute('class'));
}
+
}
return $items;
}
@@ -192,8 +210,13 @@ class GithubIssueBridge extends BridgeAbstract {
ENT_QUOTES,
'UTF-8'
);
- $comments = trim($issue->find('.col-5', 0)->plaintext);
- $item['content'] .= "\n" . 'Comments: ' . ($comments ? $comments : '0');
+
+ $comment_count = 0;
+ if($span = $issue->find('a[aria-label*="comment"] span', 0)) {
+ $comment_count = $span->plaintext;
+ }
+
+ $item['content'] .= "\n" . 'Comments: ' . $comment_count;
$item['uri'] = self::URI
. $issue->find('.js-navigation-open', 0)->getAttribute('href');
$this->items[] = $item;
@@ -216,4 +239,43 @@ class GithubIssueBridge extends BridgeAbstract {
$item['title'] = preg_replace('/\s+/', ' ', $item['title']);
});
}
+
+ public function detectParameters($url) {
+
+ if(filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false
+ || strpos($url, self::URI) !== 0) {
+ return null;
+ }
+
+ $url_components = parse_url($url);
+ $path_segments = array_values(array_filter(explode('/', $url_components['path'])));
+
+ switch(count($path_segments)) {
+ case 2: { // Project issues
+ list($user, $project) = $path_segments;
+ $show_comments = 'off';
+ } break;
+ case 3: { // Project issues with issue comments
+ if($path_segments[2] !== 'issues') {
+ return null;
+ }
+ list($user, $project) = $path_segments;
+ $show_comments = 'on';
+ } break;
+ case 4: { // Issue comments
+ list($user, $project, /* issues */, $issue) = $path_segments;
+ } break;
+ default: {
+ return null;
+ }
+ }
+
+ return array(
+ 'u' => $user,
+ 'p' => $project,
+ 'c' => isset($show_comments) ? $show_comments : null,
+ 'i' => isset($issue) ? $issue : null,
+ );
+
+ }
}
diff --git a/bridges/GithubSearchBridge.php b/bridges/GithubSearchBridge.php
index fe8a721..fd90934 100644
--- a/bridges/GithubSearchBridge.php
+++ b/bridges/GithubSearchBridge.php
@@ -24,7 +24,7 @@ class GithubSearchBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM($url)
or returnServerError('Error while downloading the website content');
- foreach($html->find('div.repo-list-item') as $element) {
+ foreach($html->find('li.repo-list-item') as $element) {
$item = array();
$uri = $element->find('h3 a', 0)->href;
diff --git a/bridges/GlassdoorBridge.php b/bridges/GlassdoorBridge.php
index 1e20077..308859d 100644
--- a/bridges/GlassdoorBridge.php
+++ b/bridges/GlassdoorBridge.php
@@ -117,7 +117,7 @@ class GlassdoorBridge extends BridgeAbstract {
$item['title'] = $post->find('header', 0)->plaintext;
$item['content'] = $post->find('div[class="excerpt-content"]', 0)->plaintext;
$item['enclosures'] = array(
- $this->getFullSizeImageURI($post->find('div[class="post-thumb"]', 0)->{'data-original'})
+ $this->getFullSizeImageURI($post->find('div[class*="post-thumb"]', 0)->{'data-original'})
);
// optionally load full articles
@@ -141,7 +141,7 @@ class GlassdoorBridge extends BridgeAbstract {
}
private function collectReviewData($html, $limit) {
- $reviews = $html->find('#EmployerReviews li[id^="empReview]')
+ $reviews = $html->find('#ReviewsFeed li[id^="empReview]')
or returnServerError('Unable to find reviews!');
foreach($reviews as $review) {
@@ -153,7 +153,19 @@ class GlassdoorBridge extends BridgeAbstract {
$item['timestamp'] = strtotime($review->find('time', 0)->datetime);
$mainText = $review->find('p.mainText', 0)->plaintext;
- $description = $review->find('div.prosConsAdvice', 0)->innertext;
+
+ $description = '';
+ foreach($review->find('div.description p') as $p) {
+
+ if ($p->hasClass('strong')) {
+ $p->tag = 'strong';
+ $p->removeClass('strong');
+ }
+
+ $description .= $p;
+
+ }
+
$item['content'] = "<p>{$mainText}</p><p>{$description}</p>";
$this->items[] = $item;
diff --git a/bridges/GlowficBridge.php b/bridges/GlowficBridge.php
new file mode 100644
index 0000000..e8975a7
--- /dev/null
+++ b/bridges/GlowficBridge.php
@@ -0,0 +1,88 @@
+<?php
+class GlowficBridge extends BridgeAbstract {
+ const MAINTAINER = 'l1n';
+ const NAME = 'Glowfic Bridge';
+ const URI = 'https://www.glowfic.com';
+ const CACHE_TIMEOUT = 3600; // 1 hour
+ const DESCRIPTION = 'Returns the latest replies on a glowfic post.';
+ const PARAMETERS = array(
+ 'global' => array(),
+ 'Thread' => array(
+ 'post_id' => array(
+ 'name' => 'Post ID',
+ 'title' => 'https://www.glowfic.com/posts/<POST ID>',
+ 'type' => 'number'
+ ),
+ 'start_page' => array(
+ 'name' => 'Start Page',
+ 'title' => 'To start from an offset page',
+ 'type' => 'number'
+ )
+ )
+ );
+
+ public function collectData() {
+ $url = $this->getAPIURI();
+ $metadata = get_headers( $url . '/replies', true ) or returnClientError('Post did not return reply headers.');
+ $metadata['Last-Page'] = ceil( $metadata['Total'] / $metadata['Per-Page'] );
+ if(!is_null($this->getInput('start_page')) &&
+ $this->getInput('start_page') < 1 && $metadata['Last-Page'] - $this->getInput('start_page') > 0) {
+ $first_page = $metadata['Last-Page'] - $this->getInput('start_page');
+ } else if(!is_null($this->getInput('start_page')) && $this->getInput('start_page') <= $metadata['Last-Page']) {
+ $first_page = $this->getInput('start_page');
+ } else {
+ $first_page = 1;
+ }
+ for ($page_offset = $first_page; $page_offset <= $metadata['Last-Page']; $page_offset++) {
+ $jsonContents = getContents($url . '/replies?page=' . $page_offset ) or
+ returnClientError('Could not retrieve replies for page ' . $page_offset . '.');
+ $replies = json_decode($jsonContents);
+ foreach ($replies as $reply) {
+ $item = array();
+
+ $item['content'] = $reply->{'content'};
+ $item['uri'] = $this->getURI() . '?page=' . $page_offset . '#reply-' . $reply->{'id'};
+ if ($reply->{'icon'}) {
+ $item['enclosures'] = array($reply->{'icon'}->{'url'});
+ }
+ $item['author'] = $reply->{'character'}->{'screenname'} . ' (' . $reply->{'character'}->{'name'} . ')';
+ $item['timestamp'] = date('r', strtotime($reply->{'created_at'}));
+ $item['title'] = 'Tag by ' . $reply->{'user'}->{'username'} . ' updated at ' . $reply->{'updated_at'};
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ private function getAPIURI() {
+ $url = parent::getURI() . '/api/v1/posts/' . $this->getInput('post_id');
+ return $url;
+ }
+
+ public function getURI() {
+ $url = parent::getURI() . '/posts/' . $this->getInput('post_id');
+ return $url;
+ }
+
+ private function getPost() {
+ $url = $this->getAPIURI();
+ $jsonPost = getContents( $url ) or returnClientError('Could not retrieve post metadata.');
+ $post = json_decode($jsonPost);
+ return $post;
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('post_id'))) {
+ $post = $this->getPost();
+ return $post->{'subject'} . ' - ' . parent::getName();
+ }
+ return parent::getName();
+ }
+
+ public function getDescription(){
+ if(!is_null($this->getInput('post_id'))) {
+ $post = $this->getPost();
+ return $post->{'content'};
+ }
+ return parent::getName();
+ }
+}
diff --git a/bridges/GogsBridge.php b/bridges/GogsBridge.php
new file mode 100644
index 0000000..a08bcc0
--- /dev/null
+++ b/bridges/GogsBridge.php
@@ -0,0 +1,206 @@
+<?php
+class GogsBridge extends BridgeAbstract {
+
+ const NAME = 'Gogs';
+ const URI = 'https://gogs.io';
+ const DESCRIPTION = 'Returns the latest issues, commits or releases';
+ const MAINTAINER = 'logmanoriginal';
+ const CACHE_TIMEOUT = 300; // 5 minutes
+
+ const PARAMETERS = array(
+ 'global' => array(
+ 'host' => array(
+ 'name' => 'Host',
+ 'exampleValue' => 'https://gogs.io',
+ 'required' => true,
+ 'title' => 'Host name without trailing slash',
+ ),
+ 'user' => array(
+ 'name' => 'Username',
+ 'exampleValue' => 'gogs',
+ 'required' => true,
+ 'title' => 'User name as it appears in the URL',
+ ),
+ 'project' => array(
+ 'name' => 'Project name',
+ 'exampleValue' => 'gogs',
+ 'required' => true,
+ 'title' => 'Project name as it appears in the URL',
+ ),
+ ),
+ 'Commits' => array(
+ 'branch' => array(
+ 'name' => 'Branch name',
+ 'defaultValue' => 'master',
+ 'required' => true,
+ 'title' => 'Branch name as it appears in the URL',
+ ),
+ ),
+ 'Issues' => array(
+ 'include_description' => array(
+ 'name' => 'Include issue description',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to include the issue description',
+ ),
+ ),
+ 'Single issue' => array(
+ 'issue' => array(
+ 'name' => 'Issue number',
+ 'type' => 'number',
+ 'exampleValue' => 102,
+ 'required' => true,
+ 'title' => 'Issue number from the issues list',
+ ),
+ ),
+ 'Releases' => array(),
+ );
+
+ private $title = '';
+
+ /**
+ * Note: detectParamters doesn't make sense for this bridge because there is
+ * no "single" host for this service. Anyone can host it.
+ */
+
+ public function getURI() {
+ switch($this->queriedContext) {
+ case 'Commits': {
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/commits/' . $this->getInput('branch');
+ } break;
+ case 'Issues': {
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/issues/';
+ } break;
+ case 'Single issue': {
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/issues/' . $this->getInput('issue');
+ } break;
+ case 'Releases': {
+ return $this->getInput('host')
+ . '/' . $this->getInput('user')
+ . '/' . $this->getInput('project')
+ . '/releases/';
+ } break;
+ default: return parent::getURI();
+ }
+ }
+
+ public function getName() {
+ switch($this->queriedContext) {
+ case 'Commits':
+ case 'Issues':
+ case 'Releases': return $this->title . ' ' . $this->queriedContext;
+ case 'Single issue': return $this->title . ' Issue ' . $this->getInput('issue');
+ default: return parent::getName();
+ }
+ }
+
+ public function getIcon() {
+ return 'https://gogs.io/img/favicon.ico';
+ }
+
+ public function collectData() {
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request ' . $this->getURI());
+
+ $html = defaultLinkTo($html, $this->getURI());
+
+ $this->title = $html->find('[property="og:title"]', 0)->content;
+
+ switch($this->queriedContext) {
+ case 'Commits': {
+ $this->collectCommitsData($html);
+ } break;
+ case 'Issues': {
+ $this->collectIssuesData($html);
+ } break;
+ case 'Single issue': {
+ $this->collectSingleIssueData($html);
+ } break;
+ case 'Releases': {
+ $this->collectReleasesData($html);
+ } break;
+ }
+
+ }
+
+ protected function collectCommitsData($html) {
+ $commits = $html->find('#commits-table tbody tr')
+ or returnServerError('Unable to find commits');
+
+ foreach($commits as $commit) {
+ $this->items[] = array(
+ 'uri' => $commit->find('a.sha', 0)->href,
+ 'title' => $commit->find('.message span', 0)->plaintext,
+ 'author' => $commit->find('.author', 0)->plaintext,
+ 'timestamp' => $commit->find('.time-since', 0)->title,
+ 'uid' => $commit->find('.sha', 0)->plaintext,
+ );
+ }
+ }
+
+ protected function collectIssuesData($html) {
+ $issues = $html->find('.issue.list li')
+ or returnServerError('Unable to find issues');
+
+ foreach($issues as $issue) {
+ $uri = $issue->find('a', 0)->href;
+
+ $item = array(
+ 'uri' => $uri,
+ 'title' => $issue->find('.label', 0)->plaintext . ' | ' . $issue->find('a.title', 0)->plaintext,
+ 'author' => $issue->find('.desc a', 0)->plaintext,
+ 'timestamp' => $issue->find('.time-since', 0)->title,
+ 'uid' => $issue->find('.label', 0)->plaintext,
+ );
+
+ if($this->getInput('include_description')) {
+ $issue_html = getSimpleHTMLDOMCached($uri, 3600)
+ or returnServerError('Unable to load issue description');
+
+ $issue_html = defaultLinkTo($issue_html, $uri);
+
+ $item['content'] = $issue_html->find('.comment .markdown', 0);
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ protected function collectSingleIssueData($html) {
+ $comments = $html->find('.comments .comment')
+ or returnServerError('Unable to find comments');
+
+ foreach($comments as $comment) {
+ $this->items[] = array(
+ 'uri' => $comment->find('a[href*="#issue"]', 0)->href,
+ 'title' => $comment->find('span', 0)->plaintext,
+ 'author' => $comment->find('.content a', 0)->plaintext,
+ 'timestamp' => $comment->find('.time-since', 0)->title,
+ 'content' => $comment->find('.markdown', 0),
+ );
+ }
+
+ $this->items = array_reverse($this->items);
+ }
+
+ protected function collectReleasesData($html) {
+ $releases = $html->find('#release-list li')
+ or returnServerError('Unable to find releases');
+
+ foreach($releases as $release) {
+ $this->items[] = array(
+ 'uri' => $release->find('a', 0)->href,
+ 'title' => 'Release ' . $release->find('h4', 0)->plaintext,
+ );
+ }
+ }
+}
diff --git a/bridges/GooglePlusPostBridge.php b/bridges/GooglePlusPostBridge.php
deleted file mode 100644
index 7911eaf..0000000
--- a/bridges/GooglePlusPostBridge.php
+++ /dev/null
@@ -1,208 +0,0 @@
-<?php
-class GooglePlusPostBridge extends BridgeAbstract{
-
- private $title;
- private $url;
-
- const MAINTAINER = 'Grummfy, logmanoriginal';
- 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
- ),
- 'include_media' => array(
- 'name' => 'Include media',
- 'type' => 'checkbox',
- 'title' => 'Enable to include media in the feed content'
- )
- ));
-
- public function getIcon() {
- return 'https://ssl.gstatic.com/images/branding/product/ico/google_plus_alldp.ico';
- }
-
- 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;
- }
-
- $html = getSimpleHTMLDOM(static::URI . '/' . urlencode($username) . '/posts')
- or returnServerError('No results for this query.');
-
- $html = defaultLinkTo($html, static::URI);
-
- $this->title = $html->find('meta[property=og:title]', 0)->getAttribute('content');
- $this->url = $html->find('meta[property=og:url]', 0)->getAttribute('content');
-
- foreach($html->find('div[jsname=WsjYwc]') as $post) {
-
- $item = array();
-
- $item['author'] = $post->find('div div div div a', 0)->innertext;
- $item['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')));
- }
-
- $message = $post->find('div[jsname=EjRJtf]', 0);
-
- // Empty messages are not supported right now
- if(!$message) {
- continue;
- }
-
- $item['content'] = '<div style="float: left; padding: 0 10px 10px 0;"><a href="'
- . $this->url
- . '"><img align="top" alt="'
- . $item['author']
- . '" src="'
- . $post->find('div img', 0)->src
- . '" /></a></div><div>'
- . trim(strip_tags($message, '<a><p><div><img>'))
- . '</div>';
-
- // Make title at least 50 characters long, but don't add '...' if it is shorter!
- if(strlen($message->plaintext) > 50) {
- $end = strpos($message->plaintext, ' ', 50) ?: strlen($message->plaintext);
- } else {
- $end = strlen($message->plaintext);
- }
-
- if(strlen(substr($message->plaintext, 0, $end)) === strlen($message->plaintext)) {
- $item['title'] = $message->plaintext;
- } else {
- $item['title'] = substr($message->plaintext, 0, $end) . '...';
- }
-
- $media = $post->find('[jsname="MTOxpb"]', 0);
-
- if($media) {
-
- $item['enclosures'] = array();
-
- foreach($media->find('img') as $img) {
- $item['enclosures'][] = $this->fixImage($img)->src;
- }
-
- if($this->getInput('include_media') === true && count($item['enclosures'] > 0)) {
- $item['content'] .= '<div style="clear: both;"><a href="'
- . $item['enclosures'][0]
- . '"><img src="'
- . $item['enclosures'][0]
- . '" /></a></div>';
- }
-
- }
-
- // Add custom parameters (only useful for JSON or Plaintext)
- $item['fullname'] = $item['author'];
- $item['avatar'] = $post->find('div img', 0)->src;
- $item['id'] = $post->find('div div div', 0)->getAttribute('id');
- $item['content_simple'] = $message->plaintext;
-
- $this->items[] = $item;
-
- }
-
- }
-
- public function getName(){
- return $this->title ?: 'Google Plus Post Bridge';
- }
-
- public function getURI(){
- return $this->url ?: parent::getURI();
- }
-
- private function fixImage($img) {
-
- // There are certain images like .gif which link to a static picture and
- // get replaced dynamically via JS in the browser. If we want the "real"
- // image we need to account for that.
-
- $urlparts = parse_url($img->src);
-
- if(array_key_exists('host', $urlparts)) {
-
- // For some reason some URIs don't contain the scheme, assume https
- if(!array_key_exists('scheme', $urlparts)) {
- $urlparts['scheme'] = 'https';
- }
-
- $pathelements = explode('/', $urlparts['path']);
-
- switch($urlparts['host']) {
-
- case 'lh3.googleusercontent.com':
-
- if(pathinfo(end($pathelements), PATHINFO_EXTENSION)) {
-
- // The second to last element of the path specifies the
- // image format. The URL is still valid if we remove it.
- unset($pathelements[count($pathelements) - 2]);
-
- } elseif(strrpos(end($pathelements), '=') !== false) {
-
- // Some images go throug a proxy. For those images they
- // add size information after an equal sign.
- // Example: '=w530-h298-n'. Again this can safely be
- // removed to get the original image.
- $pathelements[count($pathelements) - 1] = substr(
- end($pathelements),
- 0,
- strrpos(end($pathelements), '=')
- );
-
- }
-
- break;
-
- }
-
- $urlparts['path'] = implode('/', $pathelements);
-
- }
-
- $img->src = $this->build_url($urlparts);
- return $img;
-
- }
-
- /**
- * From: https://gist.github.com/Ellrion/f51ba0d40ae1d62eeae44fd1adf7b704
- * slightly adjusted to work with PHP < 7.0
- * @param array $parts
- * @return string
- */
- private function build_url(array $parts)
- {
-
- $scheme = isset($parts['scheme']) ? ($parts['scheme'] . '://') : '';
- $host = isset($parts['host']) ? $parts['host'] : '';
- $port = isset($parts['port']) ? (':' . $parts['port']) : '';
- $user = isset($parts['user']) ? $parts['user'] : '';
- $pass = isset($parts['pass']) ? (':' . $parts['pass']) : '';
- $pass = ($user || $pass) ? ($pass . '@') : '';
- $path = isset($parts['path']) ? $parts['path'] : '';
- $query = isset($parts['query']) ? ('?' . $parts['query']) : '';
- $fragment = isset($parts['fragment']) ? ('#' . $parts['fragment']) : '';
-
- return implode('', [$scheme, $user, $pass, $host, $port, $path, $query, $fragment]);
-
- }
-}
diff --git a/bridges/HDWallpapersBridge.php b/bridges/HDWallpapersBridge.php
index cea6e34..f1579e0 100644
--- a/bridges/HDWallpapersBridge.php
+++ b/bridges/HDWallpapersBridge.php
@@ -16,13 +16,13 @@ class HDWallpapersBridge extends BridgeAbstract {
),
'r' => array(
'name' => 'resolution',
- 'defaultValue' => '1920x1200',
- 'exampleValue' => '1920x1200, 1680x1050,…'
+ 'defaultValue' => 'HD',
+ 'exampleValue' => 'HD, 1920x1200, 1680x1050,…'
)
));
public function collectData(){
- $category = $this->category;
+ $category = $this->getInput('c');
if(strrpos($category, 'wallpapers') !== strlen($category) - strlen('wallpapers')) {
$category .= '-desktop-wallpapers';
}
@@ -45,13 +45,12 @@ class HDWallpapersBridge extends BridgeAbstract {
$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['title'] = $element->find('em1', 0)->text();
$item['content'] = $item['title']
. '<br><a href="'
. $item['uri']
@@ -60,6 +59,7 @@ class HDWallpapersBridge extends BridgeAbstract {
. $thumbnail->src
. '" /></a>';
+ $item['enclosures'] = array($item['uri']);
$this->items[] = $item;
$num++;
diff --git a/bridges/HaveIBeenPwnedBridge.php b/bridges/HaveIBeenPwnedBridge.php
new file mode 100644
index 0000000..96dc7b2
--- /dev/null
+++ b/bridges/HaveIBeenPwnedBridge.php
@@ -0,0 +1,138 @@
+<?php
+class HaveIBeenPwnedBridge extends BridgeAbstract {
+ const NAME = 'Have I Been Pwned (HIBP) Bridge';
+ const URI = 'https://haveibeenpwned.com';
+ const DESCRIPTION = 'Returns list of Pwned websites';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = array(array(
+ 'order' => array(
+ 'name' => 'Order by',
+ 'type' => 'list',
+ 'values' => array(
+ 'Breach date' => 'breachDate',
+ 'Date added to HIBP' => 'dateAdded',
+ ),
+ 'defaultValue' => 'dateAdded',
+ ),
+ 'item_limit' => array(
+ 'name' => 'Limit number of returned items',
+ 'type' => 'number',
+ 'defaultValue' => 20,
+ )
+ ));
+
+ const CACHE_TIMEOUT = 3600;
+
+ private $breachDateRegex = '/Breach date: ([0-9]{1,2} [A-Z-a-z]+ [0-9]{4})/';
+ private $dateAddedRegex = '/Date added to HIBP: ([0-9]{1,2} [A-Z-a-z]+ [0-9]{4})/';
+ private $accountsRegex = '/Compromised accounts: ([0-9,]+)/';
+
+ private $breaches = array();
+
+ public function collectData() {
+
+ $html = getSimpleHTMLDOM(self::URI . '/PwnedWebsites')
+ or returnServerError('Could not request: ' . self::URI . '/PwnedWebsites');
+
+ $breaches = array();
+
+ foreach($html->find('div.row') as $breach) {
+ $item = array();
+
+ if ($breach->class != 'row') {
+ continue;
+ }
+
+ preg_match($this->breachDateRegex, $breach->find('p', 1)->plaintext, $breachDate)
+ or returnServerError('Could not extract details');
+
+ preg_match($this->dateAddedRegex, $breach->find('p', 1)->plaintext, $dateAdded)
+ or returnServerError('Could not extract details');
+
+ preg_match($this->accountsRegex, $breach->find('p', 1)->plaintext, $accounts)
+ or returnServerError('Could not extract details');
+
+ $permalink = $breach->find('p', 1)->find('a', 0)->href;
+
+ // Remove permalink
+ $breach->find('p', 1)->find('a', 0)->outertext = '';
+
+ $item['title'] = html_entity_decode($breach->find('h3', 0)->plaintext, ENT_QUOTES)
+ . ' - ' . $accounts[1] . ' breached accounts';
+ $item['dateAdded'] = strtotime($dateAdded[1]);
+ $item['breachDate'] = strtotime($breachDate[1]);
+ $item['uri'] = self::URI . '/PwnedWebsites' . $permalink;
+
+ $item['content'] = '<p>' . $breach->find('p', 0)->innertext . '</p>';
+ $item['content'] .= '<p>' . $this->breachType($breach) . '</p>';
+ $item['content'] .= '<p>' . $breach->find('p', 1)->innertext . '</p>';
+
+ $this->breaches[] = $item;
+ }
+
+ $this->orderBreaches();
+ $this->createItems();
+ }
+
+ /**
+ * Extract data breach type(s)
+ */
+ private function breachType($breach) {
+
+ $content = '';
+
+ if ($breach->find('h3 > i', 0)) {
+
+ foreach ($breach->find('h3 > i') as $i) {
+ $content .= $i->title . '.<br>';
+ }
+
+ }
+
+ return $content;
+
+ }
+
+ /**
+ * Order Breaches by date added or date breached
+ */
+ private function orderBreaches() {
+
+ $sortBy = $this->getInput('order');
+ $sort = array();
+
+ foreach ($this->breaches as $key => $item) {
+ $sort[$key] = $item[$sortBy];
+ }
+
+ array_multisort($sort, SORT_DESC, $this->breaches);
+
+ }
+
+ /**
+ * Create items from breaches array
+ */
+ private function createItems() {
+
+ $limit = $this->getInput('item_limit');
+
+ if ($limit < 1) {
+ $limit = 20;
+ }
+
+ foreach ($this->breaches as $breach) {
+ $item = array();
+
+ $item['title'] = $breach['title'];
+ $item['timestamp'] = $breach[$this->getInput('order')];
+ $item['uri'] = $breach['uri'];
+ $item['content'] = $breach['content'];
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= $limit) {
+ break;
+ }
+ }
+ }
+}
diff --git a/bridges/HeiseBridge.php b/bridges/HeiseBridge.php
new file mode 100644
index 0000000..1d9d802
--- /dev/null
+++ b/bridges/HeiseBridge.php
@@ -0,0 +1,75 @@
+<?php
+
+class HeiseBridge extends FeedExpander {
+ const MAINTAINER = 'Dreckiger-Dan';
+ const NAME = 'Heise Online Bridge';
+ const URI = 'https://heise.de/';
+ const CACHE_TIMEOUT = 1800; // 30min
+ const DESCRIPTION = 'Returns the full articles instead of only the intro';
+ const PARAMETERS = array(array(
+ 'category' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => array(
+ 'Alle News'
+ => 'https://www.heise.de/newsticker/heise-atom.xml',
+ 'Top-News'
+ => 'https://www.heise.de/newsticker/heise-top-atom.xml',
+ 'Internet-Störungen'
+ => 'https://www.heise.de/netze/netzwerk-tools/imonitor-internet-stoerungen/feed/aktuelle-meldungen/',
+ 'Alle News von heise Developer'
+ => 'https://www.heise.de/developer/rss/news-atom.xml'
+ )
+ ),
+ 'limit' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'required' => false,
+ 'title' => 'Specify number of full articles to return',
+ 'defaultValue' => 5
+ )
+ ));
+ const LIMIT = 5;
+
+ public function collectData() {
+ $this->collectExpandableDatas(
+ $this->getInput('category'),
+ $this->getInput('limit') ?: static::LIMIT
+ );
+ }
+
+ protected function parseItem($feedItem) {
+ $item = parent::parseItem($feedItem);
+ $uri = $item['uri'];
+
+ do {
+ $article = getSimpleHTMLDOMCached($uri)
+ or returnServerError('Could not open article: ' . $uri);
+
+ $article = defaultLinkTo($article, $uri);
+ $item = $this->addArticleToItem($item, $article);
+
+ if($next = $article->find('.pagination a[rel="next"]', 0))
+ $uri = $next->href;
+ } while ($next);
+
+ return $item;
+ }
+
+ private function addArticleToItem($item, $article) {
+ if($author = $article->find('[itemprop="author"]', 0))
+ $item['author'] = $author->plaintext;
+
+ $content = $article->find('div[class*="article-content"]', 0);
+
+ foreach($content->find('p, h3, ul, table, pre, img') as $element) {
+ $item['content'] .= $element;
+ }
+
+ foreach($content->find('img') as $img) {
+ $item['enclosures'][] = $img->src;
+ }
+
+ return $item;
+ }
+}
diff --git a/bridges/HentaiHavenBridge.php b/bridges/HentaiHavenBridge.php
index 21a0ff5..0e4fda4 100644
--- a/bridges/HentaiHavenBridge.php
+++ b/bridges/HentaiHavenBridge.php
@@ -3,7 +3,7 @@ class HentaiHavenBridge extends BridgeAbstract {
const MAINTAINER = 'albirew';
const NAME = 'Hentai Haven';
- const URI = 'http://hentaihaven.org/';
+ const URI = 'https://hentaihaven.org/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Returns releases from Hentai Haven';
diff --git a/bridges/HotUKDealsBridge.php b/bridges/HotUKDealsBridge.php
index 8c1b3bd..ed2d28a 100644
--- a/bridges/HotUKDealsBridge.php
+++ b/bridges/HotUKDealsBridge.php
@@ -17,13 +17,11 @@ class HotUKDealsBridge extends PepperBridgeAbstract {
'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',
@@ -43,7 +41,6 @@ class HotUKDealsBridge extends PepperBridgeAbstract {
'group' => array(
'name' => 'Group',
'type' => 'list',
- 'required' => true,
'title' => 'Group whose deals must be displayed',
'values' => array(
'2DS' => '2ds',
@@ -1317,7 +1314,6 @@ class HotUKDealsBridge extends PepperBridgeAbstract {
'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',
diff --git a/bridges/IGNBridge.php b/bridges/IGNBridge.php
new file mode 100644
index 0000000..6a254b3
--- /dev/null
+++ b/bridges/IGNBridge.php
@@ -0,0 +1,55 @@
+<?php
+class IGNBridge extends FeedExpander {
+
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'IGN Bridge';
+ const URI = 'https://www.ign.com/';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'RSS Feed For IGN';
+
+ public function collectData(){
+ $this->collectExpandableDatas('http://feeds.ign.com/ign/all', 15);
+ }
+
+ // IGNs feed is both hidden and incomplete. This bridge tries to fix this.
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+
+ /*
+ * NOTE: Though articles and wiki/howtos have seperate styles of pages, there is no mechanism
+ * for handling them seperately as it just ignores the DOM querys which it does not find.
+ * (and their scraping)
+ */
+
+ // For Articles
+ $article = $articlePage->find('section.article-page', 0);
+ // add in verdicts in articles, reviews etc
+ foreach($articlePage->find('div.article-section') as $element) {
+ $article = $article . $element;
+ }
+
+ // For Wikis and HowTos
+ $uselessWikiElements = array(
+ '.wiki-page-tools',
+ '.feedback-container',
+ '.paging-container'
+ );
+ foreach($articlePage->find('.wiki-page') as $wikiContents) {
+ $copy = clone $wikiContents;
+ // Remove useless elements present in IGN wiki/howtos
+ foreach($uselessWikiElements as $uslElement) {
+ $toRemove = $wikiContents->find($uslElement, 0);
+ $copy = str_replace($toRemove, '', $copy);
+ }
+ $article = $article . $copy;
+ }
+
+ // Add content to feed
+ $item['content'] = $article;
+ return $item;
+ }
+}
diff --git a/bridges/IndeedBridge.php b/bridges/IndeedBridge.php
new file mode 100644
index 0000000..c1d0cfd
--- /dev/null
+++ b/bridges/IndeedBridge.php
@@ -0,0 +1,245 @@
+<?php
+class IndeedBridge extends BridgeAbstract {
+
+ const NAME = 'Indeed';
+ const URI = 'https://www.indeed.com/';
+ const DESCRIPTION = 'Returns reviews and comments for a company of your choice';
+ const MAINTAINER = 'logmanoriginal';
+ const CACHE_TIMEOUT = 14400; // 4 hours
+
+ const PARAMETERS = array(
+ array(
+ 'c' => array(
+ 'name' => 'Company',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Company name',
+ 'exampleValue' => 'GitHub',
+ )
+ ),
+ 'global' => array(
+ 'language' => array(
+ 'name' => 'Language Code',
+ 'type' => 'list',
+ 'title' => 'Choose your language code',
+ 'defaultValue' => 'en-US',
+ 'values' => array(
+ 'es-AR' => 'es-AR',
+ 'de-AT' => 'de-AT',
+ 'en-AU' => 'en-AU',
+ 'nl-BE' => 'nl-BE',
+ 'fr-BE' => 'fr-BE',
+ 'pt-BR' => 'pt-BR',
+ 'en-CA' => 'en-CA',
+ 'fr-CA' => 'fr-CA',
+ 'de-CH' => 'de-CH',
+ 'fr-CH' => 'fr-CH',
+ 'es-CL' => 'es-CL',
+ 'zh-CN' => 'zh-CN',
+ 'es-CO' => 'es-CO',
+ 'de-DE' => 'de-DE',
+ 'es-ES' => 'es-ES',
+ 'fr-FR' => 'fr-FR',
+ 'en-GB' => 'en-GB',
+ 'en-HK' => 'en-HK',
+ 'en-IE' => 'en-IE',
+ 'en-IN' => 'en-IN',
+ 'it-IT' => 'it-IT',
+ 'ja-JP' => 'ja-JP',
+ 'ko-KR' => 'ko-KR',
+ 'es-MX' => 'es-MX',
+ 'nl-NL' => 'nl-NL',
+ 'pl-PL' => 'pl-PL',
+ 'en-SG' => 'en-SG',
+ 'en-US' => 'en-US',
+ 'en-ZA' => 'en-ZA',
+ 'en-AE' => 'en-AE',
+ 'da-DK' => 'da-DK',
+ 'in-ID' => 'in-ID',
+ 'en-MY' => 'en-MY',
+ 'es-PE' => 'es-PE',
+ 'en-PH' => 'en-PH',
+ 'en-PK' => 'en-PK',
+ 'ro-RO' => 'ro-RO',
+ 'ru-RU' => 'ru-RU',
+ 'tr-TR' => 'tr-TR',
+ 'zh-TW' => 'zh-TW',
+ 'vi-VN' => 'vi-VN',
+ 'en-VN' => 'en-VN',
+ 'ar-EG' => 'ar-EG',
+ 'fr-MA' => 'fr-MA',
+ 'en-NG' => 'en-NG',
+ )
+ ),
+ 'limit' => array(
+ 'name' => 'Limit',
+ 'type' => 'number',
+ 'title' => 'Maximum number of items to return',
+ 'exampleValue' => 20,
+ )
+ )
+ );
+
+ const SITES = array(
+ 'es-AR' => 'https://ar.indeed.com/',
+ 'de-AT' => 'https://at.indeed.com/',
+ 'en-AU' => 'https://au.indeed.com/',
+ 'nl-BE' => 'https://be.indeed.com/',
+ 'fr-BE' => 'https://emplois.be.indeed.com/',
+ 'pt-BR' => 'https://www.indeed.com.br/',
+ 'en-CA' => 'https://ca.indeed.com/',
+ 'fr-CA' => 'https://emplois.ca.indeed.com/',
+ 'de-CH' => 'https://www.indeed.ch/',
+ 'fr-CH' => 'https://emplois.indeed.ch/',
+ 'es-CL' => 'https://www.indeed.cl/',
+ 'zh-CN' => 'https://cn.indeed.com/',
+ 'es-CO' => 'https://co.indeed.com/',
+ 'de-DE' => 'https://de.indeed.com/',
+ 'es-ES' => 'https://www.indeed.es/',
+ 'fr-FR' => 'https://www.indeed.fr/',
+ 'en-GB' => 'https://www.indeed.co.uk/',
+ 'en-HK' => 'https://www.indeed.hk/',
+ 'en-IE' => 'https://ie.indeed.com/',
+ 'en-IN' => 'https://www.indeed.co.in/',
+ 'it-IT' => 'https://it.indeed.com/',
+ 'ja-JP' => 'https://jp.indeed.com/',
+ 'ko-KR' => 'https://kr.indeed.com/',
+ 'es-MX' => 'https://www.indeed.com.mx/',
+ 'nl-NL' => 'https://www.indeed.nl/',
+ 'pl-PL' => 'https://pl.indeed.com/',
+ 'en-SG' => 'https://www.indeed.com.sg/',
+ 'en-US' => 'https://www.indeed.com/',
+ 'en-ZA' => 'https://www.indeed.co.za/',
+ 'en-AE' => 'https://www.indeed.ae/',
+ 'da-DK' => 'https://dk.indeed.com/',
+ 'in-ID' => 'https://id.indeed.com/',
+ 'en-MY' => 'https://www.indeed.com.my/',
+ 'es-PE' => 'https://www.indeed.com.pe/',
+ 'en-PH' => 'https://www.indeed.com.ph/',
+ 'en-PK' => 'https://www.indeed.com.pk/',
+ 'ro-RO' => 'https://ro.indeed.com/',
+ 'ru-RU' => 'https://ru.indeed.com/',
+ 'tr-TR' => 'https://tr.indeed.com/',
+ 'zh-TW' => 'https://tw.indeed.com/',
+ 'vi-VN' => 'https://vn.indeed.com/',
+ 'en-VN' => 'https://jobs.vn.indeed.com/',
+ 'ar-EG' => 'https://eg.indeed.com/',
+ 'fr-MA' => 'https://ma.indeed.com/',
+ 'en-NG' => 'https://ng.indeed.com/',
+ );
+
+ private $title;
+
+ public function collectData() {
+
+ $url = $this->getURI();
+ $limit = $this->getInput('limit') ?: 20;
+
+ do {
+
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Could not request ' . $url);
+
+ $html = defaultLinkTo($html, $url);
+
+ $this->title = $html->find('h1', 0)->innertext;
+
+ // Use local translation of the word "Rating"
+ $rating_local = $html->find('a[data-id="rating_desc"]', 0)->plaintext;
+
+ foreach($html->find('#cmp-content [id^="cmp-review-"]') as $review) {
+ $item = array();
+
+ $rating = $review->find('.cmp-ratingNumber', 0)->plaintext;
+ $title = $review->find('.cmp-review-title > span', 0)->plaintext;
+ $comment = $this->beautifyComment($review->find('.cmp-review-content-container', 0));
+
+ $item['uri'] = $review->find('.cmp-review-share-popup-item-link--copylink', 0)->href;
+ $item['title'] = "{$rating_local} {$rating} / {$title}";
+ $item['timestamp'] = $review->find('.cmp-review-date-created', 0)->plaintext;
+ $item['author'] = $review->find('.cmp-reviewer', 0)->plaintext;
+ $item['content'] = $comment;
+ //$item['enclosures']
+ $item['categories'][] = $review->find('.cmp-reviewer-job-location', 0)->plaintext;
+ //$item['uid']
+
+ $this->items[] = $item;
+
+ if(count($this->items) >= $limit) {
+ break;
+ }
+ }
+
+ // Break if no more pages available.
+ if($next = $html->find('a[data-tn-element="next-page"]', 0)) {
+ $url = $next->href;
+ } else {
+ break;
+ }
+
+ } while(count($this->items) < $limit);
+
+ }
+
+ public function getURI() {
+ if($this->getInput('language')
+ && $this->getInput('c')) {
+ return self::SITES[$this->getInput('language')]
+ . 'cmp/'
+ . urlencode($this->getInput('c'))
+ . '/reviews';
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName() {
+ return $this->title ?: parent::getName();
+ }
+
+ public function detectParameters($url) {
+ /**
+ * Expected: https://<...>.indeed.<...>/cmp/<company>[/reviews][/...]
+ *
+ * Note that most users will be redirected to their localized version
+ * of the page, which adds the language code to the host. For example,
+ * "en.indeed.com" or "www.indeed.fr" (see link[rel="alternate"]). At
+ * least each of the sites have ".indeed." in the name.
+ */
+
+ if(filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false
+ || stristr($url, '.indeed.') === false) {
+ return null;
+ }
+
+ $url_components = parse_url($url);
+ $path_segments = array_values(array_filter(explode('/', $url_components['path'])));
+
+ if(count($path_segments) < 2 || $path_segments[0] !== 'cmp') {
+ return null;
+ }
+
+ $language = array_search('https://' . $url_components['host'] . '/', self::SITES);
+ if($language === false) {
+ return null;
+ }
+
+ $limit = self::PARAMETERS['global']['limit']['defaultValue'] ?: 20;
+ $company = $path_segments[1];
+
+ return array(
+ 'c' => $company,
+ 'language' => $language,
+ 'limit' => $limit,
+ );
+ }
+
+ private function beautifyComment($comment) {
+ foreach($comment->find('.cmp-bold') as $bold) {
+ $bold->tag = 'strong';
+ $bold->removeClass('cmp-bold');
+ }
+
+ return $comment;
+ }
+}
diff --git a/bridges/InstagramBridge.php b/bridges/InstagramBridge.php
index 317fb12..77a48e6 100644
--- a/bridges/InstagramBridge.php
+++ b/bridges/InstagramBridge.php
@@ -42,6 +42,38 @@ class InstagramBridge extends BridgeAbstract {
);
+ const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951';
+ const TAG_QUERY_HASH = '174a5243287c5f3a7de741089750ab3b';
+ const STORY_QUERY_HASH = '865589822932d1b43dfe312121dd353a';
+
+ protected function getInstagramUserId($username) {
+
+ if(is_numeric($username)) return $username;
+
+ $cacheFac = new CacheFactory();
+ $cacheFac->setWorkingDir(PATH_LIB_CACHES);
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope(get_called_class());
+ $cache->setKey([$username]);
+ $key = $cache->loadData();
+
+ if($key == null) {
+ $data = getContents(self::URI . 'web/search/topsearch/?query=' . $username);
+
+ foreach(json_decode($data)->users as $user) {
+ if($user->user->username === $username) {
+ $key = $user->user->pk;
+ }
+ }
+ if($key == null) {
+ returnServerError('Unable to find username in search result.');
+ }
+ $cache->saveData($key);
+ }
+ return $key;
+
+ }
+
public function collectData(){
if(is_null($this->getInput('u')) && $this->getInput('media_type') == 'story') {
@@ -51,9 +83,9 @@ class InstagramBridge extends BridgeAbstract {
$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;
+ $userMedia = $data->data->user->edge_owner_to_timeline_media->edges;
} elseif(!is_null($this->getInput('h'))) {
- $userMedia = $data->entry_data->TagPage[0]->graphql->hashtag->edge_hashtag_to_media->edges;
+ $userMedia = $data->data->hashtag->edge_hashtag_to_media->edges;
} elseif(!is_null($this->getInput('l'))) {
$userMedia = $data->entry_data->LocationsPage[0]->graphql->location->edge_location_to_media->edges;
}
@@ -89,7 +121,7 @@ class InstagramBridge extends BridgeAbstract {
if (isset($media->edge_media_to_caption->edges[0]->node->text)) {
$textContent = $media->edge_media_to_caption->edges[0]->node->text;
} else {
- $textContent = basename($media->display_url);
+ $textContent = '(no text)';
}
$item['title'] = ($media->is_video ? '▶ ' : '') . trim($textContent);
@@ -99,14 +131,16 @@ class InstagramBridge extends BridgeAbstract {
}
if(!is_null($this->getInput('u')) && $media->__typename == 'GraphSidecar') {
+
$data = $this->getInstagramStory($item['uri']);
$item['content'] = $data[0];
$item['enclosures'] = $data[1];
} else {
+ $mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l';
$item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
- $item['content'] .= '<img src="' . htmlentities($media->display_url) . '" alt="' . $item['title'] . '" />';
+ $item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />';
$item['content'] .= '</a><br><br>' . nl2br(htmlentities($textContent));
- $item['enclosures'] = array($media->display_url);
+ $item['enclosures'] = array($mediaURI);
}
$item['timestamp'] = $media->taken_at_timestamp;
@@ -117,8 +151,15 @@ class InstagramBridge extends BridgeAbstract {
protected function getInstagramStory($uri) {
- $data = $this->getInstagramJSON($uri);
- $mediaInfo = $data->entry_data->PostPage[0]->graphql->shortcode_media;
+ $shortcode = explode('/', $uri)[4];
+ $data = getContents(self::URI .
+ 'graphql/query/?query_hash=' .
+ self::STORY_QUERY_HASH .
+ '&variables={"shortcode"%3A"' .
+ $shortcode .
+ '"}');
+
+ $mediaInfo = json_decode($data)->data->shortcode_media;
//Process the first element, that isn't in the node graph
if (count($mediaInfo->edge_media_to_caption->edges) > 0) {
@@ -144,13 +185,38 @@ class InstagramBridge extends BridgeAbstract {
protected function getInstagramJSON($uri) {
- $html = getContents($uri)
- or returnServerError('Could not request Instagram.');
- $scriptRegex = '/window\._sharedData = (.*);<\/script>/';
+ if(!is_null($this->getInput('u'))) {
+
+ $userId = $this->getInstagramUserId($this->getInput('u'));
- preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0);
+ $data = getContents(self::URI .
+ 'graphql/query/?query_hash=' .
+ self::USER_QUERY_HASH .
+ '&variables={"id"%3A"' .
+ $userId .
+ '"%2C"first"%3A10}');
+ return json_decode($data);
- return json_decode($matches[1][0]);
+ } elseif(!is_null($this->getInput('h'))) {
+ $data = getContents(self::URI .
+ 'graphql/query/?query_hash=' .
+ self::TAG_QUERY_HASH .
+ '&variables={"tag_name"%3A"' .
+ $this->getInput('h') .
+ '"%2C"first"%3A10}');
+ return json_decode($data);
+
+ } else {
+
+ $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]);
+
+ }
}
diff --git a/bridges/InstructablesBridge.php b/bridges/InstructablesBridge.php
index 05f1a91..e28c34b 100644
--- a/bridges/InstructablesBridge.php
+++ b/bridges/InstructablesBridge.php
@@ -1,8 +1,7 @@
<?php
/**
* This class implements a bridge for http://www.instructables.com, supporting
-* general feeds and feeds by category. Instructables doesn't support HTTPS as
-* of now (23.06.2018), so all connections are insecure!
+* general feeds and feeds by category.
*
* Remarks:
* - For some reason it is very important to have the category URI end with a
@@ -13,7 +12,7 @@
*/
class InstructablesBridge extends BridgeAbstract {
const NAME = 'Instructables Bridge';
- const URI = 'http://www.instructables.com';
+ const URI = 'https://www.instructables.com';
const DESCRIPTION = 'Returns general feeds and feeds by category';
const MAINTAINER = 'logmanoriginal';
const PARAMETERS = array(
@@ -21,226 +20,206 @@ class InstructablesBridge extends BridgeAbstract {
'category' => array(
'name' => 'Category',
'type' => 'list',
- 'required' => true,
'values' => array(
- 'Play' => array(
- 'All' => '/play/',
- 'KNEX' => '/play/knex/',
- 'Offbeat' => '/play/offbeat/',
- 'Lego' => '/play/lego/',
- 'Airsoft' => '/play/airsoft/',
- 'Card Games' => '/play/card-games/',
- 'Guitars' => '/play/guitars/',
- 'Instruments' => '/play/instruments/',
- 'Magic Tricks' => '/play/magic-tricks/',
- 'Minecraft' => '/play/minecraft/',
- 'Music' => '/play/music/',
- 'Nerf' => '/play/nerf/',
- 'Nintendo' => '/play/nintendo/',
- 'Office Supplies' => '/play/office-supplies/',
- 'Paintball' => '/play/paintball/',
- 'Paper Airplanes' => '/play/paper-airplanes/',
- 'Party Tricks' => '/play/party-tricks/',
- 'PlayStation' => '/play/playstation/',
- 'Pranks and Humor' => '/play/pranks-and-humor/',
- 'Puzzles' => '/play/puzzles/',
- 'Siege Engines' => '/play/siege-engines/',
- 'Sports' => '/play/sports/',
- 'Table Top' => '/play/table-top/',
- 'Toys' => '/play/toys/',
- 'Video Games' => '/play/video-games/',
- 'Wii' => '/play/wii/',
- 'Xbox' => '/play/xbox/',
- 'Yo-Yo' => '/play/yo-yo/',
+ 'Circuits' => array(
+ 'All' => '/circuits/',
+ 'Apple' => '/circuits/apple/projects/',
+ 'Arduino' => '/circuits/arduino/projects/',
+ 'Art' => '/circuits/art/projects/',
+ 'Assistive Tech' => '/circuits/assistive-tech/projects/',
+ 'Audio' => '/circuits/audio/projects/',
+ 'Cameras' => '/circuits/cameras/projects/',
+ 'Clocks' => '/circuits/clocks/projects/',
+ 'Computers' => '/circuits/computers/projects/',
+ 'Electronics' => '/circuits/electronics/projects/',
+ 'Gadgets' => '/circuits/gadgets/projects/',
+ 'Lasers' => '/circuits/lasers/projects/',
+ 'LEDs' => '/circuits/leds/projects/',
+ 'Linux' => '/circuits/linux/projects/',
+ 'Microcontrollers' => '/circuits/microcontrollers/projects/',
+ 'Microsoft' => '/circuits/microsoft/projects/',
+ 'Mobile' => '/circuits/mobile/projects/',
+ 'Raspberry Pi' => '/circuits/raspberry-pi/projects/',
+ 'Remote Control' => '/circuits/remote-control/projects/',
+ 'Reuse' => '/circuits/reuse/projects/',
+ 'Robots' => '/circuits/robots/projects/',
+ 'Sensors' => '/circuits/sensors/projects/',
+ 'Software' => '/circuits/software/projects/',
+ 'Soldering' => '/circuits/soldering/projects/',
+ 'Speakers' => '/circuits/speakers/projects/',
+ 'Tools' => '/circuits/tools/projects/',
+ 'USB' => '/circuits/usb/projects/',
+ 'Wearables' => '/circuits/wearables/projects/',
+ 'Websites' => '/circuits/websites/projects/',
+ 'Wireless' => '/circuits/wireless/projects/',
+ ),
+ 'Workshop' => array(
+ 'All' => '/workshop/',
+ '3D Printing' => '/workshop/3d-printing/projects/',
+ 'Cars' => '/workshop/cars/projects/',
+ 'CNC' => '/workshop/cnc/projects/',
+ 'Electric Vehicles' => '/workshop/electric-vehicles/projects/',
+ 'Energy' => '/workshop/energy/projects/',
+ 'Furniture' => '/workshop/furniture/projects/',
+ 'Home Improvement' => '/workshop/home-improvement/projects/',
+ 'Home Theater' => '/workshop/home-theater/projects/',
+ 'Hydroponics' => '/workshop/hydroponics/projects/',
+ 'Knives' => '/workshop/knives/projects/',
+ 'Laser Cutting' => '/workshop/laser-cutting/projects/',
+ 'Lighting' => '/workshop/lighting/projects/',
+ 'Metalworking' => '/workshop/metalworking/projects/',
+ 'Molds & Casting' => '/workshop/molds-and-casting/projects/',
+ 'Motorcycles' => '/workshop/motorcycles/projects/',
+ 'Organizing' => '/workshop/organizing/projects/',
+ 'Pallets' => '/workshop/pallets/projects/',
+ 'Repair' => '/workshop/repair/projects/',
+ 'Science' => '/workshop/science/projects/',
+ 'Shelves' => '/workshop/shelves/projects/',
+ 'Solar' => '/workshop/solar/projects/',
+ 'Tools' => '/workshop/tools/projects/',
+ 'Woodworking' => '/workshop/woodworking/projects/',
+ 'Workbenches' => '/workshop/workbenches/projects/',
),
'Craft' => array(
'All' => '/craft/',
- 'Art' => '/craft/art/',
- 'Sewing' => '/craft/sewing/',
- 'Paper' => '/craft/paper/',
- 'Jewelry' => '/craft/jewelry/',
- 'Fashion' => '/craft/fashion/',
- 'Books & Journals' => '/craft/books-and-journals/',
- 'Cards' => '/craft/cards/',
- 'Clay' => '/craft/clay/',
- 'Duct Tape' => '/craft/duct-tape/',
- 'Embroidery' => '/craft/embroidery/',
- 'Felt' => '/craft/felt/',
- 'Fiber Arts' => '/craft/fiber-arts/',
- 'Gifts & Wrapping' => '/craft/gifts-and-wrapping/',
- 'Knitting & Crocheting' => '/craft/knitting-and-crocheting/',
- 'Leather' => '/craft/leather/',
- 'Mason Jars' => '/craft/mason-jars/',
- 'No-Sew' => '/craft/no-sew/',
- 'Parties & Weddings' => '/craft/parties-and-weddings/',
- 'Print Making' => '/craft/print-making/',
- 'Soap' => '/craft/soap/',
- 'Wallets' => '/craft/wallets/',
+ 'Art' => '/craft/art/projects/',
+ 'Books & Journals' => '/craft/books-and-journals/projects/',
+ 'Cardboard' => '/craft/cardboard/projects/',
+ 'Cards' => '/craft/cards/projects/',
+ 'Clay' => '/craft/clay/projects/',
+ 'Costumes & Cosplay' => '/craft/costumes-and-cosplay/projects/',
+ 'Digital Graphics' => '/craft/digital-graphics/projects/',
+ 'Duct Tape' => '/craft/duct-tape/projects/',
+ 'Embroidery' => '/craft/embroidery/projects/',
+ 'Fashion' => '/craft/fashion/projects/',
+ 'Felt' => '/craft/felt/projects/',
+ 'Fiber Arts' => '/craft/fiber-arts/projects/',
+ 'Gift Wrapping' => '/craft/gift-wrapping/projects/',
+ 'Jewelry' => '/craft/jewelry/projects/',
+ 'Knitting & Crochet' => '/craft/knitting-and-crochet/projects/',
+ 'Leather' => '/craft/leather/projects/',
+ 'Mason Jars' => '/craft/mason-jars/projects/',
+ 'No-Sew' => '/craft/no-sew/projects/',
+ 'Paper' => '/craft/paper/projects/',
+ 'Parties & Weddings' => '/craft/parties-and-weddings/projects/',
+ 'Photography' => '/craft/photography/projects/',
+ 'Printmaking' => '/craft/printmaking/projects/',
+ 'Reuse' => '/craft/reuse/projects/',
+ 'Sewing' => '/craft/sewing/projects/',
+ 'Soapmaking' => '/craft/soapmaking/projects/',
+ 'Wallets' => '/craft/wallets/projects/',
),
- 'Technology' => array(
- 'All' => '/technology/',
- 'Electronics' => '/technology/electronics/',
- 'Arduino' => '/technology/arduino/',
- 'Photography' => '/technology/photography/',
- 'Leds' => '/technology/leds/',
- 'Science' => '/technology/science/',
- 'Reuse' => '/technology/reuse/',
- 'Apple' => '/technology/apple/',
- 'Computers' => '/technology/computers/',
- '3D Printing' => '/technology/3D-Printing/',
- 'Robots' => '/technology/robots/',
- 'Art' => '/technology/art/',
- 'Assistive Tech' => '/technology/assistive-technology/',
- 'Audio' => '/technology/audio/',
- 'Clocks' => '/technology/clocks/',
- 'CNC' => '/technology/cnc/',
- 'Digital Graphics' => '/technology/digital-graphics/',
- 'Gadgets' => '/technology/gadgets/',
- 'Kits' => '/technology/kits/',
- 'Laptops' => '/technology/laptops/',
- 'Lasers' => '/technology/lasers/',
- 'Linux' => '/technology/linux/',
- 'Microcontrollers' => '/technology/microcontrollers/',
- 'Microsoft' => '/technology/microsoft/',
- 'Mobile' => '/technology/mobile/',
- 'Raspberry Pi' => '/technology/raspberry-pi/',
- 'Remote Control' => '/technology/remote-control/',
- 'Sensors' => '/technology/sensors/',
- 'Software' => '/technology/software/',
- 'Soldering' => '/technology/soldering/',
- 'Speakers' => '/technology/speakers/',
- 'Steampunk' => '/technology/steampunk/',
- 'Tools' => '/technology/tools/',
- 'USB' => '/technology/usb/',
- 'Wearables' => '/technology/wearables/',
- 'Websites' => '/technology/websites/',
- 'Wireless' => '/technology/wireless/',
- ),
- 'Workshop' => array(
- 'All' => '/workshop/',
- 'Woodworking' => '/workshop/woodworking/',
- 'Tools' => '/workshop/tools/',
- 'Gardening' => '/workshop/gardening/',
- 'Cars' => '/workshop/cars/',
- 'Metalworking' => '/workshop/metalworking/',
- 'Cardboard' => '/workshop/cardboard/',
- 'Electric Vehicles' => '/workshop/electric-vehicles/',
- 'Energy' => '/workshop/energy/',
- 'Furniture' => '/workshop/furniture/',
- 'Home Improvement' => '/workshop/home-improvement/',
- 'Home Theater' => '/workshop/home-theater/',
- 'Hydroponics' => '/workshop/hydroponics/',
- 'Laser Cutting' => '/workshop/laser-cutting/',
- 'Lighting' => '/workshop/lighting/',
- 'Molds & Casting' => '/workshop/molds-and-casting/',
- 'Motorcycles' => '/workshop/motorcycles/',
- 'Organizing' => '/workshop/organizing/',
- 'Pallets' => '/workshop/pallets/',
- 'Repair' => '/workshop/repair/',
- 'Shelves' => '/workshop/shelves/',
- 'Solar' => '/workshop/solar/',
- 'Workbenches' => '/workshop/workbenches/',
+ 'Cooking' => array(
+ 'All' => '/cooking/',
+ 'Bacon' => '/cooking/bacon/projects/',
+ 'BBQ & Grilling' => '/cooking/bbq-and-grilling/projects/',
+ 'Beverages' => '/cooking/beverages/projects/',
+ 'Bread' => '/cooking/bread/projects/',
+ 'Breakfast' => '/cooking/breakfast/projects/',
+ 'Cake' => '/cooking/cake/projects/',
+ 'Candy' => '/cooking/candy/projects/',
+ 'Canning & Preserving' => '/cooking/canning-and-preserving/projects/',
+ 'Cocktails & Mocktails' => '/cooking/cocktails-and-mocktails/projects/',
+ 'Coffee' => '/cooking/coffee/projects/',
+ 'Cookies' => '/cooking/cookies/projects/',
+ 'Cupcakes' => '/cooking/cupcakes/projects/',
+ 'Dessert' => '/cooking/dessert/projects/',
+ 'Homebrew' => '/cooking/homebrew/projects/',
+ 'Main Course' => '/cooking/main-course/projects/',
+ 'Pasta' => '/cooking/pasta/projects/',
+ 'Pie' => '/cooking/pie/projects/',
+ 'Pizza' => '/cooking/pizza/projects/',
+ 'Salad' => '/cooking/salad/projects/',
+ 'Sandwiches' => '/cooking/sandwiches/projects/',
+ 'Snacks & Appetizers' => '/cooking/snacks-and-appetizers/projects/',
+ 'Soups & Stews' => '/cooking/soups-and-stews/projects/',
+ 'Vegetarian & Vegan' => '/cooking/vegetarian-and-vegan/projects/',
),
- 'Home' => array(
- 'All' => '/home/',
- 'Halloween' => '/home/halloween/',
- 'Decorating' => '/home/decorating/',
- 'Organizing' => '/home/organizing/',
- 'Pets' => '/home/pets/',
- 'Life Hacks' => '/home/life-hacks/',
- 'Beauty' => '/home/beauty/',
- 'Christmas' => '/home/christmas/',
- 'Cleaning' => '/home/cleaning/',
- 'Education' => '/home/education/',
- 'Finances' => '/home/finances/',
- 'Gardening' => '/home/gardening/',
- 'Green' => '/home/green/',
- 'Health' => '/home/health/',
- 'Hiding Places' => '/home/hiding-places/',
- 'Holidays' => '/home/holidays/',
- 'Homesteading' => '/home/homesteading/',
- 'Kids' => '/home/kids/',
- 'Kitchen' => '/home/kitchen/',
- 'Life Skills' => '/home/life-skills/',
- 'Parenting' => '/home/parenting/',
- 'Pest Control' => '/home/pest-control/',
- 'Relationships' => '/home/relationships/',
- 'Reuse' => '/home/reuse/',
- 'Travel' => '/home/travel/',
+ 'Living' => array(
+ 'All' => '/living/',
+ 'Beauty' => '/living/beauty/projects/',
+ 'Christmas' => '/living/christmas/projects/',
+ 'Cleaning' => '/living/cleaning/projects/',
+ 'Decorating' => '/living/decorating/projects/',
+ 'Education' => '/living/education/projects/',
+ 'Gardening' => '/living/gardening/projects/',
+ 'Halloween' => '/living/halloween/projects/',
+ 'Health' => '/living/health/projects/',
+ 'Hiding Places' => '/living/hiding-places/projects/',
+ 'Holidays' => '/living/holidays/projects/',
+ 'Homesteading' => '/living/homesteading/projects/',
+ 'Kids' => '/living/kids/projects/',
+ 'Kitchen' => '/living/kitchen/projects/',
+ 'LEGO & KNEX' => '/living/lego-and-knex/projects/',
+ 'Life Hacks' => '/living/life-hacks/projects/',
+ 'Music' => '/living/music/projects/',
+ 'Office Supply Hacks' => '/living/office-supply-hacks/projects/',
+ 'Organizing' => '/living/organizing/projects/',
+ 'Pest Control' => '/living/pest-control/projects/',
+ 'Pets' => '/living/pets/projects/',
+ 'Pranks, Tricks, & Humor' => '/living/pranks-tricks-and-humor/projects/',
+ 'Relationships' => '/living/relationships/projects/',
+ 'Toys & Games' => '/living/toys-and-games/projects/',
+ 'Travel' => '/living/travel/projects/',
+ 'Video Games' => '/living/video-games/projects/',
),
'Outside' => array(
'All' => '/outside/',
- 'Bikes' => '/outside/bikes/',
- 'Survival' => '/outside/survival/',
- 'Backyard' => '/outside/backyard/',
- 'Beach' => '/outside/beach/',
- 'Birding' => '/outside/birding/',
- 'Boats' => '/outside/boats/',
- 'Camping' => '/outside/camping/',
- 'Climbing' => '/outside/climbing/',
- 'Fire' => '/outside/fire/',
- 'Fishing' => '/outside/fishing/',
- 'Hunting' => '/outside/hunting/',
- 'Kites' => '/outside/kites/',
- 'Knives' => '/outside/knives/',
- 'Knots' => '/outside/knots/',
- 'Paracord' => '/outside/paracord/',
- 'Rockets' => '/outside/rockets/',
- 'Skateboarding' => '/outside/skateboarding/',
- 'Snow' => '/outside/snow/',
- 'Water' => '/outside/water/',
+ 'Backyard' => '/outside/backyard/projects/',
+ 'Beach' => '/outside/beach/projects/',
+ 'Bikes' => '/outside/bikes/projects/',
+ 'Birding' => '/outside/birding/projects/',
+ 'Boats' => '/outside/boats/projects/',
+ 'Camping' => '/outside/camping/projects/',
+ 'Climbing' => '/outside/climbing/projects/',
+ 'Fire' => '/outside/fire/projects/',
+ 'Fishing' => '/outside/fishing/projects/',
+ 'Hunting' => '/outside/hunting/projects/',
+ 'Kites' => '/outside/kites/projects/',
+ 'Knots' => '/outside/knots/projects/',
+ 'Launchers' => '/outside/launchers/projects/',
+ 'Paracord' => '/outside/paracord/projects/',
+ 'Rockets' => '/outside/rockets/projects/',
+ 'Siege Engines' => '/outside/siege-engines/projects/',
+ 'Skateboarding' => '/outside/skateboarding/projects/',
+ 'Snow' => '/outside/snow/projects/',
+ 'Sports' => '/outside/sports/projects/',
+ 'Survival' => '/outside/survival/projects/',
+ 'Water' => '/outside/water/projects/',
+ ),
+ 'Makeymakey' => array(
+ 'All' => '/makeymakey/',
+ 'Makey Makey on Instructables' => '/makeymakey/',
),
- 'Food' => array(
- 'All' => '/food/',
- 'Dessert' => '/food/dessert/',
- 'Snacks & Appetizers' => '/food/snacks-and-appetizers/',
- 'Bacon' => '/food/bacon/',
- 'BBQ & Grilling' => '/food/bbq-and-grilling/',
- 'Beverages' => '/food/beverages/',
- 'Bread' => '/food/bread/',
- 'Breakfast' => '/food/breakfast/',
- 'Cake' => '/food/cake/',
- 'Candy' => '/food/candy/',
- 'Canning & Preserves' => '/food/canning-and-preserves/',
- 'Cocktails & Mocktails' => '/food/cocktails-and-mocktails/',
- 'Coffee' => '/food/coffee/',
- 'Cookies' => '/food/cookies/',
- 'Cupcakes' => '/food/cupcakes/',
- 'Homebrew' => '/food/homebrew/',
- 'Main Course' => '/food/main-course/',
- 'Pasta' => '/food/pasta/',
- 'Pie' => '/food/pie/',
- 'Pizza' => '/food/pizza/',
- 'Salad' => '/food/salad/',
- 'Sandwiches' => '/food/sandwiches/',
- 'Soups & Stews' => '/food/soups-and-stews/',
- 'Vegetarian & Vegan' => '/food/vegetarian-and-vegan/',
+ 'Teachers' => array(
+ 'All' => '/teachers/',
+ 'ELA' => '/teachers/ela/projects/',
+ 'Math' => '/teachers/math/projects/',
+ 'Science' => '/teachers/science/projects/',
+ 'Social Studies' => '/teachers/social-studies/projects/',
+ 'Engineering' => '/teachers/engineering/projects/',
+ 'Coding' => '/teachers/coding/projects/',
+ 'Electronics' => '/teachers/electronics/projects/',
+ 'Robotics' => '/teachers/robotics/projects/',
+ 'Arduino' => '/teachers/arduino/projects/',
+ 'CNC' => '/teachers/cnc/projects/',
+ 'Laser Cutting' => '/teachers/laser-cutting/projects/',
+ '3D Printing' => '/teachers/3d-printing/projects/',
+ '3D Design' => '/teachers/3d-design/projects/',
+ 'Art' => '/teachers/art/projects/',
+ 'Music' => '/teachers/music/projects/',
+ 'Theatre' => '/teachers/theatre/projects/',
+ 'Wood Shop' => '/teachers/wood-shop/projects/',
+ 'Metal Shop' => '/teachers/metal-shop/projects/',
+ 'Resources' => '/teachers/resources/projects/',
),
- 'Costumes' => array(
- 'All' => '/costumes/',
- 'Props' => '/costumes/props-and-accessories/',
- 'Animals' => '/costumes/animals/',
- 'Comics' => '/costumes/comics/',
- 'Fantasy' => '/costumes/fantasy/',
- 'For Kids' => '/costumes/for-kids/',
- 'For Pets' => '/costumes/for-pets/',
- 'Funny' => '/costumes/funny/',
- 'Games' => '/costumes/games/',
- 'Historic & Futuristic' => '/costumes/historic-and-futuristic/',
- 'Makeup' => '/costumes/makeup/',
- 'Masks' => '/costumes/masks/',
- 'Scary' => '/costumes/scary/',
- 'TV & Movies' => '/costumes/tv-and-movies/',
- 'Weapons & Armor' => '/costumes/weapons-and-armor/',
- )
),
'title' => 'Select your category (required)',
- 'defaultValue' => 'Technology'
+ 'defaultValue' => 'Circuits'
),
'filter' => array(
'name' => 'Filter',
'type' => 'list',
- 'required' => true,
'values' => array(
'Featured' => ' ',
'Recent' => 'recent/',
@@ -254,65 +233,70 @@ class InstructablesBridge extends BridgeAbstract {
)
);
- private $uri;
-
public function collectData() {
// Enable the following line to get the category list (dev mode)
// $this->listCategories();
- $this->uri = static::URI;
-
- switch($this->queriedContext) {
- case 'Category': $this->uri .= $this->getInput('category') . $this->getInput('filter');
- }
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Error loading category ' . $this->getURI());
+ $html = defaultLinkTo($html, $this->getURI());
- $html = getSimpleHTMLDOM($this->uri)
- or returnServerError('Error loading category ' . $this->uri);
+ $covers = $html->find('
+ .category-projects-list > div,
+ .category-landing-projects-list > div,
+ ');
- foreach($html->find('ul.explore-covers-list li') as $cover) {
+ foreach($covers as $cover) {
$item = array();
- $item['uri'] = static::URI . $cover->find('a.cover-image', 0)->href;
- $item['title'] = $cover->find('.title', 0)->innertext;
+ $item['uri'] = $cover->find('a.ible-title', 0)->href;
+ $item['title'] = $cover->find('a.ible-title', 0)->innertext;
$item['author'] = $this->getCategoryAuthor($cover);
$item['content'] = '<a href='
. $item['uri']
. '><img src='
- . $cover->find('a.cover-image img', 0)->src
+ . $cover->find('img', 0)->getAttribute('data-src')
. '></a>';
- $image = str_replace('.RECTANGLE1', '.LARGE', $cover->find('a.cover-image img', 0)->src);
- $item['enclosures'] = [$image];
+ $item['enclosures'][] = str_replace(
+ '.RECTANGLE1',
+ '.LARGE',
+ $cover->find('img', 0)->getAttribute('data-src')
+ );
$this->items[] = $item;
}
}
public function getName() {
- if(!is_null($this->getInput('category'))
- && !is_null($this->getInput('filter'))) {
- foreach(self::PARAMETERS[$this->queriedContext]['category']['values'] as $key => $value) {
- $subcategory = array_search($this->getInput('category'), $value);
+ switch($this->queriedContext) {
+ case 'Category': {
+ foreach(self::PARAMETERS[$this->queriedContext]['category']['values'] as $key => $value) {
+ $subcategory = array_search($this->getInput('category'), $value);
- if($subcategory !== false)
- break;
- }
+ if($subcategory !== false)
+ break;
+ }
- $filter = array_search(
- $this->getInput('filter'),
- self::PARAMETERS[$this->queriedContext]['filter']['values']
- );
+ $filter = array_search(
+ $this->getInput('filter'),
+ self::PARAMETERS[$this->queriedContext]['filter']['values']
+ );
- return $subcategory . ' (' . $filter . ') - ' . static::NAME;
+ return $subcategory . ' (' . $filter . ') - ' . static::NAME;
+ } break;
}
return parent::getName();
}
public function getURI() {
- if(!is_null($this->getInput('category'))
- && !is_null($this->getInput('filter'))) {
- return $this->uri;
+ switch($this->queriedContext) {
+ case 'Category': {
+ return self::URI
+ . $this->getInput('category')
+ . $this->getInput('filter');
+ } break;
}
return parent::getURI();
@@ -323,24 +307,32 @@ class InstructablesBridge extends BridgeAbstract {
* parameters list)
*/
private function listCategories(){
- // Use arbitrary category to receive full list
- $html = getSimpleHTMLDOM(self::URI . '/technology/');
- foreach($html->find('.channel a') as $channel) {
- $name = html_entity_decode(trim($channel->innertext));
+ // Use home page to acquire main categories
+ $html = getSimpleHTMLDOM(self::URI);
+ $html = defaultLinkTo($html, self::URI);
+
+ foreach($html->find('.home-content-explore-link') as $category) {
+
+ // Use arbitrary category to receive full list
+ $html = getSimpleHTMLDOM($category->href);
- // Remove unwanted entities
- $name = str_replace("'", '', $name);
- $name = str_replace('&#39;', '', $name);
+ foreach($html->find('.channel-thumbnail a') as $channel) {
+ $name = html_entity_decode(trim($channel->title));
- $uri = $channel->href;
+ // Remove unwanted entities
+ $name = str_replace("'", '', $name);
+ $name = str_replace('&#39;', '', $name);
- $category = explode('/', $uri)[1];
+ $uri = $channel->href;
- if(!isset($categories)
- || !array_key_exists($category, $categories)
- || !in_array($uri, $categories[$category]))
- $categories[$category][$name] = $uri;
+ $category_name = explode('/', $uri)[1];
+
+ if(!isset($categories)
+ || !array_key_exists($category_name, $categories)
+ || !in_array($uri, $categories[$category_name]))
+ $categories[$category_name][$name] = $uri;
+ }
}
// Build PHP array manually
@@ -362,9 +354,9 @@ class InstructablesBridge extends BridgeAbstract {
*/
private function getCategoryAuthor($cover) {
return '<a href='
- . static::URI . $cover->find('span.author a', 0)->href
+ . $cover->find('.ible-author a', 0)->href
. '>'
- . $cover->find('span.author a', 0)->innertext
+ . $cover->find('.ible-author a', 0)->innertext
. '</a>';
}
}
diff --git a/bridges/InternetArchiveBridge.php b/bridges/InternetArchiveBridge.php
new file mode 100644
index 0000000..dca1c32
--- /dev/null
+++ b/bridges/InternetArchiveBridge.php
@@ -0,0 +1,293 @@
+<?php
+class InternetArchiveBridge extends BridgeAbstract {
+ const NAME = 'Internet Archive Bridge';
+ const URI = 'https://archive.org';
+ const DESCRIPTION = 'Returns newest uploads, posts and more from an account';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = array(
+ 'Account' => array(
+ 'username' => array(
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => '@verifiedjoseph',
+ ),
+ 'content' => array(
+ 'name' => 'Content',
+ 'type' => 'list',
+ 'values' => array(
+ 'Uploads' => 'uploads',
+ 'Posts' => 'posts',
+ 'Reviews' => 'reviews',
+ 'Collections' => 'collections',
+ 'Web Archives' => 'web-archive',
+ ),
+ 'defaultValue' => 'uploads',
+ )
+ )
+ );
+
+ const CACHE_TIMEOUT = 900; // 15 mins
+
+ private $skipClasses = array(
+ 'item-ia mobile-header hidden-tiles',
+ 'item-ia account-ia'
+ );
+
+ public function collectData() {
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request: ' . $this->getURI());
+
+ $html = defaultLinkTo($html, $this->getURI());
+
+ if ($this->getInput('content') !== 'posts') {
+
+ $detailsDivNumber = 0;
+
+ foreach ($html->find('div.results > div[data-id]') as $index => $result) {
+ $item = array();
+
+ if (in_array($result->class, $this->skipClasses)) {
+ continue;
+ }
+
+ switch($result->class) {
+ case 'item-ia':
+
+ switch($this->getInput('content')) {
+ case 'reviews':
+ $item = $this->processReview($result);
+ break;
+ case 'uploads':
+ $item = $this->processUpload($result);
+ break;
+ }
+
+ break;
+ case 'item-ia url-item':
+ $item = $this->processWebArchives($result);
+ break;
+ case 'item-ia collection-ia':
+ $item = $this->processCollection($result);
+ break;
+ }
+
+ if ($this->getInput('content') !== 'reviews') {
+ $hiddenDetails = $this->processHiddenDetails($html, $detailsDivNumber, $item);
+
+ $this->items[] = array_merge($item, $hiddenDetails);
+ } else {
+
+ $this->items[] = $item;
+
+ }
+
+ $detailsDivNumber++;
+ }
+ }
+
+ if ($this->getInput('content') === 'posts') {
+ $this->items = $this->processPosts($html);
+ }
+ }
+
+ public function getURI() {
+
+ if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) {
+ return self::URI . '/details/' . $this->processUsername() . '&tab=' . $this->getInput('content');
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName() {
+
+ if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) {
+
+ $contentValues = array_flip(self::PARAMETERS['Account']['content']['values']);
+
+ return $contentValues[$this->getInput('content')] . ' - '
+ . $this->processUsername() . ' - Internet Archive';
+ }
+
+ return parent::getName();
+ }
+
+ private function processUsername() {
+
+ if (substr($this->getInput('username'), 0, 1) !== '@') {
+ return '@' . $this->getInput('username');
+ }
+
+ return $this->getInput('username');
+ }
+
+ private function processUpload($result) {
+
+ $item = array();
+
+ $collection = $result->find('a.stealth', 0);
+ $collectionLink = self::URI . $collection->href;
+ $collectionTitle = $collection->find('div.item-parent-ttl', 0)->plaintext;
+
+ $item['title'] = trim($result->find('div.ttl', 0)->innertext);
+ $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
+ $item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href;
+
+ if ($result->find('div.by.C.C4', 0)->children(2)) {
+ $item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext;
+ }
+
+ $item['content'] = <<<EOD
+<p>Media Type: {$result->attr['data-mediatype']}<br>
+Collection: <a href="{$collectionLink}">{$collectionTitle}</a></p>
+EOD;
+
+ $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
+
+ return $item;
+ }
+
+ private function processReview($result) {
+
+ $item = array();
+
+ $item['title'] = trim($result->find('div.ttl', 0)->innertext);
+ $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
+ $item['uri'] = $result->find('div.review-title', 0)->children(0)->href;
+
+ if ($result->find('div.by.C.C4', 0)->children(2)) {
+ $item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext;
+ }
+
+ $item['content'] = <<<EOD
+<p><strong>Subject: {$result->find('div.review-title', 0)->plaintext}</strong></p>
+<p>{$result->find('div.hidden-lists.review' , 0)->children(1)->plaintext}</p>
+EOD;
+
+ $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
+
+ return $item;
+ }
+
+ private function processWebArchives($result) {
+
+ $item = array();
+
+ $item['title'] = trim($result->find('div.ttl', 0)->plaintext);
+ $item['timestamp'] = strtotime($result->find('div.hidden-lists', 0)->children(0)->plaintext);
+ $item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href;
+
+ $item['content'] = <<<EOD
+{$this->processUsername()} archived <a href="{$item['uri']}">{$result->find('div.ttl', 0)->plaintext}</a>
+EOD;
+
+ $item['enclosures'][] = $result->find('img.item-img', 0)->source;
+
+ return $item;
+ }
+
+ private function processCollection($result) {
+
+ $item = array();
+
+ $title = trim($result->find('div.collection-title.C.C2', 0)->children(0)->plaintext);
+ $itemCount = strtolower(trim($result->find('div.num-items.topinblock', 0)->plaintext));
+
+ $item['title'] = $title . ' (' . $itemCount . ')';
+ $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
+ $item['uri'] = $result->find('div.collection-title.C.C2 > a', 0)->href;
+
+ $item['content'] = '';
+
+ if ($result->find('img.item-img', 0)) {
+ $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
+ }
+
+ return $item;
+ }
+
+ private function processHiddenDetails($html, $detailsDivNumber, $item) {
+
+ $description = '';
+
+ if ($html->find('div.details-ia.hidden-tiles', $detailsDivNumber)) {
+ $detailsDiv = $html->find('div.details-ia.hidden-tiles', $detailsDivNumber);
+
+ if ($detailsDiv->find('div.C234', 0)->children(0)) {
+ $description = $detailsDiv->find('div.C234', 0)->children(0)->plaintext;
+
+ $detailsDiv->find('div.C234', 0)->children(0)->innertext = '';
+ }
+
+ $topics = trim($detailsDiv->find('div.C234', 0)->plaintext);
+
+ if (!empty($topics)) {
+ $topics = trim($detailsDiv->find('div.C234', 0)->plaintext);
+ $topics = trim(substr($topics, 7));
+
+ $item['categories'] = explode(',', $topics);
+ }
+
+ $item['content'] = '<p>' . $description . '</p>' . $item['content'];
+ }
+
+ return $item;
+ }
+
+ private function processPosts($html) {
+
+ $items = array();
+
+ foreach ($html->find('table.forumTable > tr') as $index => $tr) {
+ $item = array();
+
+ if ($index === 0) {
+ continue;
+ }
+
+ $item['title'] = $tr->find('td', 0)->plaintext;
+ $item['timestamp'] = strtotime($tr->find('td', 4)->children(0)->plaintext);
+ $item['uri'] = $tr->find('td', 0)->children(0)->href;
+
+ $formLink = <<<EOD
+<a href="{$tr->find('td', 2)->children(0)->href}">{$tr->find('td', 2)->children(0)->plaintext}</a>
+EOD;
+
+ $postDate = $tr->find('td', 4)->children(0)->plaintext;
+
+ $postPageHtml = getSimpleHTMLDOMCached($item['uri'], 3600)
+ or returnServerError('Could not request: ' . $item['uri']);
+
+ $postPageHtml = defaultLinkTo($postPageHtml, $this->getURI());
+
+ $post = $postPageHtml->find('div.box.well.well-sm', 0);
+
+ $parentLink = '';
+ $replyLink = <<<EOD
+<a href="{$post->find('a', 0)->href}">Reply</a>
+EOD;
+
+ if ($post->find('a', 1)->innertext = 'See parent post') {
+ $parentLink = <<<EOD
+<a href="{$post->find('a', 1)->href}">View parent post</a>
+EOD;
+ }
+
+ $post->find('h1', 0)->outertext = '';
+ $post->find('h2', 0)->outertext = '';
+
+ $item['content'] = <<<EOD
+<p>{$post->innertext}</p>{$replyLink} - {$parentLink} - Posted in {$formLink} on {$postDate}
+EOD;
+
+ $items[] = $item;
+
+ if (count($items) >= 10) {
+ break;
+ }
+ }
+ return $items;
+ }
+}
diff --git a/bridges/IvooxBridge.php b/bridges/IvooxBridge.php
new file mode 100644
index 0000000..3cdf74b
--- /dev/null
+++ b/bridges/IvooxBridge.php
@@ -0,0 +1,128 @@
+<?php
+/**
+ * IvooxRssBridge
+ * Returns the latest search result
+ * TODO: support podcast episodes list
+ */
+class IvooxBridge extends BridgeAbstract {
+ const NAME = 'Ivoox Bridge';
+ const URI = 'https://www.ivoox.com/';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the 10 newest episodes by keyword search';
+ const MAINTAINER = 'xurxof'; // based on YoutubeBridge by mitsukarenai
+ const PARAMETERS = array(
+ 'Search result' => array(
+ 's' => array(
+ 'name' => 'keyword',
+ 'exampleValue' => 'test'
+ )
+ )
+ );
+
+ private function ivBridgeAddItem(
+ $episode_link,
+ $podcast_name,
+ $episode_title,
+ $author_name,
+ $episode_description,
+ $publication_date,
+ $episode_duration) {
+ $item = array();
+ $item['title'] = htmlspecialchars_decode($podcast_name . ': ' . $episode_title);
+ $item['author'] = $author_name;
+ $item['timestamp'] = $publication_date;
+ $item['uri'] = $episode_link;
+ $item['content'] = '<a href="' . $episode_link . '">' . $podcast_name . ': ' . $episode_title
+ . '</a><br />Duration: ' . $episode_duration
+ . '<br />Description:<br />' . $episode_description;
+ $this->items[] = $item;
+ }
+
+ private function ivBridgeParseHtmlListing($html) {
+ $limit = 4;
+ $count = 0;
+
+ foreach($html->find('div.flip-container') as $flipper) {
+ $linkcount = 0;
+ if(!empty($flipper->find( 'div.modulo-type-banner' ))) {
+ // ad
+ continue;
+ }
+
+ if($count < $limit) {
+ foreach($flipper->find('div.header-modulo') as $element) {
+ foreach($element->find('a') as $link) {
+ if ($linkcount == 0) {
+ $episode_link = $link->href;
+ $episode_title = $link->title;
+ } elseif ($linkcount == 1) {
+ $author_link = $link->href;
+ $author_name = $link->title;
+ } elseif ($linkcount == 2) {
+ $podcast_link = $link->href;
+ $podcast_name = $link->title;
+ }
+
+ $linkcount++;
+ }
+ }
+
+ $episode_description = $flipper->find('button.btn-link', 0)->getAttribute('data-content');
+ $episode_duration = $flipper->find('p.time', 0)->innertext;
+ $publication_date = $flipper->find('li.date', 0)->getAttribute('title');
+
+ // alternative date_parse_from_format
+ // or DateTime::createFromFormat('G:i - d \d\e M \d\e Y', $publication);
+ // TODO: month name translations, due function doesn't support locale
+
+ $a = strptime($publication_date, '%H:%M - %d de %b. de %Y'); // obsolete function, uses c libraries
+ $publication_date = mktime(0, 0, 0, $a['tm_mon'] + 1, $a['tm_mday'], $a['tm_year'] + 1900);
+
+ $this->ivBridgeAddItem(
+ $episode_link,
+ $podcast_name,
+ $episode_title,
+ $author_name,
+ $episode_description,
+ $publication_date,
+ $episode_duration
+ );
+ $count++;
+ }
+ }
+ }
+
+ public function collectData() {
+
+ // store locale, change to spanish
+ $originalLocales = explode(';', setlocale(LC_ALL, 0));
+ setlocale(LC_ALL, 'es_ES.utf8');
+
+ $xml = '';
+ $html = '';
+ $url_feed = '';
+ if($this->getInput('s')) { /* Search modes */
+ $this->request = str_replace(' ', '-', $this->getInput('s'));
+ $url_feed = self::URI . urlencode($this->request) . '_sb_f_1.html?o=uploaddate';
+ } else {
+ returnClientError('Not valid mode at IvooxBridge');
+ }
+
+ $dom = getSimpleHTMLDOM($url_feed)
+ or returnServerError('Could not request ' . $url_feed);
+ $this->ivBridgeParseHtmlListing($dom);
+
+ // restore locale
+
+ foreach($originalLocales as $localeSetting) {
+ if(strpos($localeSetting, '=') !== false) {
+ list($category, $locale) = explode('=', $localeSetting);
+ } else {
+ $category = LC_ALL;
+ $locale = $localeSetting;
+ }
+
+ setlocale($category, $locale);
+ }
+ }
+}
diff --git a/bridges/JustETFBridge.php b/bridges/JustETFBridge.php
index 85318b8..8d5b3d5 100644
--- a/bridges/JustETFBridge.php
+++ b/bridges/JustETFBridge.php
@@ -34,7 +34,6 @@ class JustETFBridge extends BridgeAbstract {
'global' => array(
'lang' => array(
'name' => 'Language',
- 'required' => true,
'type' => 'list',
'values' => array(
'Englisch' => 'en',
diff --git a/bridges/KununuBridge.php b/bridges/KununuBridge.php
index 2f4bf0b..7cc4af6 100644
--- a/bridges/KununuBridge.php
+++ b/bridges/KununuBridge.php
@@ -11,7 +11,6 @@ class KununuBridge extends BridgeAbstract {
'site' => array(
'name' => 'Site',
'type' => 'list',
- 'required' => true,
'title' => 'Select your site',
'values' => array(
'Austria' => 'at',
@@ -23,9 +22,18 @@ class KununuBridge extends BridgeAbstract {
'full' => array(
'name' => 'Load full article',
'type' => 'checkbox',
- 'required' => false,
'exampleValue' => 'checked',
'title' => 'Activate to load full article'
+ ),
+ 'include_ratings' => array(
+ 'name' => 'Include ratings',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to include ratings in the feed'
+ ),
+ 'include_benefits' => array(
+ 'name' => 'Include benefits',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to include benefits in the feed'
)
),
array(
@@ -118,7 +126,7 @@ class KununuBridge extends BridgeAbstract {
$item = array();
$item['author'] = $this->extractArticleAuthorPosition($article);
- $item['timestamp'] = strtotime($date);
+ $item['timestamp'] = strtotime($date->content);
$item['title'] = $rating->getAttribute('aria-label')
. ' : '
. strip_tags($summary->innertext);
@@ -177,7 +185,32 @@ class KununuBridge extends BridgeAbstract {
$description = $article->find('[itemprop=reviewBody]', 0)
or returnServerError('Cannot find article description!');
- return $description->innertext;
+ $retVal = $description->innertext;
+
+ if($this->getInput('include_ratings')
+ && ($ratings = $article->find('.review-ratings .rating-group'))) {
+ $retVal .= (empty($retVal) ? '' : '<hr>') . '<table>';
+ foreach($ratings as $rating) {
+ $retVal .= <<<EOD
+<tr>
+ <td>{$rating->find('.rating-title', 0)->plaintext}
+ <td>{$rating->find('.rating-badge', 0)->plaintext}
+</tr>
+EOD;
+ }
+ $retVal .= '</table>';
+ }
+
+ if($this->getInput('include_benefits')
+ && ($benefits = $article->find('benefit'))) {
+ $retVal .= (empty($retVal) ? '' : '<hr>') . '<ul>';
+ foreach($benefits as $benefit) {
+ $retVal .= "<li>{$benefit->plaintext}</li>";
+ }
+ $retVal .= '</ul>';
+ }
+
+ return $retVal;
}
/**
diff --git a/bridges/LaCentraleBridge.php b/bridges/LaCentraleBridge.php
new file mode 100644
index 0000000..baaaa58
--- /dev/null
+++ b/bridges/LaCentraleBridge.php
@@ -0,0 +1,477 @@
+<?php
+class LaCentraleBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'jacknumber';
+ const NAME = 'La Centrale';
+ const URI = 'https://www.lacentrale.fr/';
+ const DESCRIPTION = 'Returns most recent vehicules ads from LaCentrale';
+
+ const PARAMETERS = array( array(
+ 'type' => array(
+ 'name' => 'Type de véhicule',
+ 'type' => 'list',
+ 'values' => array(
+ 'Voiture' => 'car',
+ 'Camion/Pickup' => 'truck',
+ 'Moto' => 'moto',
+ 'Scooter' => 'scooter',
+ 'Quad' => 'quad',
+ 'Caravane/Camping-car' => 'mobileHome'
+ )
+ ),
+ 'brand' => array(
+ 'name' => 'Marque',
+ 'type' => 'list',
+ 'values' => array(
+ '' => '',
+ 'ABARTH' => 'ABARTH',
+ 'AC' => 'AC',
+ 'AIXAM' => 'AIXAM',
+ 'ALFA ROMEO' => 'ALFA ROMEO',
+ 'ALKE' => 'ALKE',
+ 'ALPINA' => 'ALPINA',
+ 'ALPINE' => 'ALPINE',
+ 'AMC' => 'AMC',
+ 'ANAIG' => 'ANAIG',
+ 'APRILIA' => 'APRILIA',
+ 'ARIEL' => 'ARIEL',
+ 'ASTON MARTIN' => 'ASTON MARTIN',
+ 'AUDI' => 'AUDI',
+ 'AUSTIN HEALEY' => 'AUSTIN HEALEY',
+ 'AUSTIN' => 'AUSTIN',
+ 'AUTOBIANCHI' => 'AUTOBIANCHI',
+ 'AVINTON' => 'AVINTON',
+ 'BELLIER' => 'BELLIER',
+ 'BENELLI' => 'BENELLI',
+ 'BENTLEY' => 'BENTLEY',
+ 'BETA' => 'BETA',
+ 'BMW' => 'BMW',
+ 'BOLLORE' => 'BOLLORE',
+ 'BRIXTON' => 'BRIXTON',
+ 'BUELL' => 'BUELL',
+ 'BUGATTI' => 'BUGATTI',
+ 'BUICK' => 'BUICK',
+ 'BULLIT' => 'BULLIT',
+ 'CADILLAC' => 'CADILLAC',
+ 'CASALINI' => 'CASALINI',
+ 'CATERHAM' => 'CATERHAM',
+ 'CHATENET' => 'CHATENET',
+ 'CHEVROLET' => 'CHEVROLET',
+ 'CHRYSLER' => 'CHRYSLER',
+ 'CHUNLAN' => 'CHUNLAN',
+ 'CITROEN' => 'CITROEN',
+ 'COURB' => 'COURB',
+ 'CR&S' => 'CR&S',
+ 'CUPRA' => 'CUPRA',
+ 'CYCLONE' => 'CYCLONE',
+ 'DACIA' => 'DACIA',
+ 'DAELIM' => 'DAELIM',
+ 'DAEWOO' => 'DAEWOO',
+ 'DAF' => 'DAF',
+ 'DAIHATSU' => 'DAIHATSU',
+ 'DANGEL' => 'DANGEL',
+ 'DATSUN' => 'DATSUN',
+ 'DE SOTO' => 'DE SOTO',
+ 'DE TOMASO' => 'DE TOMASO',
+ 'DERBI' => 'DERBI',
+ 'DEVINCI' => 'DEVINCI',
+ 'DODGE' => 'DODGE',
+ 'DONKERVOORT' => 'DONKERVOORT',
+ 'DS' => 'DS',
+ 'DUCATI' => 'DUCATI',
+ 'DUCATY' => 'DUCATY',
+ 'DUE' => 'DUE',
+ 'ENFIELD' => 'ENFIELD',
+ 'EXCALIBUR' => 'EXCALIBUR',
+ 'FACEL VEGA' => 'FACEL VEGA',
+ 'FANTIC MOTOR' => 'FANTIC MOTOR',
+ 'FERRARI' => 'FERRARI',
+ 'FIAT' => 'FIAT',
+ 'FISKER' => 'FISKER',
+ 'FORD' => 'FORD',
+ 'FUSO' => 'FUSO',
+ 'GAS GAS' => 'GAS GAS',
+ 'GILERA' => 'GILERA',
+ 'GMC' => 'GMC',
+ 'GOWINN' => 'GOWINN',
+ 'GRANDIN' => 'GRANDIN',
+ 'HARLEY DAVIDSON' => 'HARLEY DAVIDSON',
+ 'HOMMELL' => 'HOMMELL',
+ 'HONDA' => 'HONDA',
+ 'HUMMER' => 'HUMMER',
+ 'HUSABERG' => 'HUSABERG',
+ 'HUSQVARNA' => 'HUSQVARNA',
+ 'HYOSUNG' => 'HYOSUNG',
+ 'HYUNDAI' => 'HYUNDAI',
+ 'INDIAN' => 'INDIAN',
+ 'INFINITI' => 'INFINITI',
+ 'INNOCENTI' => 'INNOCENTI',
+ 'ISUZU' => 'ISUZU',
+ 'IVECO' => 'IVECO',
+ 'JAGUAR' => 'JAGUAR',
+ 'JDM SIMPA' => 'JDM SIMPA',
+ 'JEEP' => 'JEEP',
+ 'JENSEN' => 'JENSEN',
+ 'JIAYUAN' => 'JIAYUAN',
+ 'KAWASAKI' => 'KAWASAKI',
+ 'KEEWAY' => 'KEEWAY',
+ 'KIA' => 'KIA',
+ 'KSR' => 'KSR',
+ 'KTM' => 'KTM',
+ 'KYMCO' => 'KYMCO',
+ 'LADA' => 'LADA',
+ 'LAMBORGHINI' => 'LAMBORGHINI',
+ 'LANCIA' => 'LANCIA',
+ 'LAND ROVER' => 'LAND ROVER',
+ 'LEXUS' => 'LEXUS',
+ 'LIGIER' => 'LIGIER',
+ 'LINCOLN' => 'LINCOLN',
+ 'LONDON TAXI COMPANY' => 'LONDON TAXI COMPANY',
+ 'LOTUS' => 'LOTUS',
+ 'MAGPOWER' => 'MAGPOWER',
+ 'MAN' => 'MAN',
+ 'MASAI' => 'MASAI',
+ 'MASERATI' => 'MASERATI',
+ 'MASH' => 'MASH',
+ 'MATRA' => 'MATRA',
+ 'MAYBACH' => 'MAYBACH',
+ 'MAZDA' => 'MAZDA',
+ 'MCLAREN' => 'MCLAREN',
+ 'MEGA' => 'MEGA',
+ 'MERCEDES' => 'MERCEDES',
+ 'MERCEDES-AMG' => 'MERCEDES-AMG',
+ 'MERCURY' => 'MERCURY',
+ 'MEYERS MANX' => 'MEYERS MANX',
+ 'MG' => 'MG',
+ 'MIA ELECTRIC' => 'MIA ELECTRIC',
+ 'MICROCAR' => 'MICROCAR',
+ 'MINAUTO' => 'MINAUTO',
+ 'MINI' => 'MINI',
+ 'MITSUBISHI' => 'MITSUBISHI',
+ 'MORGAN' => 'MORGAN',
+ 'MORRIS' => 'MORRIS',
+ 'MOTO GUZZI' => 'MOTO GUZZI',
+ 'MOTO MORINI' => 'MOTO MORINI',
+ 'MOTOBECANE' => 'MOTOBECANE',
+ 'MPM MOTORS' => 'MPM MOTORS',
+ 'MV AGUSTA' => 'MV AGUSTA',
+ 'NISSAN' => 'NISSAN',
+ 'NORTON' => 'NORTON',
+ 'NSU' => 'NSU',
+ 'OLDSMOBILE' => 'OLDSMOBILE',
+ 'OPEL' => 'OPEL',
+ 'ORCAL' => 'ORCAL',
+ 'OSSA' => 'OSSA',
+ 'PACKARD' => 'PACKARD',
+ 'PANTHER' => 'PANTHER',
+ 'PEUGEOT' => 'PEUGEOT',
+ 'PGO' => 'PGO',
+ 'PIAGGIO' => 'PIAGGIO',
+ 'PLYMOUTH' => 'PLYMOUTH',
+ 'POLARIS' => 'POLARIS',
+ 'PONTIAC' => 'PONTIAC',
+ 'PORSCHE' => 'PORSCHE',
+ 'REALM' => 'REALM',
+ 'REGAL RAPTOR' => 'REGAL RAPTOR',
+ 'RENAULT' => 'RENAULT',
+ 'RIEJU' => 'RIEJU',
+ 'ROLLS ROYCE' => 'ROLLS ROYCE',
+ 'ROVER' => 'ROVER',
+ 'ROYAL ENFIELD' => 'ROYAL ENFIELD',
+ 'SAAB' => 'SAAB',
+ 'SANTANA' => 'SANTANA',
+ 'SCANIA' => 'SCANIA',
+ 'SEAT' => 'SEAT',
+ 'SECMA' => 'SECMA',
+ 'SHELBY' => 'SHELBY',
+ 'SHERCO' => 'SHERCO',
+ 'SIMCA' => 'SIMCA',
+ 'SKODA' => 'SKODA',
+ 'SMART' => 'SMART',
+ 'SPYKER' => 'SPYKER',
+ 'SSANGYONG' => 'SSANGYONG',
+ 'STUDEBAKER' => 'STUDEBAKER',
+ 'SUBARU' => 'SUBARU',
+ 'SUNBEAM' => 'SUNBEAM',
+ 'SUZUKI' => 'SUZUKI',
+ 'SWM' => 'SWM',
+ 'SYM' => 'SYM',
+ 'TALBOT SIMCA' => 'TALBOT SIMCA',
+ 'TALBOT' => 'TALBOT',
+ 'TEILHOL' => 'TEILHOL',
+ 'TESLA' => 'TESLA',
+ 'TM' => 'TM',
+ 'TNT MOTOR' => 'TNT MOTOR',
+ 'TOYOTA' => 'TOYOTA',
+ 'TRIUMPH' => 'TRIUMPH',
+ 'TVR' => 'TVR',
+ 'VAUXHALL' => 'VAUXHALL',
+ 'VESPA' => 'VESPA',
+ 'VICTORY' => 'VICTORY',
+ 'VOLKSWAGEN' => 'VOLKSWAGEN',
+ 'VOLVO' => 'VOLVO',
+ 'VOXAN' => 'VOXAN',
+ 'WIESMANN' => 'WIESMANN',
+ 'YAMAHA' => 'YAMAHA',
+ 'YCF' => 'YCF',
+ 'ZERO' => 'ZERO',
+ 'ZONGSHEN' => 'ZONGSHEN'
+ )
+ ),
+ 'model' => array(
+ 'name' => 'Modèle',
+ 'type' => 'text',
+ 'title' => 'Get the exact name on LaCentrale'
+ ),
+ 'versions' => array(
+ 'name' => 'Version(s)',
+ 'type' => 'text',
+ 'title' => 'Get the exact name(s) on LaCentrale. Separate by comma'
+ ),
+ 'category' => array(
+ 'name' => 'Catégorie',
+ 'type' => 'list',
+ 'values' => array(
+ '' => '',
+ 'Voiture' => array(
+ '4x4, SUV & Crossover' => '47',
+ 'Citadine' => '40',
+ 'Berline' => '41_42',
+ 'Break' => '43',
+ 'Cabriolet' => '46',
+ 'Coupé' => '45',
+ 'Monospace' => '44',
+ 'Bus et minibus' => '82',
+ 'Fourgonnette' => '85',
+ 'Fourgon (< 3,5 tonnes)' => '81',
+ 'Pick-up' => '50',
+ 'Voiture société, commerciale' => '80',
+ 'Sans permis' => '48',
+ 'Camion (> 3,5 tonnes)' => '83',
+ ),
+ 'Camion/Pickup' => array(
+ 'Camion (> 3,5 tonnes)' => '83',
+ 'Fourgon (< 3,5 tonnes)' => '81',
+ 'Bus et minibus' => '82',
+ 'Fourgonnette' => '85',
+ 'Pick-up' => '50',
+ 'Voiture société, commerciale' => '80'
+ ),
+ 'Moto' => array(
+ 'Custom' => '60',
+ 'Offroad' => '61',
+ 'Roadster' => '62',
+ 'GT' => '63',
+ 'Mini moto' => '64',
+ 'Mobylette' => '65',
+ 'Supermotard' => '66',
+ 'Trail' => '67',
+ 'Side-car' => '69',
+ 'Sportive' => '68'
+ ),
+ 'Caravane/Camping-car' => array(
+ 'Caravane' => '423',
+ 'Profilé' => '506',
+ 'Fourgon aménagé' => '507',
+ 'Intégral' => '508',
+ 'Capucine' => '510'
+ )
+ )
+ ),
+ 'pricemin' => array(
+ 'name' => 'Prix min',
+ 'type' => 'number'
+ ),
+ 'pricemax' => array(
+ 'name' => 'Prix max',
+ 'type' => 'number'
+ ),
+ 'location' => array(
+ 'name' => 'CP ou département',
+ 'type' => 'number',
+ 'title' => 'Only one'
+ ),
+ 'distance' => array(
+ 'name' => 'Rayon de recherche',
+ 'type' => 'list',
+ 'values' => array(
+ '' => '',
+ '10 km' => '1',
+ '20 km' => '2',
+ '50 km' => '3',
+ '100 km' => '4',
+ '200 km' => '5'
+ )
+ ),
+ 'region' => array(
+ 'name' => 'Région',
+ 'type' => 'list',
+ 'values' => array(
+ '' => '',
+ 'Auvergne-Rhône-Alpes' => 'FR-ARA',
+ 'Bourgogne-Franche-Comté' => 'FR-BFC',
+ 'Bretagne' => 'FR-BRE',
+ 'Centre-Val de Loire' => 'FR-CVL',
+ 'Corse' => 'FR-COR',
+ 'Grand Est' => 'FR-GES',
+ 'Hauts-de-France' => 'FR-HDF',
+ 'Île-de-France' => 'FR-IDF',
+ 'Normandie' => 'FR-NOR',
+ 'Nouvelle-Aquitaine' => 'FR-PAC',
+ 'Occitanie' => 'FR-PDL',
+ 'Pays de la Loire' => 'FR-OCC',
+ 'Provence-Alpes-Côte d\'Azur' => 'FR-NAQ'
+ )
+ ),
+ 'mileagemin' => array(
+ 'name' => 'Kilométrage min',
+ 'type' => 'number'
+ ),
+ 'mileagemax' => array(
+ 'name' => 'Kilométrage max',
+ 'type' => 'number'
+ ),
+ 'yearmin' => array(
+ 'name' => 'Année min',
+ 'type' => 'number'
+ ),
+ 'yearmax' => array(
+ 'name' => 'Année max',
+ 'type' => 'number'
+ ),
+ 'cubiccapacitymin' => array(
+ 'name' => 'Cylindrée min',
+ 'type' => 'number'
+ ),
+ 'cubiccapacitymax' => array(
+ 'name' => 'Cylindrée max',
+ 'type' => 'number'
+ ),
+ 'fuel' => array(
+ 'name' => 'Énergie',
+ 'type' => 'list',
+ 'values' => array(
+ '' => '',
+ 'Diesel' => 'dies',
+ 'Essence' => 'ess',
+ 'Électrique' => 'elec',
+ 'Hybride' => 'hyb',
+ 'GPL' => 'gpl',
+ 'Bioéthanol' => 'eth',
+ 'Autre' => 'alt'
+ )
+ ),
+ 'gearbox' => array(
+ 'name' => 'Boite de vitesse',
+ 'type' => 'list',
+ 'values' => array(
+ '' => '',
+ 'Boite automatique' => 'AUTO',
+ 'Boite mécanique' => 'MANUAL'
+ )
+ ),
+ 'doors' => array(
+ 'name' => 'Nombre de portes',
+ 'type' => 'list',
+ 'values' => array(
+ '' => '',
+ '2 portes' => '2',
+ '3 portes' => '3',
+ '4 portes' => '4',
+ '5 portes' => '5',
+ '6 portes ou plus' => '6'
+ )
+ ),
+ 'firsthand' => array(
+ 'name' => 'Première main',
+ 'type' => 'checkbox'
+ ),
+ 'seller' => array(
+ 'name' => 'Vendeur',
+ 'type' => 'list',
+ 'values' => array(
+ '' => '',
+ 'Particulier' => 'PART',
+ 'Professionel' => 'PRO'
+ )
+ ),
+ 'sort' => array(
+ 'name' => 'Tri',
+ 'type' => 'list',
+ 'values' => array(
+ 'Prix (croissant)' => 'priceAsc',
+ 'Prix (décroissant)' => 'priceDesc',
+ 'Marque (croissant)' => 'makeAsc',
+ 'Marque (décroissant)' => 'makeDesc',
+ 'Kilométrage (croissant)' => 'mileageAsc',
+ 'Kilométrage (décroissant)' => 'mileageDesc',
+ 'Année (croissant)' => 'yearAsc',
+ 'Année (décroissant)' => 'yearDesc',
+ 'Département (croissant)' => 'visitPlaceAsc',
+ 'Département (décroissant)' => 'visitPlaceDesc'
+ )
+ ),
+ ));
+
+ public function collectData(){
+ // check data
+ if(!empty($this->getInput('distance'))
+ && is_null($this->getInput('location'))) {
+ returnClientError('You need a place ("CP ou département") to search arround.');
+ }
+
+ $params = array(
+ 'vertical' => $this->getInput('type'),
+ 'makesModelsCommercialNames' => $this->getInput('brand') . ':' . $this->getInput('model'),
+ 'versions' => $this->getInput('versions'),
+ 'categories' => $this->getInput('category'),
+ 'priceMin' => $this->getInput('pricemin'),
+ 'priceMax' => $this->getInput('pricemax'),
+ 'dptCp' => $this->getInput('location'),
+ 'distance' => $this->getInput('distance'),
+ 'regions' => $this->getInput('region'),
+ 'mileageMin' => $this->getInput('mileagemin'),
+ 'mileageMax' => $this->getInput('mileagemax'),
+ 'yearMin' => $this->getInput('yearmin'),
+ 'yearMax' => $this->getInput('yearmax'),
+ 'cubicMin' => $this->getInput('cubiccapacitymin'),
+ 'cubicMax' => $this->getInput('cubiccapacitymax'),
+ 'energies' => $this->getInput('fuel'),
+ 'firstHand' => $this->getInput('firsthand') ? 'true' : 'false',
+ 'gearbox' => $this->getInput('gearbox'),
+ 'doors' => $this->getInput('doors'),
+ 'sortBy' => $this->getInput('sort')
+ );
+ $url = self::URI . 'listing?' . http_build_query($params);
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Could not request LaCentrale.');
+
+ foreach($html->find('.linkAd') as $element) {
+
+ $item = array();
+ $item['uri'] = trim(self::URI, '/') . $element->href;
+ $item['title'] = $element->find('.brandModel', 0)->plaintext;
+ $item['sellerType'] = $element->find('.typeSeller', 0)->plaintext;
+ $item['author'] = $item['sellerType'];
+ $item['version'] = $element->find('.version', 0)->plaintext;
+ $item['price'] = $element->find('.fieldPrice', 0)->plaintext;
+ $item['year'] = $element->find('.fieldYear', 0)->plaintext;
+ $item['mileage'] = $element->find('.fieldMileage', 0)->plaintext;
+ $item['departement'] = str_replace(',', '', $element->find('.dptCont', 0)->plaintext);
+ $item['thumbnail'] = $element->find('.imgContent img', 0)->src;
+ $item['enclosures'] = array($item['thumbnail']);
+
+ $item['content'] = '
+ <img src="' . $item['thumbnail'] . '">
+ <br>Variation : ' . $item['version']
+ . '<br>Prix : ' . $item['price']
+ . '<br>Année : ' . $item['year']
+ . '<br>Kilométrage : ' . $item['mileage']
+ . '<br>Département : ' . $item['departement']
+ . '<br>Type de vendeur : ' . $item['sellerType'];
+
+ $this->items[] = $item;
+
+ }
+ }
+}
diff --git a/bridges/LeBonCoinBridge.php b/bridges/LeBonCoinBridge.php
index 36196cb..519fc91 100644
--- a/bridges/LeBonCoinBridge.php
+++ b/bridges/LeBonCoinBridge.php
@@ -356,6 +356,7 @@ class LeBonCoinBridge extends BridgeAbstract {
$data = $this->buildRequestJson();
$header = array(
+ 'User-Agent: LBC;Android;Null;Null;Null;Null;Null;Null;Null;Null',
'Content-Type: application/json',
'Content-Length: ' . strlen($data),
'api_key: ' . self::$LBC_API_KEY
diff --git a/bridges/LeMondeInformatiqueBridge.php b/bridges/LeMondeInformatiqueBridge.php
index 09bcf6a..45aa607 100644
--- a/bridges/LeMondeInformatiqueBridge.php
+++ b/bridges/LeMondeInformatiqueBridge.php
@@ -20,12 +20,13 @@ class LeMondeInformatiqueBridge extends FeedExpander {
str_replace(
'/grande/',
'/petite/',
- $article_html->find('.article-image', 0)->find('img', 0)->src
+ $article_html->find('.article-image > img, figure > img', 0)->src
)
);
//No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail
- $item['content'] = utf8_encode($this->cleanArticle($article_html->find('div.col-primary', 0)->innertext));
+ $content_node = $article_html->find('div.col-primary, div.col-sm-9', 0);
+ $item['content'] = utf8_encode($this->cleanArticle($content_node->innertext));
$item['author'] = utf8_encode($article_html->find('div.author-infos', 0)->find('b', 0)->plaintext);
return $item;
diff --git a/bridges/MangareaderBridge.php b/bridges/MangareaderBridge.php
index 9153706..9ecb0fe 100644
--- a/bridges/MangareaderBridge.php
+++ b/bridges/MangareaderBridge.php
@@ -13,7 +13,6 @@ class MangareaderBridge extends BridgeAbstract {
'category' => array(
'name' => 'Category',
'type' => 'list',
- 'required' => true,
'values' => array(
'All' => 'all',
'Action' => 'action',
diff --git a/bridges/MastodonBridge.php b/bridges/MastodonBridge.php
new file mode 100644
index 0000000..9e131b7
--- /dev/null
+++ b/bridges/MastodonBridge.php
@@ -0,0 +1,89 @@
+<?php
+
+class MastodonBridge extends FeedExpander {
+
+ const MAINTAINER = 'husim0';
+ const NAME = 'Mastodon Bridge';
+ const CACHE_TIMEOUT = 900; // 15mn
+ const DESCRIPTION = 'Returns toots';
+ const URI = 'https://mastodon.social';
+
+ const PARAMETERS = array(array(
+ 'canusername' => array(
+ 'name' => 'Canonical username (ex : @sebsauvage@framapiaf.org)',
+ 'required' => true,
+ ),
+ 'norep' => array(
+ 'name' => 'Without replies',
+ 'type' => 'checkbox',
+ 'title' => 'Only return initial toots'
+ ),
+ 'noboost' => array(
+ 'name' => 'Without boosts',
+ 'required' => false,
+ 'type' => 'checkbox',
+ 'title' => 'Hide boosts'
+ )
+ ));
+
+ public function getName(){
+ switch($this->queriedContext) {
+ case 'By username':
+ return $this->getInput('canusername');
+ default: return parent::getName();
+ }
+ }
+
+ protected function parseItem($newItem){
+ $item = parent::parseItem($newItem);
+
+ $content = str_get_html($item['content']);
+ $title = str_get_html($item['title']);
+
+ $item['title'] = $content->plaintext;
+
+ if(strlen($item['title']) > 75) {
+ $item['title'] = substr($item['title'], 0, strpos(wordwrap($item['title'], 75), "\n")) . '...';
+ }
+
+ if(strpos($title, 'shared a status by') !== false) {
+ if($this->getInput('noboost')) {
+ return null;
+ }
+
+ preg_match('/shared a status by (\S{0,})/', $title, $matches);
+ $item['title'] = 'Boost ' . $matches[1] . ' ' . $item['title'];
+ $item['author'] = $matches[1];
+ } else {
+ $item['author'] = $this->getInput('canusername');
+ }
+
+ // Check if it's a initial toot or a response
+ if($this->getInput('norep') && preg_match('/^@.+/', trim($content->plaintext))) {
+ return null;
+ }
+
+ return $item;
+ }
+
+ private function getInstance(){
+ preg_match('/^@[a-zA-Z0-9_]+@(.+)/', $this->getInput('canusername'), $matches);
+ return $matches[1];
+ }
+
+ private function getUsername(){
+ preg_match('/^@([a-zA-Z_0-9_]+)@.+/', $this->getInput('canusername'), $matches);
+ return $matches[1];
+ }
+
+ public function getURI(){
+ if($this->getInput('canusername'))
+ return 'https://' . $this->getInstance() . '/users/' . $this->getUsername() . '.atom';
+
+ return parent::getURI();
+ }
+
+ public function collectData(){
+ return $this->collectExpandableDatas($this->getURI());
+ }
+}
diff --git a/bridges/MediapartBridge.php b/bridges/MediapartBridge.php
new file mode 100644
index 0000000..15d1d3e
--- /dev/null
+++ b/bridges/MediapartBridge.php
@@ -0,0 +1,60 @@
+<?php
+
+class MediapartBridge extends FeedExpander {
+ const MAINTAINER = 'killruana';
+ const NAME = 'Mediapart Bridge';
+ const URI = 'https://www.mediapart.fr/';
+ const PARAMETERS = array(
+ array(
+ 'single_page_mode' => array(
+ 'name' => 'Single page article',
+ 'type' => 'checkbox',
+ 'title' => 'Display long articles on a single page',
+ 'defaultValue' => 'checked'
+ ),
+ 'mpsessid' => array(
+ 'name' => 'MPSESSID',
+ 'type' => 'text',
+ 'title' => 'Value of the session cookie MPSESSID'
+ )
+ )
+ );
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ public function collectData() {
+ $url = self::URI . 'articles/feed';
+ $this->collectExpandableDatas($url);
+ }
+
+ protected function parseItem($newsItem) {
+ $item = parent::parseItem($newsItem);
+
+ // Enable single page mode?
+ if ($this->getInput('single_page_mode') === true) {
+ $item['uri'] .= '?onglet=full';
+ }
+
+ // If a session cookie is defined, get the full article
+ $mpsessid = $this->getInput('mpsessid');
+ if (!empty($mpsessid)) {
+ // Set the session cookie
+ $opt = array();
+ $opt[CURLOPT_COOKIE] = 'MPSESSID=' . $mpsessid;
+
+ // Get the page
+ $articlePage = getSimpleHTMLDOM(
+ $newsItem->link . '?onglet=full',
+ array(),
+ $opt);
+
+ // Extract the article content
+ $content = $articlePage->find('div.content-article', 0)->innertext;
+ $content = sanitize($content);
+ $content = defaultLinkTo($content, static::URI);
+ $item['content'] .= $content;
+ }
+
+ return $item;
+ }
+}
diff --git a/bridges/MozillaBugTrackerBridge.php b/bridges/MozillaBugTrackerBridge.php
new file mode 100644
index 0000000..356bedc
--- /dev/null
+++ b/bridges/MozillaBugTrackerBridge.php
@@ -0,0 +1,153 @@
+<?php
+class MozillaBugTrackerBridge extends BridgeAbstract {
+
+ const NAME = 'Mozilla Bug Tracker';
+ const URI = 'https://bugzilla.mozilla.org';
+ const DESCRIPTION = 'Returns feeds for bug comments';
+ const MAINTAINER = 'AntoineTurmel';
+ 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 getIcon() {
+ return self::URI . '/extensions/BMO/web/images/favicon.ico';
+ }
+
+ 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('.bz_comment_table 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/MozillaSecurityBridge.php b/bridges/MozillaSecurityBridge.php
index 0b951a1..52672f5 100644
--- a/bridges/MozillaSecurityBridge.php
+++ b/bridges/MozillaSecurityBridge.php
@@ -21,7 +21,8 @@ class MozillaSecurityBridge extends BridgeAbstract {
$item['title'] = $element->innertext;
$item['timestamp'] = strtotime($element->innertext);
$item['content'] = $element->next_sibling()->innertext;
- $item['uri'] = self::URI;
+ $item['uri'] = self::URI . '?' . $item['timestamp'];
+ $item['uid'] = self::URI . '?' . $item['timestamp'];
$this->items[] = $item;
}
}
diff --git a/bridges/MydealsBridge.php b/bridges/MydealsBridge.php
index aa5bb48..603f4e0 100644
--- a/bridges/MydealsBridge.php
+++ b/bridges/MydealsBridge.php
@@ -17,13 +17,11 @@ class MydealsBridge extends PepperBridgeAbstract {
'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',
@@ -43,7 +41,6 @@ class MydealsBridge extends PepperBridgeAbstract {
'group' => array(
'name' => 'Gruppen',
'type' => 'list',
- 'required' => true,
'title' => 'Gruppe, deren Deals angezeigt werden müssen',
'values' => array(
'Elektronik' => 'elektronik',
@@ -66,7 +63,6 @@ class MydealsBridge extends PepperBridgeAbstract {
'order' => array(
'name' => 'sortieren nach',
'type' => 'list',
- 'required' => true,
'title' => 'Sortierung der deals',
'values' => array(
'Vom heißesten zum kältesten Deal' => '',
diff --git a/bridges/NYTBridge.php b/bridges/NYTBridge.php
new file mode 100644
index 0000000..687d088
--- /dev/null
+++ b/bridges/NYTBridge.php
@@ -0,0 +1,26 @@
+<?php
+class NYTBridge extends FeedExpander {
+
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'New York Times Bridge';
+ const URI = 'https://www.nytimes.com/';
+ const CACHE_TIMEOUT = 3600;
+ const DESCRIPTION = 'RSS feed for the New York Times';
+
+ public function collectData(){
+ $this->collectExpandableDatas('https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', 15);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+ // figure contain's the main article image
+ $article = $articlePage->find('figure', 0);
+ // p > css-exrw3m has the actual article
+ foreach($articlePage->find('p.css-exrw3m') as $element)
+ $article = $article . $element;
+ $item['content'] = $article;
+ return $item;
+ }
+}
diff --git a/bridges/NationalGeographicBridge.php b/bridges/NationalGeographicBridge.php
new file mode 100644
index 0000000..dfccd25
--- /dev/null
+++ b/bridges/NationalGeographicBridge.php
@@ -0,0 +1,194 @@
+<?php
+class NationalGeographicBridge extends BridgeAbstract {
+
+ const CONTEXT_BY_TOPIC = 'By Topic';
+ const PARAMETER_TOPIC = 'topic';
+ const PARAMETER_FULL_ARTICLE = 'full';
+ const TOPIC_MAGAZINE = 'Magazine';
+ const TOPIC_LATEST_STORIES = 'Latest Stories';
+
+ const NAME = 'National Geographic';
+ const URI = 'https://www.nationalgeographic.com/';
+ const DESCRIPTION = 'Fetches the latest articles from the National Geographic Magazine';
+ const MAINTAINER = 'logmanoriginal';
+ const PARAMETERS = array(
+ self::CONTEXT_BY_TOPIC => array(
+ self::PARAMETER_TOPIC => array(
+ 'name' => 'Topic',
+ 'type' => 'list',
+ 'values' => array(
+ self::TOPIC_MAGAZINE => 'magazine',
+ self::TOPIC_LATEST_STORIES => 'latest-stories'
+ ),
+ 'title' => 'Select your topic',
+ 'defaultValue' => 'Magazine'
+ )
+ ),
+ 'global' => array(
+ self::PARAMETER_FULL_ARTICLE => array(
+ 'name' => 'Full Article',
+ 'type' => 'checkbox',
+ 'title' => 'Enable to load full articles (takes longer)'
+ )
+ )
+ );
+
+ private $topicName = '';
+
+ public function getURI() {
+ switch ($this->queriedContext) {
+ case self::CONTEXT_BY_TOPIC: {
+ return self::URI . $this->getInput(self::PARAMETER_TOPIC);
+ } break;
+ default: {
+ return parent::getURI();
+ }
+ }
+ }
+
+ public function collectData() {
+ $this->topicName = $this->getTopicName($this->getInput(self::PARAMETER_TOPIC));
+
+ switch($this->topicName) {
+ case self::TOPIC_MAGAZINE: {
+ return $this->collectMagazine();
+ } break;
+ case self::TOPIC_LATEST_STORIES: {
+ return $this->collectLatestStories();
+ } break;
+ default: {
+ returnServerError('Unknown topic: "' . $this->topicName . '"');
+ }
+ }
+ }
+
+ public function getName() {
+ switch ($this->queriedContext) {
+ case self::CONTEXT_BY_TOPIC: {
+ return static::NAME . ': ' . $this->topicName;
+ } break;
+ default: {
+ return parent::getName();
+ }
+ }
+ }
+
+ private function getTopicName($topic) {
+ return array_search($topic, static::PARAMETERS[self::CONTEXT_BY_TOPIC][self::PARAMETER_TOPIC]['values']);
+ }
+
+ private function collectMagazine() {
+ $uri = $this->getURI();
+
+ $html = getSimpleHTMLDOM($uri)
+ or returnServerError('Could not request ' . $uri);
+
+ $script = $html->find('#lead-component script')[0];
+
+ $json = json_decode($script->innertext, true);
+
+ // This is probably going to break in the future, fix it then :)
+ foreach($json['body']['0']['multilayout_promo_beta']['stories'] as $story) {
+ $this->addStory($story);
+ }
+ }
+
+ private function collectLatestStories() {
+ $uri = self::URI . 'latest-stories/_jcr_content/content/hubfeed.promo-hub-feed-all-stories.json';
+
+ $json_raw = getContents($uri)
+ or returnServerError('Could not request ' . $uri);
+
+ foreach(json_decode($json_raw, true) as $story) {
+ $this->addStory($story);
+ }
+ }
+
+ private function addStory($story) {
+ $title = 'Unknown title';
+ $content = '';
+
+ foreach($story['components'] as $component) {
+ switch($component['content_type']) {
+ case 'title': {
+ $title = $component['title']['text'];
+ } break;
+ case 'dek': {
+ $content = $component['dek']['text'];
+ } break;
+ }
+ }
+
+ $item = array();
+
+ $item['uri'] = $story['uri'];
+ $item['title'] = $title;
+
+ // if full article is requested!
+ if ($this->getInput(self::PARAMETER_FULL_ARTICLE))
+ $item['content'] = $this->getFullArticle($item['uri']);
+ else
+ $item['content'] = $content;
+
+ if (isset($story['promo_image'])) {
+ switch($story['promo_image']['content_type']) {
+ case 'image': {
+ $item['enclosures'][] = $story['promo_image']['image']['uri'];
+ } break;
+ }
+ }
+
+ if (isset($story['lead_media'])) {
+ $media = $story['lead_media'];
+ switch($media['content_type']) {
+ case 'image': {
+ // Don't add if promo_image was added
+ if (empty($item['enclosures']))
+ $item['enclosures'][] = $media['image']['uri'];
+ } break;
+ case 'image_gallery': {
+ foreach($media['image_gallery']['images'] as $image) {
+ $item['enclosures'][] = $image['uri'];
+ }
+ } break;
+ }
+ }
+
+ $this->items[] = $item;
+ }
+
+ private function getFullArticle($uri) {
+ $html = getSimpleHTMLDOMCached($uri)
+ or returnServerError('Could not load ' . $uri);
+
+ $html = defaultLinkTo($html, $uri);
+
+ $content = '';
+
+ foreach($html->find('
+ .content > .smartbody.text,
+ .content > .section.image script[type="text/json"],
+ .content > .section.image span[itemprop="caption"],
+ .content > .section.inline script[type="text/json"]
+ ') as $element) {
+ if ($element->tag === 'script') {
+ $json = json_decode($element->innertext, true);
+ if (isset($json['src'])) {
+ $content .= '<img src="' . $json['src'] . '" width="100%" alt="' . $json['alt'] . '">';
+ } elseif (isset($json['galleryType']) && isset($json['endpoint'])) {
+ $doc = getContents($json['endpoint'])
+ or returnServerError('Could not load ' . $json['endpoint']);
+ $json = json_decode($doc, true);
+ foreach($json['items'] as $item) {
+ $content .= '<p>' . $item['caption'] . '</p>';
+ $content .= '<img src="' . $item['url'] . '" width="100%" alt="' . $item['caption'] . '">';
+ }
+ }
+ } else {
+ $content .= $element->outertext;
+ }
+ }
+
+ return $content;
+ }
+}
diff --git a/bridges/NineGagBridge.php b/bridges/NineGagBridge.php
index f526135..e726c73 100644
--- a/bridges/NineGagBridge.php
+++ b/bridges/NineGagBridge.php
@@ -11,7 +11,6 @@ class NineGagBridge extends BridgeAbstract {
'd' => array(
'name' => 'Section',
'type' => 'list',
- 'required' => true,
'values' => array(
'Hot' => 'hot',
'Trending' => 'trending',
@@ -28,7 +27,6 @@ class NineGagBridge extends BridgeAbstract {
'g' => array(
'name' => 'Section',
'type' => 'list',
- 'required' => true,
'values' => array(
'Animals' => 'cute',
'Anime & Manga' => 'anime-manga',
@@ -88,7 +86,6 @@ class NineGagBridge extends BridgeAbstract {
't' => array(
'name' => 'Type',
'type' => 'list',
- 'required' => true,
'values' => array(
'Hot' => 'hot',
'Fresh' => 'fresh',
diff --git a/bridges/NotAlwaysBridge.php b/bridges/NotAlwaysBridge.php
index b2f4c35..c7758c3 100644
--- a/bridges/NotAlwaysBridge.php
+++ b/bridges/NotAlwaysBridge.php
@@ -21,8 +21,7 @@ class NotAlwaysBridge extends BridgeAbstract {
'Friendly' => 'friendly',
'Hopeless' => 'hopeless',
'Unfiltered' => 'unfiltered'
- ),
- 'required' => true
+ )
)
));
diff --git a/bridges/NovelUpdatesBridge.php b/bridges/NovelUpdatesBridge.php
index 729eb48..05acd8e 100644
--- a/bridges/NovelUpdatesBridge.php
+++ b/bridges/NovelUpdatesBridge.php
@@ -3,7 +3,7 @@ class NovelUpdatesBridge extends BridgeAbstract {
const MAINTAINER = 'albirew';
const NAME = 'Novel Updates';
- const URI = 'http://www.novelupdates.com/';
+ const URI = 'https://www.novelupdates.com/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Returns releases from Novel Updates';
const PARAMETERS = array( array(
diff --git a/bridges/OnVaSortirBridge.php b/bridges/OnVaSortirBridge.php
index ee6baf1..ed1dcb6 100644
--- a/bridges/OnVaSortirBridge.php
+++ b/bridges/OnVaSortirBridge.php
@@ -9,7 +9,6 @@ class OnVaSortirBridge extends FeedExpander {
'city' => array(
'name' => 'City',
'type' => 'list',
- 'required' => true,
'values' => array(
'Agen' => 'Agen',
'Ajaccio' => 'Ajaccio',
diff --git a/bridges/OneFortuneADayBridge.php b/bridges/OneFortuneADayBridge.php
index ed0b5ec..62fe767 100644
--- a/bridges/OneFortuneADayBridge.php
+++ b/bridges/OneFortuneADayBridge.php
@@ -35,25 +35,33 @@ class OneFortuneADayBridge extends BridgeAbstract {
'23:00' => 23,
),
'defaultValue' => 5
+ ),
+ 'lucky' => array(
+ 'name' => 'Lucky number (optional)',
+ 'type' => 'text'
)
));
const LIMIT_ITEMS = 7;
const DAY_SECS = 86400;
+ public function getDescription(){
+ return self::DESCRIPTION . '<br/>Set a lucky number to get your personal quotes, like ' . mt_rand();
+ }
+
public function collectData() {
$time = gmmktime((int)$this->getInput('time'), 0, 0);
if ($time > time())
$time -= self::DAY_SECS;
for ($i = self::LIMIT_ITEMS; $i > 0; --$i) {
- $seed = date('Ymd', $time);
+ $seed = gmdate('Ymd', $time) . $this->getInput('lucky');
$quote = $this->getQuote($seed);
$item['title'] = strftime('%A, %x', $time);
$item['content'] = htmlentities($quote, ENT_QUOTES, 'UTF-8');
$item['timestamp'] = $time;
- $item['uri'] = 'urn:sha1:' . hash('sha1', $seed);
+ $item['uid'] = hash('sha1', $seed);
$this->items[] = $item;
diff --git a/bridges/OpenClassroomsBridge.php b/bridges/OpenClassroomsBridge.php
index 5f0daca..4db7bc1 100644
--- a/bridges/OpenClassroomsBridge.php
+++ b/bridges/OpenClassroomsBridge.php
@@ -11,7 +11,6 @@ class OpenClassroomsBridge extends BridgeAbstract {
'u' => array(
'name' => 'Catégorie',
'type' => 'list',
- 'required' => true,
'values' => array(
'Arts & Culture' => 'arts',
'Code' => 'code',
diff --git a/bridges/PatreonBridge.php b/bridges/PatreonBridge.php
new file mode 100644
index 0000000..57727a3
--- /dev/null
+++ b/bridges/PatreonBridge.php
@@ -0,0 +1,203 @@
+<?php
+class PatreonBridge extends BridgeAbstract {
+ const NAME = 'Patreon Bridge';
+ const URI = 'https://www.patreon.com/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns posts by creators on Patreon';
+ const MAINTAINER = 'Roliga';
+ const PARAMETERS = array( array(
+ 'creator' => array(
+ 'name' => 'Creator',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Creator name as seen in their page URL'
+ )
+ ));
+
+ public function collectData(){
+ $html = getSimpleHTMLDOMCached($this->getURI(), 86400)
+ or returnServerError('Failed to load creator page at ' . $this->getURI());
+ $regex = '#/api/campaigns/([0-9]+)#';
+ if(preg_match($regex, $html->save(), $matches) > 0) {
+ $campaign_id = $matches[1];
+ } else {
+ returnServerError('Could not find campaign ID');
+ }
+
+ $query = array(
+ 'include' => implode(',', array(
+ 'user',
+ 'attachments',
+ 'user_defined_tags',
+ //'campaign',
+ //'poll.choices',
+ //'poll.current_user_responses.user',
+ //'poll.current_user_responses.choice',
+ //'poll.current_user_responses.poll',
+ //'access_rules.tier.null',
+ //'images.null',
+ //'audio.null'
+ )),
+ 'fields' => array(
+ 'post' => implode(',', array(
+ //'change_visibility_at',
+ //'comment_count',
+ 'content',
+ //'current_user_can_delete',
+ //'current_user_can_view',
+ //'current_user_has_liked',
+ //'embed',
+ 'image',
+ //'is_paid',
+ //'like_count',
+ //'min_cents_pledged_to_view',
+ //'patreon_url',
+ //'patron_count',
+ //'pledge_url',
+ //'post_file',
+ //'post_metadata',
+ //'post_type',
+ 'published_at',
+ 'teaser_text',
+ //'thumbnail_url',
+ 'title',
+ //'upgrade_url',
+ 'url',
+ //'was_posted_by_campaign_owner'
+ )),
+ 'user' => implode(',', array(
+ //'image_url',
+ 'full_name',
+ //'url'
+ ))
+ ),
+ 'filter' => array(
+ 'contains_exclusive_posts' => true,
+ 'is_draft' => false,
+ 'campaign_id' => $campaign_id
+ ),
+ 'sort' => '-published_at'
+ );
+ $posts = $this->apiGet('posts', $query);
+
+ foreach($posts->data as $post) {
+ $item = array(
+ 'uri' => $post->attributes->url,
+ 'title' => $post->attributes->title,
+ 'timestamp' => $post->attributes->published_at,
+ 'content' => '',
+ 'uid' => 'patreon.com/' . $post->id
+ );
+
+ $user = $this->findInclude($posts,
+ 'user',
+ $post->relationships->user->data->id);
+ $item['author'] = $user->full_name;
+
+ if(isset($post->attributes->image))
+ $item['content'] .= '<p><a href="'
+ . $post->attributes->url
+ . '"><img src="'
+ . $post->attributes->image->thumb_url
+ . '" /></a></p>';
+
+ if(isset($post->attributes->content)) {
+ $item['content'] .= $post->attributes->content;
+ } elseif (isset($post->attributes->teaser_text)) {
+ $item['content'] .= '<p>'
+ . $post->attributes->teaser_text
+ . '</p>';
+ }
+
+ if(isset($post->relationships->user_defined_tags)) {
+ $item['categories'] = array();
+ foreach($post->relationships->user_defined_tags->data as $tag) {
+ $attrs = $this->findInclude($posts, 'post_tag', $tag->id);
+ $item['categories'][] = $attrs->value;
+ }
+ }
+
+ if(isset($post->relationships->attachments)) {
+ $item['enclosures'] = array();
+ foreach($post->relationships->attachments->data as $attachment) {
+ $attrs = $this->findInclude($posts, 'attachment', $attachment->id);
+ $item['enclosures'][] = $attrs->url;
+ }
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ /*
+ * Searches the "included" array in an API response and returns attributes
+ * for the first match.
+ */
+ private function findInclude($data, $type, $id) {
+ foreach($data->included as $include)
+ if($include->type === $type && $include->id === $id)
+ return $include->attributes;
+ }
+
+ private function apiGet($endpoint, $query_data = array()) {
+ $query_data['json-api-version'] = 1.0;
+ $query_data['json-api-use-default-includes'] = 0;
+
+ $url = 'https://www.patreon.com/api/'
+ . $endpoint
+ . '?'
+ . http_build_query($query_data);
+
+ /*
+ * Accept-Language header and the CURL cipher list are for bypassing the
+ * Cloudflare anti-bot protection on the Patreon API. If this ever breaks,
+ * here are some other project that also deal with this:
+ * https://github.com/mikf/gallery-dl/issues/342
+ * https://github.com/daemionfox/patreon-feed/issues/7
+ * https://www.patreondevelopers.com/t/api-returning-cloudflare-challenge/2025
+ * https://github.com/splitbrain/patreon-rss/issues/4
+ */
+ $header = array(
+ 'Accept-Language: en-US',
+ 'Content-Type: application/json'
+ );
+ $opts = array(
+ CURLOPT_SSL_CIPHER_LIST => implode(':', array(
+ 'DEFAULT',
+ '!DHE-RSA-CHACHA20-POLY1305'
+ ))
+ );
+
+ $data = json_decode(getContents($url, $header, $opts))
+ or returnServerError('API request to "' . $url . '" failed.');
+
+ return $data;
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('creator')))
+ return $this->getInput('creator') . ' posts';
+
+ return parent::getName();
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('creator')))
+ return self::URI . $this->getInput('creator');
+
+ return parent::getURI();
+ }
+
+ public function detectParameters($url){
+ $params = array();
+
+ // Matches e.g. https://www.patreon.com/SomeCreator
+ $regex = '/^(https?:\/\/)?(www\.)?patreon\.com\/([^\/&?\n]+)/';
+ if(preg_match($regex, $url, $matches) > 0) {
+ $params['creator'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ return null;
+ }
+}
diff --git a/bridges/PikabuBridge.php b/bridges/PikabuBridge.php
index af603ac..987070d 100644
--- a/bridges/PikabuBridge.php
+++ b/bridges/PikabuBridge.php
@@ -6,6 +6,16 @@ class PikabuBridge extends BridgeAbstract {
const DESCRIPTION = 'Выводит посты по тегу';
const MAINTAINER = 'em92';
+ const PARAMETERS_FILTER = array(
+ 'name' => 'Фильтр',
+ 'type' => 'list',
+ 'values' => array(
+ 'Горячее' => 'hot',
+ 'Свежее' => 'new',
+ ),
+ 'defaultValue' => 'hot'
+ );
+
const PARAMETERS = array(
'По тегу' => array(
'tag' => array(
@@ -13,21 +23,38 @@ class PikabuBridge extends BridgeAbstract {
'exampleValue' => 'it',
'required' => true
),
- 'filter' => array(
- 'name' => 'Фильтр',
- 'type' => 'list',
- 'values' => array(
- 'Горячее' => 'hot',
- 'Свежее' => 'new',
- ),
- 'defaultValue' => 'hot'
+ 'filter' => self::PARAMETERS_FILTER
+ ),
+ 'По сообществу' => array(
+ 'community' => array(
+ 'name' => 'Сообщество',
+ 'exampleValue' => 'linux',
+ 'required' => true
+ ),
+ 'filter' => self::PARAMETERS_FILTER
+ ),
+ 'По пользователю' => array(
+ 'user' => array(
+ 'name' => 'Пользователь',
+ 'exampleValue' => 'admin',
+ 'required' => true
)
)
);
+ protected $title = null;
+
public function getURI() {
if ($this->getInput('tag')) {
return self::URI . '/tag/' . rawurlencode($this->getInput('tag')) . '/' . rawurlencode($this->getInput('filter'));
+ } else if ($this->getInput('user')) {
+ return self::URI . '/@' . rawurlencode($this->getInput('user'));
+ } else if ($this->getInput('community')) {
+ $uri = self::URI . '/community/' . rawurlencode($this->getInput('community'));
+ if ($this->getInput('filter') != 'hot') {
+ $uri .= '/' . rawurlencode($this->getInput('filter'));
+ }
+ return $uri;
} else {
return parent::getURI();
}
@@ -38,10 +65,10 @@ class PikabuBridge extends BridgeAbstract {
}
public function getName() {
- if (is_string($this->getInput('tag'))) {
- return $this->getInput('tag') . ' - ' . parent::getName();
- } else {
+ if (is_null($this->title)) {
return parent::getName();
+ } else {
+ return $this->title . ' - ' . parent::getName();
}
}
@@ -52,6 +79,8 @@ class PikabuBridge extends BridgeAbstract {
$text_html = iconv('windows-1251', 'utf-8', $text_html);
$html = str_get_html($text_html);
+ $this->title = $html->find('title', 0)->innertext;
+
foreach($html->find('article.story') as $post) {
$time = $post->find('time.story__datetime', 0);
if (is_null($time)) continue;
@@ -67,6 +96,11 @@ class PikabuBridge extends BridgeAbstract {
}
}
+ foreach($post->find('[data-type=gifx]') as $el) {
+ $src = $el->getAttribute('data-source');
+ $el->outertext = '<img src="' . $src . '">';
+ }
+
foreach($post->find('img') as $img) {
$src = $img->getAttribute('src');
if (!$src) {
diff --git a/bridges/PinterestBridge.php b/bridges/PinterestBridge.php
index 2917b26..3e51863 100644
--- a/bridges/PinterestBridge.php
+++ b/bridges/PinterestBridge.php
@@ -16,12 +16,6 @@ class PinterestBridge extends FeedExpander {
'name' => 'board',
'required' => true
)
- ),
- 'From search' => array(
- 'q' => array(
- 'name' => 'Keyword',
- 'required' => true
- )
)
);
@@ -29,17 +23,9 @@ class PinterestBridge extends FeedExpander {
return 'https://s.pinimg.com/webapp/style/images/favicon-9f8f9adf.png';
}
- 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);
- }
+ public function collectData() {
+ $this->collectExpandableDatas($this->getURI() . '.rss');
+ $this->fixLowRes();
}
private function fixLowRes() {
@@ -55,71 +41,21 @@ class PinterestBridge extends FeedExpander {
}
- 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']);
+ public function getURI() {
- 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;
+ if ($this->queriedContext === 'By username and board') {
+ return self::URI . '/' . urlencode($this->getInput('u')) . '/' . urlencode($this->getInput('b'));
}
- }
- 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;
+ return parent::getURI();
}
- 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();
+ public function getName() {
+
+ if ($this->queriedContext === 'By username and board') {
+ return $this->getInput('u') . ' - ' . $this->getInput('b') . ' - ' . self::NAME;
}
- return $specific . ' - ' . self::NAME;
+
+ return parent::getName();
}
}
diff --git a/bridges/PirateCommunityBridge.php b/bridges/PirateCommunityBridge.php
new file mode 100644
index 0000000..fcf97b9
--- /dev/null
+++ b/bridges/PirateCommunityBridge.php
@@ -0,0 +1,88 @@
+<?php
+class PirateCommunityBridge extends BridgeAbstract {
+ const NAME = 'Pirate-Community Bridge';
+ const URI = 'https://raymanpc.com/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Returns replies to topics';
+ const MAINTAINER = 'Roliga';
+ const PARAMETERS = array( array(
+ 't' => array(
+ 'name' => 'Topic ID',
+ 'type' => 'number',
+ 'title' => 'Topic ID from topic URL. If the URL contains t=12 the ID is 12.',
+ 'required' => true
+ )));
+
+ private $feedName = '';
+
+ public function detectParameters($url){
+ $parsed_url = parse_url($url);
+
+ if($parsed_url['host'] !== 'raymanpc.com')
+ return null;
+
+ parse_str($parsed_url['query'], $parsed_query);
+
+ if($parsed_url['path'] === '/forum/viewtopic.php'
+ && array_key_exists('t', $parsed_query)) {
+ return array('t' => $parsed_query['t']);
+ }
+
+ return null;
+ }
+
+ public function getName() {
+ if(!empty($this->feedName))
+ return $this->feedName;
+
+ return parent::getName();
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('t'))) {
+ return self::URI
+ . 'forum/viewtopic.php?t='
+ . $this->getInput('t')
+ . '&sd=d'; // sort posts decending by ate so first page has latest posts
+ }
+
+ return parent::getURI();
+ }
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not retrieve topic page at ' . $this->getURI());
+
+ $this->feedName = $html->find('head title', 0)->plaintext;
+
+ foreach($html->find('.post') as $reply) {
+ $item = array();
+
+ $item['uri'] = $this->getURI()
+ . $reply->find('h3 a', 0)->getAttribute('href');
+
+ $item['title'] = $reply->find('h3 a', 0)->plaintext;
+
+ $author_html = $reply->find('.author', 0);
+ // author_html contains the timestamp as text directly inside it,
+ // so delete all other child elements
+ foreach($author_html->children as $child)
+ $child->outertext = '';
+ // Timestamps are always in UTC+1
+ $item['timestamp'] = trim($author_html->innertext) . ' +01:00';
+
+ $item['author'] = $reply
+ ->find('.username, .username-coloured', 0)
+ ->plaintext;
+
+ $item['content'] = defaultLinkTo($reply->find('.content', 0)->innertext,
+ $this->getURI());
+
+ $item['enclosures'] = array();
+ foreach($reply->find('.attachbox img.postimage') as $img)
+ $item['enclosures'][] = urljoin($this->getURI(), $img->src);
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/QPlayBridge.php b/bridges/QPlayBridge.php
new file mode 100644
index 0000000..f204326
--- /dev/null
+++ b/bridges/QPlayBridge.php
@@ -0,0 +1,132 @@
+<?php
+class QPlayBridge extends BridgeAbstract {
+ const NAME = 'Q Play';
+ const URI = 'https://www.qplay.pt';
+ const DESCRIPTION = 'Entretenimento e humor em Português';
+ const MAINTAINER = 'somini';
+ const PARAMETERS = array(
+ 'Program' => array(
+ 'program' => array(
+ 'name' => 'Program Name',
+ 'type' => 'text',
+ 'required' => true,
+ ),
+ ),
+ 'Catalog' => array(
+ 'all_pages' => array(
+ 'name' => 'All Pages',
+ 'type' => 'checkbox',
+ 'defaultValue' => false,
+ ),
+ ),
+ );
+
+ public function getIcon() {
+ # This should be the favicon served on `self::URI`
+ return 'https://s3.amazonaws.com/unode1/assets/4957/r3T9Lm9LTLmpAEX6FlSA_apple-touch-icon.png';
+ }
+
+ public function getURI() {
+ switch ($this->queriedContext) {
+ case 'Program':
+ return self::URI . '/programs/' . $this->getInput('program');
+ case 'Catalog':
+ return self::URI . '/catalog';
+ }
+ return parent::getURI();
+ }
+
+ public function getName() {
+ switch ($this->queriedContext) {
+ case 'Program':
+ $html = getSimpleHTMLDOMCached($this->getURI())
+ or returnServerError('Could not load content');
+
+ return $html->find('h1.program--title', 0)->innertext;
+ case 'Catalog':
+ return self::NAME . ' | Programas';
+ }
+
+ return parent::getName();
+ }
+
+ /* This uses the uscreen platform, other sites can adapt this. https://www.uscreen.tv/ */
+ public function collectData() {
+ switch ($this->queriedContext) {
+ case 'Program':
+ $program = $this->getInput('program');
+ $html = getSimpleHTMLDOMCached($this->getURI())
+ or returnServerError('Could not load content');
+
+ foreach($html->find('.cce--thumbnails-video-chapter') as $element) {
+ $cid = $element->getAttribute('data-id');
+ $item['title'] = $element->find('.cce--chapter-title', 0)->innertext;
+ $item['content'] = $element->find('.cce--thumbnails-image-block', 0)
+ . $element->find('.cce--chapter-body', 0)->innertext;
+ $item['uri'] = $this->getURI() . '?cid=' . $cid;
+
+ /* TODO: Suport login credentials? */
+ /* # Get direct video URL */
+ /* $json_source = getContents(self::URI . '/chapters/' . $cid, array('Cookie: _uscreen2_session=???;')) */
+ /* or returnServerError('Could not request chapter JSON'); */
+ /* $json = json_decode($json_source); */
+
+ /* $item['enclosures'] = [$json->fallback]; */
+
+ $this->items[] = $item;
+ }
+
+ break;
+ case 'Catalog':
+ $json_raw = getContents($this->getCatalogURI(1))
+ or returnServerError('Could not load catalog content');
+
+ $json = json_decode($json_raw);
+ $total_pages = $json->total_pages;
+
+ foreach($this->parseCatalogPage($json) as $item) {
+ $this->items[] = $item;
+ }
+
+ if ($this->getInput('all_pages') === true) {
+ foreach(range(2, $total_pages) as $page) {
+ $json_raw = getContents($this->getCatalogURI($page))
+ or returnServerError('Could not load catalog content (all pages)');
+
+ $json = json_decode($json_raw);
+
+ foreach($this->parseCatalogPage($json) as $item) {
+ $this->items[] = $item;
+ }
+ }
+ }
+
+ break;
+ }
+ }
+
+ private function getCatalogURI($page) {
+ return self::URI . '/catalog.json?page=' . $page;
+ }
+
+ private function parseCatalogPage($json) {
+ $items = array();
+
+ foreach($json->records as $record) {
+ $item = array();
+
+ $item['title'] = $record->title;
+ $item['content'] = $record->description
+ . '<div>Duration: ' . $record->duration . '</div>';
+ $item['timestamp'] = strtotime($record->release_date);
+ $item['uri'] = self::URI . $record->url;
+ $item['enclosures'] = array(
+ $record->main_poster,
+ );
+
+ $items[] = $item;
+ }
+
+ return $items;
+ }
+}
diff --git a/bridges/RadioMelodieBridge.php b/bridges/RadioMelodieBridge.php
index ca033fd..fb5aca6 100644
--- a/bridges/RadioMelodieBridge.php
+++ b/bridges/RadioMelodieBridge.php
@@ -1,34 +1,87 @@
<?php
class RadioMelodieBridge extends BridgeAbstract {
const NAME = 'Radio Melodie Actu';
- const URI = 'https://www.radiomelodie.com/';
+ const URI = 'https://www.radiomelodie.com';
const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie';
const MAINTAINER = 'sysadminstory';
public function getIcon() {
- return self::URI . 'img/favicon.png';
+ return self::URI . '/img/favicon.png';
}
public function collectData(){
- $html = getSimpleHTMLDOM(self::URI . 'actu')
+ $html = getSimpleHTMLDOM(self::URI . '/actu/')
or returnServerError('Could not request Radio Melodie.');
- $list = $html->find('div[class=actuitem]');
+ $list = $html->find('div[class=displayList]', 0)->children();
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;
+ if($element->tag == 'a') {
+ $articleURL = self::URI . $element->href;
+ $article = getSimpleHTMLDOM($articleURL);
+ $textDOM = $article->find('article', 0);
+
+ // Initialise arrays
+ $item = array();
+ $audio = array();
+ $picture = array();
+
+ // Get the Main picture URL
+ $picture[] = $this->rewriteImage($article->find('div[id=pictureTitleSupport]', 0)->find('img', 0)->src);
+ $audioHTML = $article->find('audio');
+
+ // Add the audio element to the enclosure
+ foreach($audioHTML as $audioElement) {
+ $audioURL = $audioElement->src;
+ $audio[] = $audioURL;
+ }
+
+ // Rewrite pictures URL
+ $imgs = $textDOM->find('img[src^="http://www.radiomelodie.com/image.php]');
+ foreach($imgs as $img) {
+ $img->src = $this->rewriteImage($img->src);
+ $article->save();
+ }
+
+ // Remove Google Ads
+ $ads = $article->find('div[class=adInline]');
+ foreach($ads as $ad) {
+ $ad->outertext = '';
+ $article->save();
+ }
+
+ // Remove Radio Melodie Logo
+ $logoHTML = $article->find('div[id=logoArticleRM]', 0);
+ $logoHTML->outertext = '';
+ $article->save();
+
+ $author = $article->find('p[class=AuthorName]', 0)->plaintext;
+
+ $item['enclosures'] = array_merge($picture, $audio);
+ $item['author'] = $author;
+ $item['uri'] = $articleURL;
+ $item['title'] = $article->find('meta[property=og:title]', 0)->content;
+ $date = $article->find('p[class*=date]', 0)->plaintext;
+
+ // Header Image
+ $header = '<img src="' . $picture[0] . '"/>';
+
+ // Remove the Date and Author part
+ $textDOM->find('div[class=AuthorDate]', 0)->outertext = '';
+ $article->save();
+ $text = $textDOM->innertext;
+ $item['content'] = '<h1>' . $item['title'] . '</h1>' . $date . '<br/>' . $header . $text;
+ $this->items[] = $item;
+ }
}
}
+
+ /*
+ * Function to rewrite image URL to use the real Image URL and not the resized one (which is very slow)
+ */
+ private function rewriteImage($url)
+ {
+ $parts = explode('?', $url);
+ parse_str(html_entity_decode($parts[1]), $params);
+ return self::URI . '/' . $params['image'];
+
+ }
}
diff --git a/bridges/RoadAndTrackBridge.php b/bridges/RoadAndTrackBridge.php
new file mode 100644
index 0000000..b3f0acc
--- /dev/null
+++ b/bridges/RoadAndTrackBridge.php
@@ -0,0 +1,68 @@
+<?php
+class RoadAndTrackBridge extends BridgeAbstract {
+ const MAINTAINER = 'teromene';
+ const NAME = 'Road And Track Bridge';
+ const URI = 'https://www.roadandtrack.com/';
+ const CACHE_TIMEOUT = 86400; // 24h
+ const DESCRIPTION = 'Returns the latest news from Road & Track.';
+
+ public function collectData() {
+
+ $page = getSimpleHTMLDOM(self::URI);
+
+ //Process the first element
+ $firstArticleLink = $page->find('.custom-promo-title', 0)->href;
+ $this->items[] = $this->fetchArticle($firstArticleLink);
+
+ $limit = 19;
+ foreach($page->find('.full-item-title') as $article) {
+ $this->items[] = $this->fetchArticle($article->href);
+ $limit -= 1;
+ if($limit == 0) break;
+ }
+
+ }
+
+ private function fixImages($content) {
+
+ $enclosures = [];
+ foreach($content->find('img') as $image) {
+ $image->src = explode('?', $image->getAttribute('data-src'))[0];
+ $enclosures[] = $image->src;
+ }
+
+ foreach($content->find('.embed-image-wrap, .content-lede-image-wrap') as $imgContainer) {
+ $imgContainer->style = '';
+ }
+
+ return $enclosures;
+
+ }
+
+ private function fetchArticle($articleLink) {
+
+ $articleLink = self::URI . $articleLink;
+ $article = getSimpleHTMLDOM($articleLink);
+ $item = array();
+
+ $item['title'] = $article->find('.content-hed', 0)->innertext;
+ $item['author'] = $article->find('.byline-name', 0)->innertext;
+ $item['timestamp'] = strtotime($article->find('.content-info-date', 0)->getAttribute('datetime'));
+
+ $content = $article->find('.content-container', 0);
+ if($content->find('.content-rail', 0) !== null)
+ $content->find('.content-rail', 0)->innertext = '';
+ $enclosures = $this->fixImages($content);
+
+ $item['enclosures'] = $enclosures;
+ $item['content'] = $content;
+ return $item;
+
+ }
+
+ private function getArticleContent($article) {
+
+ return getContents($article->contentUrl);
+
+ }
+}
diff --git a/bridges/Rue89Bridge.php b/bridges/Rue89Bridge.php
index 934ef99..bbb1466 100644
--- a/bridges/Rue89Bridge.php
+++ b/bridges/Rue89Bridge.php
@@ -9,7 +9,7 @@ class Rue89Bridge extends BridgeAbstract {
public function collectData() {
$jsonArticles = getContents('https://appdata.nouvelobs.com/rue89/feed.json')
- or die('Unable to query Rue89 !');
+ or returnServerError('Unable to query Rue89 !');
$articles = json_decode($jsonArticles)->items;
foreach($articles as $article) {
$this->items[] = $this->getArticle($article);
@@ -19,7 +19,8 @@ class Rue89Bridge extends BridgeAbstract {
private function getArticle($articleInfo) {
- $articleJson = getContents($articleInfo->json_url) or die('Unable to get article !');
+ $articleJson = getContents($articleInfo->json_url)
+ or returnServerError('Unable to get article !');
$article = json_decode($articleJson);
$item = array();
$item['title'] = $article->title;
diff --git a/bridges/Rule34pahealBridge.php b/bridges/Rule34pahealBridge.php
index 1a74616..d130d36 100644
--- a/bridges/Rule34pahealBridge.php
+++ b/bridges/Rule34pahealBridge.php
@@ -7,4 +7,21 @@ class Rule34pahealBridge extends Shimmie2Bridge {
const NAME = 'Rule34paheal';
const URI = 'http://rule34.paheal.net/';
const DESCRIPTION = 'Returns images from given page';
+
+ 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 = $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/SIMARBridge.php b/bridges/SIMARBridge.php
new file mode 100644
index 0000000..1e446cf
--- /dev/null
+++ b/bridges/SIMARBridge.php
@@ -0,0 +1,63 @@
+<?php
+class SIMARBridge extends BridgeAbstract {
+ const NAME = 'SIMAR';
+ const URI = 'http://www.simar-louresodivelas.pt/';
+ const DESCRIPTION = 'Verificar estado da rede SIMAR';
+ const MAINTAINER = 'somini';
+ const PARAMETERS = array(
+ 'Público' => array(
+ 'interventions' => array(
+ 'type' => 'checkbox',
+ 'name' => 'Incluir Intervenções?',
+ 'defaultValue' => 'checked',
+ )
+ )
+ );
+
+ public function collectData() {
+ $html = getSimpleHTMLDOM(self::getURI())
+ or returnServerError('Could not load content');
+ $e_home = $html->find('#home', 0)
+ or returnServerError('Invalid site structure');
+
+ foreach($e_home->find('span') as $element) {
+ $item = array();
+
+ $item['title'] = 'Rotura: ' . $element->plaintext;
+ $item['content'] = $element->innertext;
+ $item['uid'] = 'urn:sha1:' . hash('sha1', $item['content']);
+
+ $this->items[] = $item;
+ }
+
+ if ($this->getInput('interventions')) {
+ $e_main1 = $html->find('#menu1', 0)
+ or returnServerError('Invalid site structure');
+
+ foreach ($e_main1->find('a') as $element) {
+ $item = array();
+
+ $item['title'] = 'Intervenção: ' . $element->plaintext;
+ $item['uri'] = self::getURI() . $element->href;
+ $item['content'] = $element->innertext;
+
+ /* Try to get the actual contents for this kind of item */
+ $item_html = getSimpleHTMLDOMCached($item['uri']);
+ if ($item_html) {
+ $e_item = $item_html->find('.auto-style59', 0);
+ foreach($e_item->find('p') as $paragraph) {
+ /* Remove empty paragraphs */
+ if (preg_match('/^(\W|&nbsp;)+$/', $paragraph->innertext) == 1) {
+ $paragraph->outertext = '';
+ }
+ }
+ if ($e_item) {
+ $item['content'] = $e_item->innertext;
+ }
+ }
+
+ $this->items[] = $item;
+ }
+ }
+ }
+}
diff --git a/bridges/SakugabooruBridge.php b/bridges/SakugabooruBridge.php
deleted file mode 100644
index 1d6cee0..0000000
--- a/bridges/SakugabooruBridge.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?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/ShanaprojectBridge.php b/bridges/ShanaprojectBridge.php
index 6eadcb1..ca6980c 100644
--- a/bridges/ShanaprojectBridge.php
+++ b/bridges/ShanaprojectBridge.php
@@ -2,70 +2,152 @@
class ShanaprojectBridge extends BridgeAbstract {
const MAINTAINER = 'logmanoriginal';
const NAME = 'Shanaproject Bridge';
- const URI = 'http://www.shanaproject.com';
+ const URI = 'https://www.shanaproject.com';
const DESCRIPTION = 'Returns a list of anime from the current Season Anime List';
+ const PARAMETERS = array(
+ array(
+ 'min_episodes' => array(
+ 'name' => 'Minimum Episodes',
+ 'type' => 'number',
+ 'title' => 'Minimum number of episodes before including in feed',
+ 'defaultValue' => 0,
+ ),
+ 'min_total_episodes' => array(
+ 'name' => 'Minimum Total Episodes',
+ 'type' => 'number',
+ 'title' => 'Minimum total number of episodes before including in feed',
+ 'defaultValue' => 0,
+ ),
+ 'require_banner' => array(
+ 'name' => 'Require Banner',
+ 'type' => 'checkbox',
+ 'title' => 'Only include anime with custom banner image',
+ 'defaultValue' => false,
+ ),
+ ),
+ );
+
+ private $uri;
+
+ public function getURI() {
+ return isset($this->uri) ? $this->uri : parent::getURI();
+ }
+
+ public function collectData(){
+ $html = $this->loadSeasonAnimeList();
+
+ $animes = $html->find('div.header_display_box_info')
+ or returnServerError('Could not find anime headers!');
+
+ $min_episodes = $this->getInput('min_episodes') ?: 0;
+ $min_total_episodes = $this->getInput('min_total_episodes') ?: 0;
+
+ foreach($animes as $anime) {
+
+ list(
+ $episodes_released,
+ /* of */,
+ $episodes_total
+ ) = explode(' ', $this->extractAnimeEpisodeInformation($anime));
+
+ // Skip if not enough episodes yet
+ if ($episodes_released < $min_episodes) {
+ continue;
+ }
+
+ // Skip if too many episodes in total
+ if ($episodes_total !== '?' && $episodes_total < $min_total_episodes) {
+ continue;
+ }
+
+ // Skip if https://static.shanaproject.com/no-art.jpg
+ if ($this->getInput('require_banner')
+ && strpos($this->extractAnimeBackgroundImage($anime), 'no-art') !== false) {
+ continue;
+ }
+
+ $this->items[] = array(
+ 'title' => $this->extractAnimeTitle($anime),
+ 'author' => $this->extractAnimeAuthor($anime),
+ 'uri' => $this->extractAnimeUri($anime),
+ 'timestamp' => $this->extractAnimeTimestamp($anime),
+ 'content' => $this->buildAnimeContent($anime),
+ );
+
+ }
+ }
// 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(
+
+ $html = getSimpleHTMLDOM(self::URI . '/seasons')
+ or returnServerError('Could not load \'seasons\' page!');
+
+ $html = defaultLinkTo($html, self::URI . '/seasons');
+
+ $season = $html->find('div.follows_menu > a', 1)
+ or returnServerError('Could not find \'Season Anime List\'!');
+
+ $html = getSimpleHTMLDOM($season->href)
+ or returnServerError(
'Could not load \'Season Anime List\' from \''
. $season->innertext
. '\'!'
);
+ $this->uri = $season->href;
+
+ $html = defaultLinkTo($html, $season->href);
+
return $html;
+
}
// Extracts the anime title
private function extractAnimeTitle($anime){
- $title = $anime->find('a', 0);
- if(!$title)
- returnServerError('Could not find anime title!');
+ $title = $anime->find('a', 0)
+ or 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;
+ $uri = $anime->find('a', 0)
+ or returnServerError('Could not find anime URI!');
+ return $uri->href;
}
// Extracts the anime release date (timestamp)
private function extractAnimeTimestamp($anime){
$timestamp = $anime->find('span.header_info_block', 1);
- if(!$timestamp)
+
+ 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
+
+ if(!$author) {
+ return null; // 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);
+ $episode = $anime->find('div.header_info_episode', 0)
+ or returnServerError('Could not find anime episode information!');
+
+ $retVal = preg_replace('/\r|\n/', ' ', $episode->plaintext);
+ $retVal = preg_replace('/\s+/', ' ', $retVal);
+
+ return $retVal;
}
// Extracts the background image
@@ -73,15 +155,16 @@ class ShanaprojectBridge extends BridgeAbstract {
// 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))
+ 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()
+ return self::URI
. '/search/?title='
. urlencode($this->extractAnimeTitle($anime))
. '&subber=';
@@ -102,22 +185,4 @@ class ShanaprojectBridge extends BridgeAbstract {
. $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/SkimfeedBridge.php b/bridges/SkimfeedBridge.php
index 9fdd454..1b78baf 100644
--- a/bridges/SkimfeedBridge.php
+++ b/bridges/SkimfeedBridge.php
@@ -18,7 +18,6 @@ class SkimfeedBridge extends BridgeAbstract {
'box_channel' => array(
'name' => 'Channel',
'type' => 'list',
- 'required' => true,
'title' => 'Select your channel',
'values' => array(
'Hacker News' => '/news/hacker-news.html',
@@ -68,7 +67,6 @@ class SkimfeedBridge extends BridgeAbstract {
'tech_channel' => array(
'name' => 'Tech channel',
'type' => 'list',
- 'required' => true,
'title' => 'Select your tech channel',
'values' => array(
'Agg' => array(
diff --git a/bridges/SoundcloudBridge.php b/bridges/SoundcloudBridge.php
index 91ac2b5..8938ff9 100644
--- a/bridges/SoundcloudBridge.php
+++ b/bridges/SoundcloudBridge.php
@@ -14,7 +14,9 @@ class SoundCloudBridge extends BridgeAbstract {
)
));
- const CLIENT_ID = '4jkoEFmZEDaqjwJ9Eih4ATNhcH3vMVfp';
+ const CLIENT_ID = 'W0KEWWILAjDiRH89X0jpwzuq6rbSK08R';
+
+ private $feedIcon = null;
public function collectData(){
@@ -25,6 +27,8 @@ class SoundCloudBridge extends BridgeAbstract {
. self::CLIENT_ID
)) or returnServerError('No results for this query');
+ $this->feedIcon = $res->avatar_url;
+
$tracks = json_decode(getContents(
'https://api.soundcloud.com/users/'
. urlencode($res->id)
@@ -56,6 +60,14 @@ class SoundCloudBridge extends BridgeAbstract {
}
+ public function getIcon(){
+ if ($this->feedIcon) {
+ return $this->feedIcon;
+ }
+
+ return parent::getIcon();
+ }
+
public function getName(){
if(!is_null($this->getInput('u'))) {
return self::NAME . ' - ' . $this->getInput('u');
diff --git a/bridges/SplCenterBridge.php b/bridges/SplCenterBridge.php
new file mode 100644
index 0000000..7a69090
--- /dev/null
+++ b/bridges/SplCenterBridge.php
@@ -0,0 +1,64 @@
+<?php
+class SplCenterBridge extends FeedExpander {
+
+ const NAME = 'Southern Poverty Law Center Bridge';
+ const URI = 'https://www.splcenter.org';
+ const DESCRIPTION = 'Returns the newest posts from the Southern Poverty Law Center';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = array(array(
+ 'content' => array(
+ 'name' => 'Content',
+ 'type' => 'list',
+ 'values' => array(
+ 'News' => 'news',
+ 'Hatewatch' => 'hatewatch',
+ ),
+ 'defaultValue' => 'news',
+ )
+ )
+ );
+
+ const CACHE_TIMEOUT = 3600; // 1 hour
+
+ protected function parseItem($item) {
+ $item = parent::parseItem($item);
+
+ $articleHtml = getSimpleHTMLDOMCached($item['uri'])
+ or returnServerError('Could not request: ' . $item['uri']);
+
+ foreach ($articleHtml->find('.file') as $index => $media) {
+ $articleHtml->find('div.file', $index)->outertext = '<em>' . $media->outertext . '</em>';
+ }
+
+ $item['content'] = $articleHtml->find('div#group-content-container', 0)->innertext;
+ $item['enclosures'][] = $articleHtml->find('meta[name="twitter:image"]', 0)->content;
+
+ return $item;
+ }
+
+ public function collectData() {
+ $this->collectExpandableDatas($this->getURI() . '/rss.xml');
+ }
+
+ public function getURI() {
+
+ if (!is_null($this->getInput('content'))) {
+ return self::URI . '/' . $this->getInput('content');
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName() {
+
+ if (!is_null($this->getInput('content'))) {
+ $parameters = $this->getParameters();
+
+ $contentValues = array_flip($parameters[0]['content']['values']);
+
+ return $contentValues[$this->getInput('content')] . ' - Southern Poverty Law Center';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/SteamBridge.php b/bridges/SteamBridge.php
index 8ff456d..d0acd6d 100644
--- a/bridges/SteamBridge.php
+++ b/bridges/SteamBridge.php
@@ -8,44 +8,12 @@ class SteamBridge extends BridgeAbstract {
const MAINTAINER = 'jacknumber';
const PARAMETERS = array(
'Wishlist' => array(
- 'username' => array(
- 'name' => 'Username',
+ 'userid' => array(
+ 'name' => 'Steamid64 (find it on steamid.io)',
+ 'title' => 'User ID (17 digits). Find your user ID with steamid.io or steamidfinder.com',
'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',
- ),
+ 'exampleValue' => '76561198821231205',
+ 'pattern' => '[0-9]{17}',
),
'only_discount' => array(
'name' => 'Only discount',
@@ -56,27 +24,15 @@ class SteamBridge extends BridgeAbstract {
public function collectData(){
- $username = $this->getInput('username');
- $params = array(
- 'cc' => $this->getInput('currency')
- );
-
- $url = self::URI . 'wishlist/id/' . $username . '?' . http_build_query($params);
+ $userid = $this->getInput('userid');
- $targetVariable = 'g_rgAppInfo';
+ $sourceUrl = self::URI . 'wishlist/profiles/' . $userid . '/wishlistdata?p=0';
$sort = array();
- $html = '';
- $html = getSimpleHTMLDOM($url)
- or returnServerError("Could not request Steam Wishlist. Tried:\n - $url");
+ $json = getContents($sourceUrl)
+ or returnServerError('Could not get content from wishlistdata (' . $sourceUrl . ')');
- $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.");
- }
+ $appsData = json_decode($json);
foreach($appsData as $id => $element) {
@@ -87,6 +43,8 @@ class SteamBridge extends BridgeAbstract {
if($element->subs) {
$appIsBuyable = 1;
+ $priceBlock = str_get_html($element->subs[0]->discount_block);
+ $appPrice = str_replace('--', '00', $priceBlock->find('.discount_final_price', 0)->plaintext);
if($element->subs[0]->discount_pct) {
@@ -94,8 +52,6 @@ class SteamBridge extends BridgeAbstract {
$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 {
@@ -103,7 +59,6 @@ class SteamBridge extends BridgeAbstract {
continue;
}
- $appPrice = $element->subs[0]->price / 100;
}
} else {
@@ -117,11 +72,14 @@ class SteamBridge extends BridgeAbstract {
}
}
+ $coverUrl = str_replace('_292x136', '', strtok($element->capsule, '?'));
+ $picturesPath = pathinfo($coverUrl)['dirname'] . '/';
+
$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['cover'] = $coverUrl;
$item['timestamp'] = $element->added;
$item['isBuyable'] = $appIsBuyable;
$item['hasDiscount'] = $appHasDiscount;
@@ -129,22 +87,29 @@ class SteamBridge extends BridgeAbstract {
$item['priority'] = $element->priority;
if($appIsBuyable) {
+
$item['price'] = floatval(str_replace(',', '.', $appPrice));
+ $item['content'] = $appPrice;
+
+ }
+
+ if($appIsFree) {
+ $item['content'] = 'Free';
}
if($appHasDiscount) {
$item['discount']['value'] = $appDiscountValue;
- $item['discount']['oldPrice'] = floatval(str_replace(',', '.', $appOldPrice));
- $item['discount']['newPrice'] = floatval(str_replace(',', '.', $appNewPrice));
+ $item['discount']['oldPrice'] = $appOldPrice;
+ $item['content'] = '<s>' . $appOldPrice . '</s> <b>' . $appPrice . '</b> (' . $appDiscountValue . ')';
}
$item['enclosures'] = array();
- $item['enclosures'][] = str_replace('_292x136', '', $element->capsule);
+ $item['enclosures'][] = $coverUrl;
- foreach($element->screenshots as $screenshot) {
- $item['enclosures'][] = substr($element->capsule, 0, -31) . $screenshot;
+ foreach($element->screenshots as $screenshotFileName) {
+ $item['enclosures'][] = $picturesPath . $screenshotFileName;
}
$sort[$id] = $element->priority;
diff --git a/bridges/SteamCommunityBridge.php b/bridges/SteamCommunityBridge.php
new file mode 100644
index 0000000..9919a4b
--- /dev/null
+++ b/bridges/SteamCommunityBridge.php
@@ -0,0 +1,191 @@
+<?php
+class SteamCommunityBridge extends BridgeAbstract {
+ const NAME = 'Steam Community';
+ const URI = 'https://www.steamcommunity.com';
+ const DESCRIPTION = 'Get the latest community updates for a game on Steam.';
+ const MAINTAINER = 'thefranke';
+ const CACHE_TIMEOUT = 3600; // 1h
+
+ const PARAMETERS = array(
+ array(
+ 'i' => array(
+ 'name' => 'App ID',
+ 'required' => true
+ ),
+ 'category' => array(
+ 'name' => 'category',
+ 'type' => 'list',
+ 'exampleValue' => 'Artwork',
+ 'title' => 'Select a category',
+ 'values' => array(
+ 'Artwork' => 'images',
+ 'Screenshots' => 'screenshots',
+ 'Videos' => 'videos',
+ 'Workshop' => 'workshop'
+ )
+ )
+ )
+ );
+
+ public function getIcon() {
+ return self::URI . '/favicon.ico';
+ }
+
+ protected function getMainPage() {
+ $category = $this->getInput('category');
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not fetch Steam data.');
+
+ return $html;
+ }
+
+ public function getName() {
+ $category = $this->getInput('category');
+
+ if (is_null('i') || is_null($category)) {
+ return self::NAME;
+ }
+
+ $html = $this->getMainPage();
+
+ $titleItem = $html->find('div.apphub_AppName', 0);
+
+ if (!$titleItem)
+ return self::NAME;
+
+ return $titleItem->innertext . ' (' . ucwords($category) . ')';
+ }
+
+ public function getURI() {
+ if ($this->getInput('category') === 'workshop')
+ return self::URI . '/workshop/browse/?appid='
+ . $this->getInput('i') . '&browsesort=mostrecent';
+
+ return self::URI . '/app/'
+ . $this->getInput('i') . '/'
+ . $this->getInput('category')
+ . '/?p=1&browsefilter=mostrecent';
+ }
+
+ private function collectMedia() {
+ $category = $this->getInput('category');
+ $html = $this->getMainPage();
+ $cards = $html->find('div.apphub_Card');
+
+ foreach($cards as $card) {
+ $uri = $card->getAttribute('data-modal-content-url');
+
+ $htmlCard = getSimpleHTMLDOMCached($uri);
+
+ $author = $card->find('div.apphub_CardContentAuthorName', 0)->innertext;
+ $author = strip_tags($author);
+
+ $title = $author . '\'s screenshot';
+
+ if ($category != 'screenshots')
+ $title = $htmlCard->find('div.workshopItemTitle', 0)->innertext;
+
+ $date = $htmlCard->find('div.detailsStatRight', 0)->innertext;
+
+ // create item
+ $item = array();
+ $item['title'] = $title;
+ $item['uri'] = $uri;
+ $item['timestamp'] = strtotime($date);
+ $item['author'] = $author;
+ $item['categories'] = $category;
+
+ $media = $htmlCard->getElementById('ActualMedia');
+ $mediaURI = $media->getAttribute('src');
+ $downloadURI = $mediaURI;
+
+ if ($category == 'videos') {
+ preg_match('/.*\/embed\/(.*)\?/', $mediaURI, $result);
+ $youtubeID = $result[1];
+ $mediaURI = 'https://img.youtube.com/vi/' . $youtubeID . '/hqdefault.jpg';
+ $downloadURI = 'https://www.youtube.com/watch?v=' . $youtubeID;
+ }
+
+ $desc = '';
+
+ if ($category == 'screenshots') {
+ $descItem = $htmlCard->find('div.screenshotDescription', 0);
+ if ($descItem)
+ $desc = $descItem->innertext;
+ }
+
+ if ($category == 'images') {
+ $descItem = $htmlCard->find('div.nonScreenshotDescription', 0);
+ if ($descItem)
+ $desc = $descItem->innertext;
+ $downloadURI = $htmlCard->find('a.downloadImage', 0)->href;
+ }
+
+ $item['content'] = '<p><a href="' . $downloadURI . '"><img src="' . $mediaURI . '"/></a></p>';
+ $item['content'] .= '<p>' . $desc . '</p>';
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10)
+ break;
+ }
+ }
+
+ private function collectWorkshop() {
+ $category = $this->getInput('category');
+ $html = $this->getMainPage();
+ $workShopItems = $html->find('div.workshopItem');
+
+ foreach($workShopItems as $workShopItem) {
+ $author = $workShopItem->find('div.workshopItemAuthorName', 0)->find('a', 0);
+ $author = $author->innertext;
+
+ $fileRating = $workShopItem->find('img.fileRating', 0);
+
+ $uri = $workShopItem->find('a.ugc', 0)->getAttribute('href');
+
+ $htmlItem = getSimpleHTMLDOMCached($uri);
+
+ $title = $htmlItem->find('div.workshopItemTitle', 0)->innertext;
+ $date = $htmlItem->find('div.detailsStatRight', 0)->innertext;
+ $description = $htmlItem->find('div.workshopItemDescription', 0)->innertext;
+
+ $previewImage = $htmlItem->find('#previewImage', 0);
+
+ $htmlTags = $htmlItem->find('div.workshopTags');
+
+ $tags = '';
+
+ foreach($htmlTags as $htmlTag) {
+ if ($tags !== '')
+ $tags .= ',';
+
+ $tags .= $htmlTag->find('a', 0)->innertext;
+ }
+
+ // create item
+ $item = array();
+ $item['title'] = $title;
+ $item['uri'] = $uri;
+ $item['timestamp'] = strtotime($date);
+ $item['author'] = $author;
+ $item['categories'] = $category;
+
+ $item['content'] = '<p><a href="' . $uri . '">'
+ . $previewImage . '</a></p><p>' . $fileRating
+ . '</p><p>' . $description . '</p>';
+
+ $this->items[] = $item;
+
+ if (count($this->items) >= 10)
+ break;
+ }
+ }
+
+ public function collectData() {
+ if ($this->getInput('category') === 'workshop')
+ $this->collectWorkshop();
+ else
+ $this->collectMedia();
+ }
+}
diff --git a/bridges/StockFilingsBridge.php b/bridges/StockFilingsBridge.php
new file mode 100644
index 0000000..f774244
--- /dev/null
+++ b/bridges/StockFilingsBridge.php
@@ -0,0 +1,80 @@
+<?php
+
+class StockFilingsBridge extends FeedExpander {
+ const MAINTAINER = 'captn3m0';
+ const NAME = 'SEC Stock filings';
+ const URI = 'https://www.sec.gov/edgar/searchedgar/companysearch.html';
+ const CACHE_TIMEOUT = 3600; // 1h
+ const DESCRIPTION = 'Tracks SEC Filings for a single company';
+ const SEARCH_URL = 'https://www.sec.gov/cgi-bin/browse-edgar?owner=exclude&action=getcompany&CIK=';
+ const WEBSITE_ROOT = 'https://www.sec.gov';
+
+ const PARAMETERS = array(
+ array(
+ 'ticker' => array(
+ 'name' => 'cik',
+ 'required' => true,
+ 'exampleValue' => 'AMD',
+ // https://stackoverflow.com/a/12827734
+ 'pattern' => '[A-Za-z0-9]+',
+ ),
+ ));
+
+ public function getIcon() {
+ return 'https://www.sec.gov/favicon.ico';
+ }
+
+ /**
+ * Generates search URL
+ */
+ private function getSearchUrl() {
+ return self::SEARCH_URL . $this->getInput('ticker');
+ }
+
+ /**
+ * Returns the Company Name
+ */
+ private function getRssFeed($html) {
+ $links = $html->find('#contentDiv a');
+
+ foreach ($links as $link) {
+ $href = $link->href;
+
+ if (substr($href, 0, 4) !== 'http') {
+ $href = self::WEBSITE_ROOT . $href;
+ }
+ parse_str(html_entity_decode(parse_url($href, PHP_URL_QUERY)), $query);
+
+ if (isset($query['output']) and ($query['output'] == 'atom')) {
+ return $href;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return \simple_html_dom object
+ * for the entire html of the product page
+ */
+ private function getHtml() {
+ $uri = $this->getSearchUrl();
+
+ return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request SEC.');
+ }
+
+ /**
+ * Scrape the SEC Stock Filings RSS Feed URL
+ * and redirect there
+ */
+ public function collectData() {
+ $html = $this->getHtml();
+ $rssFeedUrl = $this->getRssFeed($html);
+
+ if ($rssFeedUrl) {
+ parent::collectExpandableDatas($rssFeedUrl);
+ } else {
+ returnClientError('Could not find RSS Feed URL. Are you sure you used a valid CIK?');
+ }
+ }
+}
diff --git a/bridges/StoriesIGBridge.php b/bridges/StoriesIGBridge.php
new file mode 100644
index 0000000..ddf9846
--- /dev/null
+++ b/bridges/StoriesIGBridge.php
@@ -0,0 +1,47 @@
+<?php
+class StoriesIGBridge extends BridgeAbstract {
+
+ const NAME = 'Instagram Stories';
+ const URI = 'https://storiesig.com';
+ const DESCRIPTION = 'Display Instagram Stories';
+ const MAINTAINER = 'antoineturmel';
+ const PARAMETERS = array(
+ array(
+ 'username' => array(
+ 'name' => 'Instagram username',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Insert the username here'
+ ),
+ )
+ );
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Failed to receive ' . $this->getURI());
+
+ $results = $html->find('article');
+
+ foreach($results as $result) {
+
+ $item = array();
+
+ $item['title'] = $this->getInput('username') . ' story';
+ $item['uri'] = $result->find('div.download', 0)->find('a', 0)->href;
+ $item['author'] = $this->getInput('username');
+ $item['uid'] = $result->find('time', 0)->datetime;
+
+ $item['content'] = $result;
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getURI(){
+ $uri = self::URI . '/stories/';
+ $uri .= urlencode($this->getInput('username'));
+ return $uri;
+
+ return parent::getURI();
+ }
+}
diff --git a/bridges/TelegramBridge.php b/bridges/TelegramBridge.php
new file mode 100644
index 0000000..3afc283
--- /dev/null
+++ b/bridges/TelegramBridge.php
@@ -0,0 +1,301 @@
+<?php
+class TelegramBridge extends BridgeAbstract {
+ const NAME = 'Telegram Bridge';
+ const URI = 'https://t.me';
+ const DESCRIPTION = 'Returns newest posts from a public Telegram channel';
+ const MAINTAINER = 'VerifiedJoseph';
+ const PARAMETERS = array(array(
+ 'username' => array(
+ 'name' => 'Username',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => '@telegram',
+ )
+ )
+ );
+
+ const CACHE_TIMEOUT = 900; // 15 mins
+
+ private $feedName = '';
+ private $enclosures = array();
+ private $itemTitle = '';
+
+ private $backgroundImageRegex = "/background-image:url\('(.*)'\)/";
+
+ public function collectData() {
+
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request: ' . $this->getURI());
+
+ $channelTitle = htmlspecialchars_decode(
+ $html->find('div.tgme_channel_info_header_title span', 0)->plaintext,
+ ENT_QUOTES
+ );
+ $this->feedName = $channelTitle . ' (@' . $this->processUsername() . ')';
+
+ foreach($html->find('div.tgme_widget_message_wrap.js-widget_message_wrap') as $index => $messageDiv) {
+ $this->itemTitle = '';
+ $this->enclosures = array();
+ $item = array();
+
+ $item['uri'] = $this->processUri($messageDiv);
+ $item['content'] = html_entity_decode($this->processContent($messageDiv), ENT_QUOTES);
+ $item['title'] = html_entity_decode($this->itemTitle, ENT_QUOTES);
+ $item['timestamp'] = $this->processDate($messageDiv);
+ $item['enclosures'] = $this->enclosures;
+ $author = trim($messageDiv->find('a.tgme_widget_message_owner_name', 0)->plaintext);
+ $item['author'] = html_entity_decode($author, ENT_QUOTES);
+
+ $this->items[] = $item;
+ }
+ $this->items = array_reverse($this->items);
+ }
+
+ public function getURI() {
+
+ if (!is_null($this->getInput('username'))) {
+ return self::URI . '/s/' . $this->processUsername();
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName() {
+
+ if (!empty($this->feedName)) {
+ return $this->feedName . ' - Telegram';
+ }
+
+ return parent::getName();
+ }
+
+ private function processUsername() {
+
+ if (substr($this->getInput('username'), 0, 1) === '@') {
+ return substr($this->getInput('username'), 1);
+ }
+
+ return $this->getInput('username');
+ }
+
+ private function processUri($messageDiv) {
+ return $messageDiv->find('a.tgme_widget_message_date', 0)->href;
+ }
+
+ private function processContent($messageDiv) {
+ $message = '';
+
+ if ($messageDiv->find('div.tgme_widget_message_forwarded_from', 0)) {
+ $message = $messageDiv->find('div.tgme_widget_message_forwarded_from', 0)->innertext . '<br><br>';
+ }
+
+ if ($messageDiv->find('a.tgme_widget_message_reply', 0)) {
+ $message = $this->processReply($messageDiv);
+ }
+
+ if ($messageDiv->find('div.tgme_widget_message_sticker_wrap', 0)) {
+ $message .= $this->processSticker($messageDiv);
+ }
+
+ if ($messageDiv->find('div.tgme_widget_message_poll', 0)) {
+ $message .= $this->processPoll($messageDiv);
+ }
+
+ if ($messageDiv->find('video', 0)) {
+ $message .= $this->processVideo($messageDiv);
+ }
+
+ if ($messageDiv->find('a.tgme_widget_message_photo_wrap', 0)) {
+ $message .= $this->processPhoto($messageDiv);
+ }
+
+ if ($messageDiv->find('a.not_supported', 0)) {
+ $message .= $this->processNotSupported($messageDiv);
+ }
+
+ if ($messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)) {
+ $message .= $messageDiv->find('div.tgme_widget_message_text.js-message_text', 0);
+
+ $this->itemTitle = $this->ellipsisTitle(
+ $messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)->plaintext
+ );
+ }
+
+ if ($messageDiv->find('a.tgme_widget_message_link_preview', 0)) {
+ $message .= $this->processLinkPreview($messageDiv);
+ }
+
+ return $message;
+ }
+
+ private function processReply($messageDiv) {
+
+ $reply = $messageDiv->find('a.tgme_widget_message_reply', 0);
+
+ return <<<EOD
+<blockquote>{$reply->find('span.tgme_widget_message_author_name', 0)->plaintext}<br>
+{$reply->find('div.tgme_widget_message_text', 0)->innertext}
+<a href="{$reply->href}">{$reply->href}</a></blockquote><hr>
+EOD;
+ }
+
+ private function processSticker($messageDiv) {
+
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = '@' . $this->processUsername() . ' posted a sticker';
+ }
+
+ $stickerDiv = $messageDiv->find('div.tgme_widget_message_sticker_wrap', 0);
+
+ preg_match($this->backgroundImageRegex, $stickerDiv->find('i', 0)->style, $sticker);
+
+ $this->enclosures[] = $sticker[1];
+
+ return <<<EOD
+<a href="{$stickerDiv->children(0)->herf}"><img src="{$sticker[1]}"></a>
+EOD;
+ }
+
+ private function processPoll($messageDiv) {
+
+ $poll = $messageDiv->find('div.tgme_widget_message_poll', 0);
+
+ $title = $poll->find('div.tgme_widget_message_poll_question', 0)->plaintext;
+ $type = $poll->find('div.tgme_widget_message_poll_type', 0)->plaintext;
+
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = $title;
+ }
+
+ $pollOptions = '<ul>';
+
+ foreach ($poll->find('div.tgme_widget_message_poll_option') as $option) {
+ $pollOptions .= '<li>' . $option->children(0)->plaintext . ' - ' .
+ $option->find('div.tgme_widget_message_poll_option_text', 0)->plaintext . '</li>';
+ }
+ $pollOptions .= '</ul>';
+
+ return <<<EOD
+ {$title}<br><small>$type</small><br>{$pollOptions}
+EOD;
+ }
+
+ private function processLinkPreview($messageDiv) {
+
+ $image = '';
+ $title = '';
+ $site = '';
+ $description = '';
+
+ $preview = $messageDiv->find('a.tgme_widget_message_link_preview', 0);
+
+ if (trim($preview->innertext) === '') {
+ return '';
+ }
+
+ if($preview->find('i', 0) &&
+ preg_match($this->backgroundImageRegex, $preview->find('i', 0)->style, $photo)) {
+
+ $image = '<img src="' . $photo[1] . '"/>';
+ $this->enclosures[] = $photo[1];
+ }
+
+ if ($preview->find('div.link_preview_title', 0)) {
+ $title = $preview->find('div.link_preview_title', 0)->plaintext;
+ }
+
+ if ($preview->find('div.link_preview_site_name', 0)) {
+ $site = $preview->find('div.link_preview_site_name', 0)->plaintext;
+ }
+
+ if ($preview->find('div.link_preview_description', 0)) {
+ $description = $preview->find('div.link_preview_description', 0)->plaintext;
+ }
+
+ return <<<EOD
+<blockquote><a href="{$preview->href}">$image</a><br><a href="{$preview->href}">
+{$title} - {$site}</a><br>{$description}</blockquote>
+EOD;
+ }
+
+ private function processVideo($messageDiv) {
+
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = '@' . $this->processUsername() . ' posted a video';
+ }
+
+ if ($messageDiv->find('i.tgme_widget_message_video_thumb')) {
+ preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo);
+ } elseif ($messageDiv->find('i.link_preview_video_thumb')) {
+ preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo);
+ }
+
+ $this->enclosures[] = $photo[1];
+
+ return <<<EOD
+<video controls="" poster="{$photo[1]}" preload="none">
+ <source src="{$messageDiv->find('video', 0)->src}" type="video/mp4">
+</video>
+EOD;
+ }
+
+ private function processPhoto($messageDiv) {
+
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = '@' . $this->processUsername() . ' posted a photo';
+ }
+
+ $photos = '';
+
+ foreach ($messageDiv->find('a.tgme_widget_message_photo_wrap') as $photoWrap) {
+ preg_match($this->backgroundImageRegex, $photoWrap->style, $photo);
+
+ $this->enclosures[] = $photo[1];
+
+ $photos .= <<<EOD
+<a href="{$photoWrap->href}"><img src="{$photo[1]}"/></a><br>
+EOD;
+ }
+ return $photos;
+ }
+
+ private function processNotSupported($messageDiv) {
+
+ if (empty($this->itemTitle)) {
+ $this->itemTitle = '@' . $this->processUsername() . ' posted a video';
+ }
+
+ if ($messageDiv->find('i.tgme_widget_message_video_thumb')) {
+ preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo);
+ } elseif ($messageDiv->find('i.link_preview_video_thumb')) {
+ preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo);
+ }
+
+ $this->enclosures[] = $photo[1];
+
+ return <<<EOD
+<a href="{$messageDiv->find('a.not_supported', 0)->href}">
+{$messageDiv->find('div.message_media_not_supported_label', 0)->innertext}<br><br>
+{$messageDiv->find('span.message_media_view_in_telegram', 0)->innertext}<br><br>
+<img src="{$photo[1]}"/></a>
+EOD;
+ }
+
+ private function processDate($messageDiv) {
+
+ $messageMeta = $messageDiv->find('span.tgme_widget_message_meta', 0);
+ return $messageMeta->find('time', 0)->datetime;
+
+ }
+
+ private function ellipsisTitle($text) {
+
+ $length = 100;
+
+ if (strlen($text) > $length) {
+ $text = explode('<br>', wordwrap($text, $length, '<br>'));
+ return $text[0] . '...';
+ }
+ return $text;
+ }
+}
diff --git a/bridges/TheGuardianBridge.php b/bridges/TheGuardianBridge.php
new file mode 100644
index 0000000..e655f0e
--- /dev/null
+++ b/bridges/TheGuardianBridge.php
@@ -0,0 +1,96 @@
+<?php
+class TheGuardianBridge extends FeedExpander {
+ const MAINTAINER = 'IceWreck';
+ const NAME = 'The Guardian Bridge';
+ const URI = 'https://www.theguardian.com/';
+ const CACHE_TIMEOUT = 600; // This is a news site, so don't cache for more than 10 mins
+ const DESCRIPTION = 'RSS feed for The Guardian';
+ const PARAMETERS = array( array(
+ 'feed' => array(
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'values' => array(
+ 'World News' => 'world/rss',
+ 'US News' => '/us-news/rss',
+ 'UK News' => '/uk-news/rss',
+ 'Europe News' => '/world/europe-news/rss',
+ 'Asia News' => '/world/asia/rss',
+ 'Tech' => '/uk/technology/rss',
+ 'Business News' => '/uk/business/rss',
+ 'Opinion' => '/uk/commentisfree/rss',
+ 'Lifestyle' => '/uk/lifeandstyle/rss',
+ 'Culture' => '/uk/culture/rss',
+ 'Sports' => '/uk/sport/rss'
+ )
+ )
+
+ /*
+
+ Topicwise Links
+
+ You can find the base feed for any topic by appending /rss to the url.
+
+ Example:
+
+ https://feeds.theguardian.com/theguardian/uk-news/rss
+ https://feeds.theguardian.com/theguardian/us-news/rss
+
+ Or simply
+
+ https://www.theguardian.com/world/rss
+
+ Just add that topic as a value in the PARAMETERS const.
+
+ */
+
+
+ ));
+
+ public function collectData(){
+ $feed = $this->getInput('feed');
+ $feedURL = 'https://feeds.theguardian.com/theguardian/' . $feed;
+ $this->collectExpandableDatas($feedURL, 10);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+
+ // --- Recovering the article ---
+
+ // $articlePage gets the entire page's contents
+ $articlePage = getSimpleHTMLDOM($newsItem->link);
+ // figure contain's the main article image
+ $article = $articlePage->find('figure', 0);
+ // content__article-body has the actual article
+ foreach($articlePage->find('.content__article-body') as $element)
+ $article = $article . $element;
+
+ // --- Fixing ugly elements ---
+
+ // Replace the image viewer and BS with the image itself
+ foreach($articlePage->find('a.article__img-container') as $uslElementLoc) {
+ $main_img = $uslElementLoc->find('img', 0);
+ $article = str_replace($uslElementLoc, $main_img, $article);
+ }
+
+ // List of all the crap in the article
+ $uselessElements = array(
+ '#show-caption',
+ '.element-atom',
+ '.submeta',
+ 'youtube-media-atom',
+ 'svg'
+ );
+
+ // Remove the listed crap
+ foreach($uselessElements as $uslElement) {
+ foreach($articlePage->find($uslElement) as $uslElementLoc) {
+ $article = str_replace($uslElementLoc, '', $article);
+ }
+ }
+
+ $item['content'] = $article;
+
+ return $item;
+ }
+}
diff --git a/bridges/ThePirateBayBridge.php b/bridges/ThePirateBayBridge.php
index 9aefcbb..5fc04eb 100644
--- a/bridges/ThePirateBayBridge.php
+++ b/bridges/ThePirateBayBridge.php
@@ -3,7 +3,7 @@ class ThePirateBayBridge extends BridgeAbstract {
const MAINTAINER = 'mitsukarenai';
const NAME = 'The Pirate Bay';
- const URI = 'https://thepiratebay.wf/';
+ 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
@@ -149,11 +149,12 @@ class ThePirateBayBridge extends BridgeAbstract {
|| !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['uri'] = self::URI . $element->find('a.detLink', 0)->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['magnet'] = $element->find('a', 3)->href;
$item['seeders'] = (int)$element->find('td', 2)->plaintext;
$item['leechers'] = (int)$element->find('td', 3)->plaintext;
$item['content'] = $element->find('font', 0)->plaintext
@@ -163,7 +164,9 @@ class ThePirateBayBridge extends BridgeAbstract {
. $item['leechers']
. '<br><a href="'
. $item['id']
- . '">info page</a>';
+ . '">info page</a><br><a href="'
+ . $item['magnet']
+ . '">magnet link</a>';
if(isset($item['title']))
$this->items[] = $item;
diff --git a/bridges/TwitchBridge.php b/bridges/TwitchBridge.php
new file mode 100644
index 0000000..39b4601
--- /dev/null
+++ b/bridges/TwitchBridge.php
@@ -0,0 +1,202 @@
+<?php
+class TwitchBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Roliga';
+ const NAME = 'Twitch Bridge';
+ const URI = 'https://twitch.tv/';
+ const CACHE_TIMEOUT = 300; // 5min
+ const DESCRIPTION = 'Twitch channel videos';
+ const PARAMETERS = array( array(
+ 'channel' => array(
+ 'name' => 'Channel',
+ 'type' => 'text',
+ 'required' => true,
+ 'title' => 'Lowercase channel name as seen in channel URL'
+ ),
+ 'type' => array(
+ 'name' => 'Type',
+ 'type' => 'list',
+ 'values' => array(
+ 'All' => 'all',
+ 'Archive' => 'archive',
+ 'Highlights' => 'highlight',
+ 'Uploads' => 'upload'
+ ),
+ 'defaultValue' => 'archive'
+ )
+ ));
+
+ /*
+ * Official instructions for obtaining your own client ID can be found here:
+ * https://dev.twitch.tv/docs/v5/#getting-a-client-id
+ */
+ const CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
+
+ public function collectData(){
+ // get channel user
+ $query_data = array(
+ 'login' => $this->getInput('channel')
+ );
+ $users = $this->apiGet('users', $query_data)->users;
+ if(count($users) === 0)
+ returnClientError('User "'
+ . $this->getInput('channel')
+ . '" could not be found');
+ $user = $users[0];
+
+ // get video list
+ $query_endpoint = 'channels/' . $user->_id . '/videos';
+ $query_data = array(
+ 'broadcast_type' => $this->getInput('type'),
+ 'limit' => 10
+ );
+ $videos = $this->apiGet($query_endpoint, $query_data)->videos;
+
+ foreach($videos as $video) {
+ $item = array(
+ 'uri' => $video->url,
+ 'title' => $video->title,
+ 'timestamp' => $video->published_at,
+ 'author' => $video->channel->display_name,
+ );
+
+ // Add categories for tags and played game
+ $item['categories'] = array_filter(explode(' ', $video->tag_list));
+ if(!empty($video->game))
+ $item['categories'][] = $video->game;
+
+ // Add enclosures for thumbnails from a few points in the video
+ $item['enclosures'] = array();
+ foreach($video->thumbnails->large as $thumbnail)
+ $item['enclosures'][] = $thumbnail->url;
+
+ /*
+ * Content format example:
+ *
+ * [Preview Image]
+ *
+ * Some optional video description.
+ *
+ * Duration: 1:23:45
+ * Views: 123
+ *
+ * Played games:
+ * * 00:00:00 Game 1
+ * * 00:12:34 Game 2
+ *
+ */
+ $item['content'] = '<p><a href="'
+ . $video->url
+ . '"><img src="'
+ . $video->preview->large
+ . '" /></a></p><p>'
+ . $video->description_html
+ . '</p><p><b>Duration:</b> '
+ . $this->formatTimestampTime($video->length)
+ . '<br/><b>Views:</b> '
+ . $video->views
+ . '</p>';
+
+ // Add played games list to content
+ $video_id = trim($video->_id, 'v'); // _id gives 'v1234' but API wants '1234'
+ $markers = $this->apiGet('videos/' . $video_id . '/markers')->markers;
+ $item['content'] .= '<p><b>Played games:</b></b><ul><li><a href="'
+ . $video->url
+ . '">00:00:00</a> - '
+ . $video->game
+ . '</li>';
+ if(isset($markers->game_changes)) {
+ usort($markers->game_changes, function($a, $b) {
+ return $a->time - $b->time;
+ });
+ foreach($markers->game_changes as $game_change) {
+ $item['categories'][] = $game_change->label;
+ $item['content'] .= '<li><a href="'
+ . $video->url
+ . '?t='
+ . $this->formatQueryTime($game_change->time)
+ . '">'
+ . $this->formatTimestampTime($game_change->time)
+ . '</a> - '
+ . $game_change->label
+ . '</li>';
+ }
+ }
+ $item['content'] .= '</ul></p>';
+
+ $this->items[] = $item;
+ }
+ }
+
+ // e.g. 01:53:27
+ private function formatTimestampTime($seconds) {
+ return sprintf('%02d:%02d:%02d',
+ floor($seconds / 3600),
+ ($seconds / 60) % 60,
+ $seconds % 60);
+ }
+
+ // e.g. 01h53m27s
+ private function formatQueryTime($seconds) {
+ return sprintf('%02dh%02dm%02ds',
+ floor($seconds / 3600),
+ ($seconds / 60) % 60,
+ $seconds % 60);
+ }
+
+ /*
+ * Ideally the new 'helix' API should be used as v5/'kraken' is deprecated.
+ * The new API however still misses many features (markers, played game..) of
+ * the old one, so let's use the old one for as long as it's available.
+ */
+ private function apiGet($endpoint, $query_data = array()) {
+ $query_data['api_version'] = 5;
+ $url = 'https://api.twitch.tv/kraken/'
+ . $endpoint
+ . '?'
+ . http_build_query($query_data);
+ $header = array(
+ 'Client-ID: ' . self::CLIENT_ID
+ );
+
+ $data = json_decode(getContents($url, $header))
+ or returnServerError('API request to "' . $url . '" failed.');
+
+ return $data;
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('channel'))) {
+ return $this->getInput('channel') . ' twitch videos';
+ }
+
+ return parent::getName();
+ }
+
+ public function getURI(){
+ if(!is_null($this->getInput('channel'))) {
+ return self::URI . $this->getInput('channel');
+ }
+
+ return parent::getURI();
+ }
+
+ public function detectParameters($url){
+ $params = array();
+
+ // Matches e.g. https://www.twitch.tv/someuser/videos?filter=archives
+ $regex = '/^(https?:\/\/)?
+ (www\.)?
+ twitch\.tv\/
+ ([^\/&?\n]+)
+ \/videos\?.*filter=
+ (all|archive|highlight|upload)/x';
+ if(preg_match($regex, $url, $matches) > 0) {
+ $params['channel'] = urldecode($matches[3]);
+ $params['type'] = $matches[4];
+ return $params;
+ }
+
+ return null;
+ }
+}
diff --git a/bridges/TwitterBridge.php b/bridges/TwitterBridge.php
index 32ed942..2f5565b 100644
--- a/bridges/TwitterBridge.php
+++ b/bridges/TwitterBridge.php
@@ -28,7 +28,31 @@ class TwitterBridge extends BridgeAbstract {
'name' => 'Keyword or #hashtag',
'required' => true,
'exampleValue' => 'rss-bridge, #rss-bridge',
- 'title' => 'Insert a keyword or hashtag'
+ 'title' => <<<EOD
+* To search for multiple words (must contain all of these words), put a space between them.
+
+Example: `rss-bridge release`.
+
+* To search for multiple words (contains any of these words), put "OR" between them.
+
+Example: `rss-bridge OR rssbridge`.
+
+* To search for an exact phrase (including whitespace), put double-quotes around them.
+
+Example: `"rss-bridge release"`
+
+* If you want to search for anything **but** a specific word, put a hyphen before it.
+
+Example: `rss-bridge -release` (ignores "release")
+
+* Of course, this also works for hashtags.
+
+Example: `#rss-bridge OR #rssbridge`
+
+* And you can combine them in any shape or form you like.
+
+Example: `#rss-bridge OR #rssbridge -release`
+EOD
)
),
'By username' => array(
@@ -146,8 +170,15 @@ class TwitterBridge extends BridgeAbstract {
public function collectData(){
$html = '';
+ $page = $this->getURI();
+
+ if(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo'))) {
+ $cookies = $this->getCookies($page);
+ $html = getSimpleHTMLDOM($page, array("Cookie: $cookies"));
+ } else {
+ $html = getSimpleHTMLDOM($page, array(), array(CURLOPT_COOKIEFILE => ''));
+ }
- $html = getSimpleHTMLDOM($this->getURI());
if(!$html) {
switch($this->queriedContext) {
case 'By keyword or hashtag':
@@ -165,7 +196,7 @@ class TwitterBridge extends BridgeAbstract {
// Skip retweets?
if($this->getInput('noretweet')
- && $tweet->getAttribute('data-screen-name') !== $this->getInput('u')) {
+ && $tweet->find('div.context span.js-retweet-text a', 0)) {
continue;
}
@@ -189,6 +220,9 @@ class TwitterBridge extends BridgeAbstract {
$item['fullname'] = htmlspecialchars_decode($tweet->getAttribute('data-name'), ENT_QUOTES);
// get author
$item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
+ if($rt = $tweet->find('div.context span.js-retweet-text a', 0)) {
+ $item['author'] .= ' RT: @' . $rt->plaintext;
+ }
// get avatar link
$item['avatar'] = $tweet->find('img', 0)->src;
// get TweetID
@@ -242,22 +276,26 @@ EOD;
// Add embeded image to content
$image_html = '';
- $image = $this->getImageURI($tweet);
- if(!$this->getInput('noimg') && !is_null($image)) {
- // Set image scaling
- $image_orig = $this->getInput('noimgscaling') ? $image : $image . ':orig';
- $image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb';
+ $images = $this->getImageURI($tweet);
+ if(!$this->getInput('noimg') && !is_null($images)) {
+
+ foreach ($images as $image) {
- // add enclosures
- $item['enclosures'] = array($image_orig);
+ // Set image scaling
+ $image_orig = $this->getInput('noimgscaling') ? $image : $image . ':orig';
+ $image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb';
+
+ // add enclosures
+ $item['enclosures'][] = $image_orig;
- $image_html = <<<EOD
+ $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
@@ -288,22 +326,27 @@ EOD;
// Add embeded image to content
$quotedImage_html = '';
- $quotedImage = $this->getQuotedImageURI($tweet);
- if(!$this->getInput('noimg') && !is_null($quotedImage)) {
- // Set image scaling
- $quotedImage_orig = $this->getInput('noimgscaling') ? $quotedImage : $quotedImage . ':orig';
- $quotedImage_thumb = $this->getInput('noimgscaling') ? $quotedImage : $quotedImage . ':thumb';
+ $quotedImages = $this->getQuotedImageURI($tweet);
- // add enclosures
- $item['enclosures'] = array($quotedImage_orig);
+ if(!$this->getInput('noimg') && !is_null($quotedImages)) {
+
+ foreach ($quotedImages as $image) {
+
+ // Set image scaling
+ $image_orig = $this->getInput('noimgscaling') ? $image : $image . ':orig';
+ $image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb';
- $quotedImage_html = <<<EOD
-<a href="{$quotedImage_orig}">
+ // add enclosures
+ $item['enclosures'][] = $image_orig;
+
+ $quotedImage_html .= <<<EOD
+<a href="{$image_orig}">
<img
style="align:top; max-width:558px; border:1px solid black;"
- src="{$quotedImage_thumb}" />
+ src="{$image_thumb}" />
</a>
EOD;
+ }
}
$item['content'] = <<<EOD
@@ -357,9 +400,18 @@ EOD;
private function getImageURI($tweet){
// Find media in tweet
+ $images = array();
+
$container = $tweet->find('div.AdaptiveMedia-container', 0);
+
if($container && $container->find('img', 0)) {
- return $container->find('img', 0)->src;
+ foreach ($container->find('img') as $img) {
+ $images[] = $img->src;
+ }
+ }
+
+ if (!empty($images)) {
+ return $images;
}
return null;
@@ -367,11 +419,43 @@ EOD;
private function getQuotedImageURI($tweet){
// Find media in tweet
+ $images = array();
+
$container = $tweet->find('div.QuoteMedia-container', 0);
+
if($container && $container->find('img', 0)) {
- return $container->find('img', 0)->src;
+ foreach ($container->find('img') as $img) {
+ $images[] = $img->src;
+ }
+ }
+
+ if (!empty($images)) {
+ return $images;
}
return null;
}
+
+ private function getCookies($pageURL){
+
+ $ctx = stream_context_create(array(
+ 'http' => array(
+ 'follow_location' => false
+ )
+ )
+ );
+ $a = file_get_contents($pageURL, 0, $ctx);
+
+ //First request to get the cookie
+ $cookies = '';
+ foreach($http_response_header as $hdr) {
+ if(stripos($hdr, 'Set-Cookie') !== false) {
+ $cLine = explode(':', $hdr)[1];
+ $cLine = explode(';', $cLine)[0];
+ $cookies .= ';' . $cLine;
+ }
+ }
+
+ return substr($cookies, 2);
+ }
}
diff --git a/bridges/UnsplashBridge.php b/bridges/UnsplashBridge.php
index ae76734..dad0efc 100644
--- a/bridges/UnsplashBridge.php
+++ b/bridges/UnsplashBridge.php
@@ -3,7 +3,7 @@ class UnsplashBridge extends BridgeAbstract {
const MAINTAINER = 'nel50n';
const NAME = 'Unsplash Bridge';
- const URI = 'http://unsplash.com/';
+ const URI = 'https://unsplash.com/';
const CACHE_TIMEOUT = 43200; // 12h
const DESCRIPTION = 'Returns the latests photos from Unsplash';
@@ -27,51 +27,42 @@ class UnsplashBridge extends BridgeAbstract {
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.');
+ $api_response = getContents('https://unsplash.com/napi/photos?page=1&per_page=' . $max)
+ or returnServerError('Could not request Unsplash API.');
+ $json = json_decode($api_response, true);
- if($page === 1) {
- preg_match(
- '/=(\d+)$/',
- $html->find('.pagination > a[!class]', -1)->href,
- $matches
- );
+ foreach ($json as $json_item) {
+ $item = array();
- $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);
+ // Get image URI
+ $uri = $json_item['urls']['regular'] . '.jpg'; // '.jpg' only for format hint
+ $uri = str_replace('q=80', 'q=' . $quality, $uri);
+ $uri = str_replace('w=1080', 'w=' . $width, $uri);
+ $item['uri'] = $uri;
- $item = array();
- $item['uri'] = str_replace(
- array('q=75', 'w=400'),
- array("q=$quality", "w=$width"),
- $thumbnail->src) . '.jpg'; // '.jpg' only for format hint
+ // Get title from description
+ if (is_null($json_item['alt_description'])) {
+ if (is_null($json_item['description'])) {
+ $item['title'] = 'Unsplash picture from ' . $json_item['user']['name'];
+ } else {
+ $item['title'] = $json_item['description'];
+ }
+ } else {
+ $item['title'] = $json_item['alt_description'];
+ }
- $item['timestamp'] = time();
- $item['title'] = $thumbnail->alt;
- $item['content'] = $item['title']
+ $item['timestamp'] = time();
+ $item['content'] = $item['title']
. '<br><a href="'
. $item['uri']
. '"><img src="'
- . $thumbnail->src
+ . $json_item['urls']['thumb']
. '" /></a>';
- $this->items[] = $item;
-
- $num++;
- if ($num >= $max)
- break 2;
- }
+ $this->items[] = $item;
}
}
}
diff --git a/bridges/VMwareSecurityBridge.php b/bridges/VMwareSecurityBridge.php
new file mode 100644
index 0000000..326d26a
--- /dev/null
+++ b/bridges/VMwareSecurityBridge.php
@@ -0,0 +1,31 @@
+<?php
+class VMwareSecurityBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'm0le.net';
+ const NAME = 'VMware Security Advisories';
+ const URI = 'https://www.vmware.com/security/advisories.html';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'VMware Security Advisories';
+ const WEBROOT = 'https://www.vmware.com';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request VSA.');
+
+ $html = defaultLinkTo($html, self::WEBROOT);
+
+ $item = array();
+ $articles = $html->find('div[class="news_block"]');
+
+ foreach ($articles as $element) {
+ $item['uri'] = $element->find('a', 0)->getAttribute('href');
+ $title = $element->find('a', 0)->innertext;
+ $item['title'] = $title;
+ $item['timestamp'] = strtotime($element->find('p', 0)->innertext);
+ $item['content'] = $element->find('p', 1)->innertext;
+ $item['uid'] = $title;
+
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/VimeoBridge.php b/bridges/VimeoBridge.php
new file mode 100644
index 0000000..d318e30
--- /dev/null
+++ b/bridges/VimeoBridge.php
@@ -0,0 +1,175 @@
+<?php
+
+class VimeoBridge extends BridgeAbstract {
+
+ const NAME = 'Vimeo Bridge';
+ const URI = 'https://vimeo.com/';
+ const DESCRIPTION = 'Returns search results from Vimeo';
+ const MAINTAINER = 'logmanoriginal';
+
+ const PARAMETERS = array(
+ array(
+ 'q' => array(
+ 'name' => 'Search Query',
+ 'type' => 'text',
+ 'required' => true
+ ),
+ 'type' => array(
+ 'name' => 'Show results for',
+ 'type' => 'list',
+ 'defaultValue' => 'Videos',
+ 'values' => array(
+ 'Videos' => 'search',
+ 'On Demand' => 'search/ondemand',
+ 'People' => 'search/people',
+ 'Channels' => 'search/channels',
+ 'Groups' => 'search/groups'
+ )
+ )
+ )
+ );
+
+ public function getURI() {
+ if(($query = $this->getInput('q'))
+ && ($type = $this->getInput('type'))) {
+ return self::URI . $type . '/sort:latest?q=' . $query;
+ }
+
+ return parent::getURI();
+ }
+
+ public function collectData() {
+
+ $html = getSimpleHTMLDOM($this->getURI(),
+ $header = array(),
+ $opts = array(),
+ $lowercase = true,
+ $forceTagsClosed = true,
+ $target_charset = DEFAULT_TARGET_CHARSET,
+ $stripRN = false, // We want to keep newline characters
+ $defaultBRText = DEFAULT_BR_TEXT,
+ $defaultSpanText = DEFAULT_SPAN_TEXT)
+ or returnServerError('Could not request ' . $this->getURI());
+
+ $json = null; // Holds the JSON data
+
+ /**
+ * Search results are included as JSON formatted string inside a script
+ * tag that has the variable 'vimeo.config'. The data is condensed into
+ * a single line of code, so we can just search for the newline.
+ *
+ * Everything after "vimeo.config = _extend((vimeo.config || {}), " is
+ * the JSON formatted string.
+ */
+ foreach($html->find('script') as $script) {
+ foreach(explode("\n", $script) as $line) {
+ $line = trim($line);
+
+ if(strpos($line, 'vimeo.config') !== 0)
+ continue;
+
+ // 45 = strlen("vimeo.config = _extend((vimeo.config || {}), ");
+ // 47 = 45 + 2, because we don't want the final ");"
+ $json = json_decode(substr($line, 45, strlen($line) - 47));
+ }
+ }
+
+ if(is_null($json)) {
+ returnClientError('No results for this query!');
+ }
+
+ foreach($json->api->initial_json->data as $element) {
+ switch($element->type) {
+ case 'clip': $this->addClip($element); break;
+ case 'ondemand': $this->addOnDemand($element); break;
+ case 'people': $this->addPeople($element); break;
+ case 'channel': $this->addChannel($element); break;
+ case 'group': $this->addGroup($element); break;
+
+ default: returnServerError('Unknown type: ' . $element->type);
+ }
+ }
+
+ }
+
+ private function addClip($element) {
+ $item = array();
+
+ $item['uri'] = $element->clip->link;
+ $item['title'] = $element->clip->name;
+ $item['author'] = $element->clip->user->name;
+ $item['timestamp'] = strtotime($element->clip->created_time);
+
+ $item['enclosures'] = array(
+ end($element->clip->pictures->sizes)->link
+ );
+
+ $item['content'] = "<img src={$item['enclosures'][0]} />";
+
+ $this->items[] = $item;
+ }
+
+ private function addOnDemand($element) {
+ $item = array();
+
+ $item['uri'] = $element->ondemand->link;
+ $item['title'] = $element->ondemand->name;
+
+ // Only for films
+ if(isset($element->ondemand->film))
+ $item['timestamp'] = strtotime($element->ondemand->film->release_time);
+
+ $item['enclosures'] = array(
+ end($element->ondemand->pictures->sizes)->link
+ );
+
+ $item['content'] = "<img src={$item['enclosures'][0]} />";
+
+ $this->items[] = $item;
+ }
+
+ private function addPeople($element) {
+ $item = array();
+
+ $item['uri'] = $element->people->link;
+ $item['title'] = $element->people->name;
+
+ $item['enclosures'] = array(
+ end($element->people->pictures->sizes)->link
+ );
+
+ $item['content'] = "<img src={$item['enclosures'][0]} />";
+
+ $this->items[] = $item;
+ }
+
+ private function addChannel($element) {
+ $item = array();
+
+ $item['uri'] = $element->channel->link;
+ $item['title'] = $element->channel->name;
+
+ $item['enclosures'] = array(
+ end($element->channel->pictures->sizes)->link
+ );
+
+ $item['content'] = "<img src={$item['enclosures'][0]} />";
+
+ $this->items[] = $item;
+ }
+
+ private function addGroup($element) {
+ $item = array();
+
+ $item['uri'] = $element->group->link;
+ $item['title'] = $element->group->name;
+
+ $item['enclosures'] = array(
+ end($element->group->pictures->sizes)->link
+ );
+
+ $item['content'] = "<img src={$item['enclosures'][0]} />";
+
+ $this->items[] = $item;
+ }
+}
diff --git a/bridges/VkBridge.php b/bridges/VkBridge.php
index d4e84d9..f9aaa66 100644
--- a/bridges/VkBridge.php
+++ b/bridges/VkBridge.php
@@ -13,6 +13,10 @@ class VkBridge extends BridgeAbstract
'u' => array(
'name' => 'Group or user name',
'required' => true
+ ),
+ 'hide_reposts' => array(
+ 'name' => 'Hide reposts',
+ 'type' => 'checkbox',
)
)
);
@@ -48,7 +52,7 @@ class VkBridge extends BridgeAbstract
$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);
+ $text_html = iconv('windows-1251', 'utf-8//ignore', $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);
@@ -234,6 +238,9 @@ class VkBridge extends BridgeAbstract
}
if (is_object($post->find('div.copy_quote', 0))) {
+ if ($this->getInput('hide_reposts') === true) {
+ continue;
+ }
$copy_quote = $post->find('div.copy_quote', 0);
if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) {
$copy_post_header->outertext = '';
diff --git a/bridges/WikiLeaksBridge.php b/bridges/WikiLeaksBridge.php
index c5b9bb6..363cf0c 100644
--- a/bridges/WikiLeaksBridge.php
+++ b/bridges/WikiLeaksBridge.php
@@ -9,7 +9,6 @@ class WikiLeaksBridge extends BridgeAbstract {
'category' => array(
'name' => 'Category',
'type' => 'list',
- 'required' => true,
'title' => 'Select your category',
'values' => array(
'News' => '-News-',
@@ -28,7 +27,6 @@ class WikiLeaksBridge extends BridgeAbstract {
'teaser' => array(
'name' => 'Show teaser',
'type' => 'checkbox',
- 'required' => false,
'title' => 'If checked feeds will display the teaser',
'defaultValue' => true
)
diff --git a/bridges/WikipediaBridge.php b/bridges/WikipediaBridge.php
index 6b53440..7ca763f 100644
--- a/bridges/WikipediaBridge.php
+++ b/bridges/WikipediaBridge.php
@@ -13,7 +13,6 @@ class WikipediaBridge extends BridgeAbstract {
'language' => array(
'name' => 'Language',
'type' => 'list',
- 'required' => true,
'title' => 'Select your language',
'exampleValue' => 'English',
'values' => array(
@@ -27,7 +26,6 @@ class WikipediaBridge extends BridgeAbstract {
'subject' => array(
'name' => 'Subject',
'type' => 'list',
- 'required' => true,
'title' => 'What subject are you interested in?',
'exampleValue' => 'Today\'s featured article',
'values' => array(
diff --git a/bridges/WiredBridge.php b/bridges/WiredBridge.php
new file mode 100644
index 0000000..8da93d0
--- /dev/null
+++ b/bridges/WiredBridge.php
@@ -0,0 +1,102 @@
+<?php
+class WiredBridge extends FeedExpander {
+ const MAINTAINER = 'ORelio';
+ const NAME = 'WIRED Bridge';
+ const URI = 'https://www.wired.com/';
+ const DESCRIPTION = 'Returns the newest articles from WIRED';
+
+ const PARAMETERS = array( array(
+ 'feed' => array(
+ 'name' => 'Feed',
+ 'type' => 'list',
+ 'values' => array(
+ 'WIRED Top Stories' => 'rss', // /feed/rss
+ 'Business' => 'business', // /feed/category/business/latest/rss
+ 'Culture' => 'culture', // /feed/category/culture/latest/rss
+ 'Gear' => 'gear', // /feed/category/gear/latest/rss
+ 'Ideas' => 'ideas', // /feed/category/ideas/latest/rss
+ 'Science' => 'science', // /feed/category/science/latest/rss
+ 'Security' => 'security', // /feed/category/security/latest/rss
+ 'Transportation' => 'transportation', // /feed/category/transportation/latest/rss
+ 'Backchannel' => 'backchannel', // /feed/category/backchannel/latest/rss
+ 'WIRED Guides' => 'wired-guide', // /feed/tag/wired-guide/latest/rss
+ 'Photo' => 'photo' // /feed/category/photo/latest/rss
+ )
+ )
+ ));
+
+ public function collectData(){
+ $feed = $this->getInput('feed');
+ if(empty($feed) || !ctype_alpha(str_replace('-', '', $feed))) {
+ returnClientError('Invalid feed, please check the "feed" parameter.');
+ }
+
+ $feed_url = $this->getURI() . 'feed/';
+ if ($feed != 'rss') {
+ if ($feed != 'wired-guide') {
+ $feed_url .= 'category/';
+ } else {
+ $feed_url .= 'tag/';
+ }
+ $feed_url .= "$feed/latest/";
+ }
+ $feed_url .= 'rss';
+
+ $this->collectExpandableDatas($feed_url);
+ }
+
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $article = getSimpleHTMLDOMCached($item['uri'])
+ or returnServerError('Could not request WIRED: ' . $item['uri']);
+ $item['content'] = $this->extractArticleContent($article);
+
+ $headline = strval($newsItem->description);
+ if(!empty($headline)) {
+ $item['content'] = '<p><b>' . $headline . '</b></p>' . $item['content'];
+ }
+
+ $item_image = $article->find('meta[property="og:image"]', 0);
+ if(!empty($item_image)) {
+ $item['enclosures'] = array($item_image->content);
+ $item['content'] = '<p><img src="' . $item_image->content . '" /></p>' . $item['content'];
+ }
+
+ return $item;
+ }
+
+ private function extractArticleContent($article){
+ $content = $article->find('article', 0);
+ $truncate = true;
+
+ if (empty($content)) {
+ $content = $article->find('div.listicle-main-component__container', 0);
+ $truncate = false;
+ }
+
+ if (!empty($content)) {
+ $content = $content->innertext;
+ }
+
+ foreach (array(
+ '<div class="content-header',
+ '<div class="mid-banner-wrap',
+ '<div class="related',
+ '<div class="social-icons',
+ '<div class="recirc-most-popular',
+ '<div class="grid--item article-related-video',
+ '<div class="row full-bleed-ad',
+ ) as $div_start) {
+ $content = stripRecursiveHTMLSection($content, 'div', $div_start);
+ }
+
+ if ($truncate) {
+ //Clutter after standard article is too hard to clean properly
+ $content = trim(explode('<hr', $content)[0]);
+ }
+
+ $content = str_replace('href="/', 'href="' . $this->getURI() . '/', $content);
+
+ return $content;
+ }
+}
diff --git a/bridges/WordPressPluginUpdateBridge.php b/bridges/WordPressPluginUpdateBridge.php
index 80b53ea..9101c4e 100644
--- a/bridges/WordPressPluginUpdateBridge.php
+++ b/bridges/WordPressPluginUpdateBridge.php
@@ -71,16 +71,4 @@ class WordPressPluginUpdateBridge extends BridgeAbstract {
return parent::getName();
}
-
- private function getCachedDate($url){
- Debug::log('getting pubdate from url ' . $url . '');
- // Initialize cache
- $cache = Cache::create('FileCache');
- $cache->setPath(PATH_CACHE . '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
index 46dd588..d48b2d6 100644
--- a/bridges/WorldOfTanksBridge.php
+++ b/bridges/WorldOfTanksBridge.php
@@ -3,7 +3,7 @@ class WorldOfTanksBridge extends FeedExpander {
const MAINTAINER = 'Riduidel';
const NAME = 'World of Tanks';
- const URI = 'http://worldoftanks.eu/';
+ const URI = 'https://worldoftanks.eu/';
const DESCRIPTION = 'News about the tank slaughter game.';
const PARAMETERS = array( array(
@@ -22,6 +22,8 @@ class WorldOfTanksBridge extends FeedExpander {
)
));
+ const POSSIBLE_ARTICLES = array('article', 'rich-article');
+
public function collectData() {
$this->collectExpandableDatas(sprintf('https://worldoftanks.eu/%s/rss/news/', $this->getInput('lang')));
}
@@ -40,13 +42,17 @@ class WorldOfTanksBridge extends FeedExpander {
private function loadFullArticle($uri){
$html = getSimpleHTMLDOMCached($uri);
- $content = $html->find('article', 0);
+ foreach(self::POSSIBLE_ARTICLES as $article_class) {
+ $content = $html->find('article', 0);
- // Remove the scripts, please
- foreach($content->find('script') as $script) {
- $script->outertext = '';
+ if($content !== null) {
+ // Remove the scripts, please
+ foreach($content->find('script') as $script) {
+ $script->outertext = '';
+ }
+ return $content->innertext;
+ }
}
-
- return $content->innertext;
+ return null;
}
}
diff --git a/bridges/XenForoBridge.php b/bridges/XenForoBridge.php
index 7bf1f15..983654e 100644
--- a/bridges/XenForoBridge.php
+++ b/bridges/XenForoBridge.php
@@ -118,7 +118,7 @@ class XenForoBridge extends BridgeAbstract {
// Notice: The DOM structure changes depending on the XenForo version used
if($mainContent = $html->find('div.mainContent', 0)) {
$this->version = self::XENFORO_VERSION_1;
- } elseif ($mainContent = $html->find('div[class="p-body"]', 0)) {
+ } elseif ($mainContent = $html->find('div[class~="p-body"]', 0)) {
$this->version = self::XENFORO_VERSION_2;
} else {
returnServerError('This forum is currently not supported!');
@@ -127,7 +127,7 @@ class XenForoBridge extends BridgeAbstract {
switch($this->version) {
case self::XENFORO_VERSION_1:
- $titleBar = $mainContent->find('div.titleBar h1', 0)
+ $titleBar = $mainContent->find('div.titleBar > h1', 0)
or returnServerError('Error finding title bar!');
$this->title = $titleBar->plaintext;
@@ -140,7 +140,7 @@ class XenForoBridge extends BridgeAbstract {
case self::XENFORO_VERSION_2:
- $titleBar = $mainContent->find('div[class="p-title"] h1', 0)
+ $titleBar = $mainContent->find('div[class~="p-title"] h1', 0)
or returnServerError('Error finding title bar!');
$this->title = $titleBar->plaintext;
@@ -166,7 +166,7 @@ class XenForoBridge extends BridgeAbstract {
$lang = $html->find('html', 0)->lang;
// Posts are contained in an "ol"
- $messageList = $html->find('#messageList li')
+ $messageList = $html->find('#messageList > li')
or returnServerError('Error finding message list!');
foreach($messageList as $post) {
@@ -179,7 +179,7 @@ class XenForoBridge extends BridgeAbstract {
$item['uri'] = $url . '#' . $post->getAttribute('id');
- $content = $post->find('.messageContent article', 0);
+ $content = $post->find('.messageContent > article', 0);
// Add some style to quotes
foreach($content->find('.bbCodeQuote') as $quote) {
@@ -255,7 +255,7 @@ class XenForoBridge extends BridgeAbstract {
$lang = $html->find('html', 0)->lang;
- $messageList = $html->find('div[class="block-body"] article')
+ $messageList = $html->find('div[class~="block-body"] article')
or returnServerError('Error finding message list!');
foreach($messageList as $post) {
@@ -268,13 +268,17 @@ class XenForoBridge extends BridgeAbstract {
$item['uri'] = $url . '#' . $post->getAttribute('id');
- $title = $post->find('div[class="message-content"] article', 0)->plaintext;
+ $title = $post->find('div[class~="message-content"] article', 0)->plaintext;
$end = strpos($title, ' ', 70);
$item['title'] = substr($title, 0, $end);
- $item['timestamp'] = $this->fixDate($post->find('time', 0)->title, $lang);
+ if ($post->find('time[datetime]', 0)) {
+ $item['timestamp'] = $post->find('time[datetime]', 0)->datetime;
+ } else {
+ $item['timestamp'] = $this->fixDate($post->find('time', 0)->title, $lang);
+ }
$item['author'] = $post->getAttribute('data-author');
- $item['content'] = $post->find('div[class="message-content"] article', 0);
+ $item['content'] = $post->find('div[class~="message-content"] article', 0);
// Bridge specific properties
$item['id'] = $post->getAttribute('id');
@@ -305,7 +309,7 @@ class XenForoBridge extends BridgeAbstract {
// Load at least the last page
do {
- $pageurl = $hosturl . str_replace($sentinel, $lastpage, $baseurl);
+ $pageurl = str_replace($sentinel, $lastpage, $baseurl);
// We can optimize performance by caching all but the last page
if($page != $lastpage) {
@@ -339,7 +343,7 @@ class XenForoBridge extends BridgeAbstract {
}
// Manually extract baseurl and inject sentinel
- $baseurl = $pageNav->find('li a', -1)->href;
+ $baseurl = $pageNav->find('li > a', -1)->href;
$baseurl = str_replace('page-' . $lastpage, 'page-{{sentinel}}', $baseurl);
$sentinel = '{{sentinel}}';
@@ -353,7 +357,7 @@ class XenForoBridge extends BridgeAbstract {
// Load at least the last page
do {
- $pageurl = $hosturl . str_replace($sentinel, $lastpage, $baseurl);
+ $pageurl = str_replace($sentinel, $lastpage, $baseurl);
// We can optimize performance by caching all but the last page
if($page != $lastpage) {
@@ -364,9 +368,9 @@ class XenForoBridge extends BridgeAbstract {
or returnServerError('Error loading contents from ' . $pageurl . '!');
}
- $html = defaultLinkTo($html, $this->hosturl);
+ $html = defaultLinkTo($html, $hosturl);
- $this->extractThreadPostsV2($html, $this->pageurl);
+ $this->extractThreadPostsV2($html, $pageurl);
$page--;
diff --git a/bridges/YoutubeBridge.php b/bridges/YoutubeBridge.php
index 67e9566..90ee049 100644
--- a/bridges/YoutubeBridge.php
+++ b/bridges/YoutubeBridge.php
@@ -65,7 +65,7 @@ class YoutubeBridge extends BridgeAbstract {
private $feedName = '';
private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time){
- $html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid");
+ $html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid", true);
// Skip unavailable videos
if(!strpos($html->innertext, 'IS_UNAVAILABLE_PAGE')) {
@@ -127,7 +127,6 @@ class YoutubeBridge extends BridgeAbstract {
}
private function ytBridgeParseHtmlListing($html, $element_selector, $title_selector, $add_parsed_items = true) {
- $limit = $add_parsed_items ? 10 : INF;
$count = 0;
$duration_min = $this->getInput('duration_min') ?: -1;
@@ -141,40 +140,38 @@ class YoutubeBridge extends BridgeAbstract {
}
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 = trim($this->ytBridgeFixTitle($element->find($title_selector, 0)->plaintext));
-
- if (strpos($vid, 'googleads') !== false
- || $title == '[Private video]'
- || $title == '[Deleted video]'
- ) {
- continue;
- }
-
- // The duration comes in one of the formats:
- // hh:mm:ss / mm:ss / m:ss
- // 01:03:30 / 15:06 / 1:24
- $durationText = trim($element->find('div.timestamp span', 0)->plaintext);
- $durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText);
-
- sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds);
- $duration = $hours * 3600 + $minutes * 60 + $seconds;
-
- if($duration < $duration_min || $duration > $duration_max) {
- continue;
- }
-
- if ($add_parsed_items) {
- $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
- $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
- }
- $count++;
+ $author = '';
+ $desc = '';
+ $time = 0;
+ $vid = str_replace('/watch?v=', '', $element->find('a', 0)->href);
+ $vid = substr($vid, 0, strpos($vid, '&') ?: strlen($vid));
+ $title = trim($this->ytBridgeFixTitle($element->find($title_selector, 0)->plaintext));
+
+ if (strpos($vid, 'googleads') !== false
+ || $title == '[Private video]'
+ || $title == '[Deleted video]'
+ ) {
+ continue;
}
+
+ // The duration comes in one of the formats:
+ // hh:mm:ss / mm:ss / m:ss
+ // 01:03:30 / 15:06 / 1:24
+ $durationText = trim($element->find('div.timestamp span', 0)->plaintext);
+ $durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText);
+
+ sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds);
+ $duration = $hours * 3600 + $minutes * 60 + $seconds;
+
+ if($duration < $duration_min || $duration > $duration_max) {
+ continue;
+ }
+
+ if ($add_parsed_items) {
+ $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
+ $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
+ }
+ $count++;
}
return $count;
}
@@ -184,18 +181,38 @@ class YoutubeBridge extends BridgeAbstract {
return html_entity_decode($title, ENT_QUOTES, 'UTF-8');
}
- private function ytGetSimpleHTMLDOM($url){
+ private function ytGetSimpleHTMLDOM($url, $cached = false){
+ $header = array(
+ 'Accept-Language: en-US'
+ );
+ $opts = array();
+ $lowercase = true;
+ $forceTagsClosed = true;
+ $target_charset = DEFAULT_TARGET_CHARSET;
+ $stripRN = false;
+ $defaultBRText = DEFAULT_BR_TEXT;
+ $defaultSpanText = DEFAULT_SPAN_TEXT;
+ if ($cached) {
+ return getSimpleHTMLDOMCached($url,
+ 86400,
+ $header,
+ $opts,
+ $lowercase,
+ $forceTagsClosed,
+ $target_charset,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText);
+ }
return getSimpleHTMLDOM($url,
- $header = array(
- 'Accept-Language: en-US'
- ),
- $opts = array(),
- $lowercase = true,
- $forceTagsClosed = true,
- $target_charset = DEFAULT_TARGET_CHARSET,
- $stripRN = false,
- $defaultBRText = DEFAULT_BR_TEXT,
- $defaultSpanText = DEFAULT_SPAN_TEXT);
+ $header,
+ $opts,
+ $lowercase,
+ $forceTagsClosed,
+ $target_charset,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText);
}
public function collectData(){
diff --git a/cache/pages/.gitkeep b/cache/pages/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cache/pages/.gitkeep
diff --git a/cache/server/.gitkeep b/cache/server/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cache/server/.gitkeep
diff --git a/caches/FileCache.php b/caches/FileCache.php
index 04d08a2..166ecdb 100644
--- a/caches/FileCache.php
+++ b/caches/FileCache.php
@@ -3,20 +3,21 @@
* Cache with file system
*/
class FileCache implements CacheInterface {
-
protected $path;
- protected $param;
+ protected $key;
public function loadData(){
if(file_exists($this->getCacheFile())) {
return unserialize(file_get_contents($this->getCacheFile()));
}
+
+ return null;
}
- public function saveData($datas){
+ public function saveData($data){
// Notice: We use plain serialize() here to reduce memory footprint on
// large input data.
- $writeStream = file_put_contents($this->getCacheFile(), serialize($datas));
+ $writeStream = file_put_contents($this->getCacheFile(), serialize($data));
if($writeStream === false) {
throw new \Exception('Cannot write the cache... Do you have the right permissions ?');
@@ -29,13 +30,14 @@ class FileCache implements CacheInterface {
$cacheFile = $this->getCacheFile();
clearstatcache(false, $cacheFile);
if(file_exists($cacheFile)) {
- return filemtime($cacheFile);
+ $time = filemtime($cacheFile);
+ return ($time !== false) ? $time : null;
}
- return false;
+ return null;
}
- public function purgeCache($duration){
+ public function purgeCache($seconds){
$cachePath = $this->getPath();
if(file_exists($cachePath)) {
$cacheIterator = new RecursiveIteratorIterator(
@@ -47,7 +49,7 @@ class FileCache implements CacheInterface {
if(in_array($cacheFile->getBasename(), array('.', '..', '.gitkeep')))
continue;
elseif($cacheFile->isFile()) {
- if(filemtime($cacheFile->getPathname()) < time() - $duration)
+ if(filemtime($cacheFile->getPathname()) < time() - $seconds)
unlink($cacheFile->getPathname());
}
}
@@ -55,34 +57,34 @@ class FileCache implements CacheInterface {
}
/**
- * Set cache path
+ * Set scope
* @return self
*/
- public function setPath($path){
- if(is_null($path) || !is_string($path)) {
- throw new \Exception('The given path is invalid!');
+ public function setScope($scope){
+ if(is_null($scope) || !is_string($scope)) {
+ throw new \Exception('The given scope 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);
+ $this->path = PATH_CACHE . trim($scope, " \t\n\r\0\x0B\\\/") . '/';
return $this;
}
/**
- * Set HTTP GET parameters
+ * Set key
* @return self
*/
- public function setParameters(array $param){
- $this->param = array_map('strtolower', $param);
+ public function setKey($key){
+ if (!empty($key) && is_array($key)) {
+ $key = array_map('strtolower', $key);
+ }
+ $key = json_encode($key);
+ if (!is_string($key)) {
+ throw new \Exception('The given key is invalid!');
+ }
+
+ $this->key = $key;
return $this;
}
@@ -90,9 +92,15 @@ class FileCache implements CacheInterface {
* Return cache path (and create if not exist)
* @return string Cache path
*/
- protected function getPath(){
+ private function getPath(){
if(is_null($this->path)) {
- throw new \Exception('Call "setPath" first!');
+ throw new \Exception('Call "setScope" first!');
+ }
+
+ if(!is_dir($this->path)) {
+ if (mkdir($this->path, 0755, true) !== true) {
+ throw new \Exception('Unable to create ' . $this->path);
+ }
}
return $this->path;
@@ -102,7 +110,7 @@ class FileCache implements CacheInterface {
* Get the file name use for cache store
* @return string Path to the file cache
*/
- protected function getCacheFile(){
+ private function getCacheFile(){
return $this->getPath() . $this->getCacheName();
}
@@ -110,13 +118,11 @@ class FileCache implements CacheInterface {
* Determines file name for store the cache
* return string
*/
- protected function getCacheName(){
- if(is_null($this->param)) {
- throw new \Exception('Call "setParameters" first!');
+ private function getCacheName(){
+ if(is_null($this->key)) {
+ throw new \Exception('Call "setKey" 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';
+ return hash('md5', $this->key) . '.cache';
}
}
diff --git a/caches/MemcachedCache.php b/caches/MemcachedCache.php
new file mode 100644
index 0000000..f69f10b
--- /dev/null
+++ b/caches/MemcachedCache.php
@@ -0,0 +1,115 @@
+<?php
+class MemcachedCache implements CacheInterface {
+
+ private $scope;
+ private $key;
+ private $conn;
+ private $expiration = 0;
+ private $time = false;
+ private $data = null;
+
+ public function __construct() {
+ if (!extension_loaded('memcached')) {
+ returnServerError('"memcached" extension not loaded. Please check "php.ini"');
+ }
+
+ $host = Configuration::getConfig(get_called_class(), 'host');
+ $port = Configuration::getConfig(get_called_class(), 'port');
+ if (empty($host) && empty($port)) {
+ returnServerError('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG);
+ } else if (empty($host)) {
+ returnServerError('"host" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
+ } else if (empty($port)) {
+ returnServerError('"port" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
+ } else if (!ctype_digit($port)) {
+ returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
+ }
+
+ $port = intval($port);
+
+ if ($port < 1 || $port > 65535) {
+ returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
+ }
+
+ $conn = new Memcached();
+ $conn->addServer($host, $port) or returnServerError('Could not connect to memcached server');
+ $this->conn = $conn;
+ }
+
+ public function loadData(){
+ if ($this->data) return $this->data;
+ $result = $this->conn->get($this->getCacheKey());
+ if ($result === false) {
+ return false;
+ }
+
+ $this->time = $result['time'];
+ $this->data = $result['data'];
+ return $result['data'];
+ }
+
+ public function saveData($datas){
+ $time = time();
+ $object_to_save = array(
+ 'data' => $datas,
+ 'time' => $time,
+ );
+ $result = $this->conn->set($this->getCacheKey(), $object_to_save, $this->expiration);
+
+ if($result === false) {
+ returnServerError('Cannot write the cache to memcached server');
+ }
+
+ $this->time = $time;
+
+ return $this;
+ }
+
+ public function getTime(){
+ if ($this->time === false) {
+ $this->loadData();
+ }
+ return $this->time;
+ }
+
+ public function purgeCache($duration){
+ // Note: does not purges cache right now
+ // Just sets cache expiration and leave cache purging for memcached itself
+ $this->expiration = $duration;
+ }
+
+ /**
+ * Set scope
+ * @return self
+ */
+ public function setScope($scope){
+ $this->scope = $scope;
+ return $this;
+ }
+
+ /**
+ * Set key
+ * @return self
+ */
+ public function setKey($key){
+ if (!empty($key) && is_array($key)) {
+ $key = array_map('strtolower', $key);
+ }
+ $key = json_encode($key);
+
+ if (!is_string($key)) {
+ throw new \Exception('The given key is invalid!');
+ }
+
+ $this->key = $key;
+ return $this;
+ }
+
+ private function getCacheKey(){
+ if(is_null($this->key)) {
+ returnServerError('Call "setKey" first!');
+ }
+
+ return 'rss_bridge_cache_' . hash('md5', $this->scope . $this->key . 'A');
+ }
+}
diff --git a/caches/SQLiteCache.php b/caches/SQLiteCache.php
new file mode 100644
index 0000000..394e25f
--- /dev/null
+++ b/caches/SQLiteCache.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * Cache based on SQLite 3 <https://www.sqlite.org>
+ */
+class SQLiteCache implements CacheInterface {
+ protected $scope;
+ protected $key;
+
+ private $db = null;
+
+ public function __construct() {
+ if (!extension_loaded('sqlite3')) {
+ die('"sqlite3" extension not loaded. Please check "php.ini"');
+ }
+
+ $file = Configuration::getConfig(get_called_class(), 'file');
+ if (empty($file)) {
+ die('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG);
+ }
+ if (dirname($file) == '.') {
+ $file = PATH_CACHE . $file;
+ } elseif (!is_dir(dirname($file))) {
+ die('Invalid configuration for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
+ }
+
+ if (!is_file($file)) {
+ $this->db = new SQLite3($file);
+ $this->db->enableExceptions(true);
+ $this->db->exec("CREATE TABLE storage ('key' BLOB PRIMARY KEY, 'value' BLOB, 'updated' INTEGER)");
+ } else {
+ $this->db = new SQLite3($file);
+ $this->db->enableExceptions(true);
+ }
+ $this->db->busyTimeout(5000);
+ }
+
+ public function loadData(){
+ $Qselect = $this->db->prepare('SELECT value FROM storage WHERE key = :key');
+ $Qselect->bindValue(':key', $this->getCacheKey());
+ $result = $Qselect->execute();
+ if ($result instanceof SQLite3Result) {
+ $data = $result->fetchArray(SQLITE3_ASSOC);
+ if (isset($data['value'])) {
+ return unserialize($data['value']);
+ }
+ }
+
+ return null;
+ }
+
+ public function saveData($data){
+ $Qupdate = $this->db->prepare('INSERT OR REPLACE INTO storage (key, value, updated) VALUES (:key, :value, :updated)');
+ $Qupdate->bindValue(':key', $this->getCacheKey());
+ $Qupdate->bindValue(':value', serialize($data));
+ $Qupdate->bindValue(':updated', time());
+ $Qupdate->execute();
+
+ return $this;
+ }
+
+ public function getTime(){
+ $Qselect = $this->db->prepare('SELECT updated FROM storage WHERE key = :key');
+ $Qselect->bindValue(':key', $this->getCacheKey());
+ $result = $Qselect->execute();
+ if ($result instanceof SQLite3Result) {
+ $data = $result->fetchArray(SQLITE3_ASSOC);
+ if (isset($data['updated'])) {
+ return $data['updated'];
+ }
+ }
+
+ return null;
+ }
+
+ public function purgeCache($seconds){
+ $Qdelete = $this->db->prepare('DELETE FROM storage WHERE updated < :expired');
+ $Qdelete->bindValue(':expired', time() - $seconds);
+ $Qdelete->execute();
+ }
+
+ /**
+ * Set scope
+ * @return self
+ */
+ public function setScope($scope){
+ if(is_null($scope) || !is_string($scope)) {
+ throw new \Exception('The given scope is invalid!');
+ }
+
+ $this->scope = $scope;
+ return $this;
+ }
+
+ /**
+ * Set key
+ * @return self
+ */
+ public function setKey($key){
+ if (!empty($key) && is_array($key)) {
+ $key = array_map('strtolower', $key);
+ }
+ $key = json_encode($key);
+
+ if (!is_string($key)) {
+ throw new \Exception('The given key is invalid!');
+ }
+
+ $this->key = $key;
+ return $this;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+
+ private function getCacheKey(){
+ if(is_null($this->key)) {
+ throw new \Exception('Call "setKey" first!');
+ }
+
+ return hash('sha1', $this->scope . $this->key, true);
+ }
+}
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..8748cb3
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,12 @@
+{
+ "require": {
+ "php": ">=5.6",
+ "ext-mbstring": "*",
+ "ext-sqlite3": "*",
+ "ext-curl": "*",
+ "ext-openssl": "*",
+ "ext-libxml": "*",
+ "ext-simplexml": "*",
+ "ext-json": "*"
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..3d8d9f2
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,26 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "ef341ee18f28c7bd5832e188fe157734",
+ "packages": [],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=5.6",
+ "ext-mbstring": "*",
+ "ext-sqlite3": "*",
+ "ext-curl": "*",
+ "ext-openssl": "*",
+ "ext-libxml": "*",
+ "ext-simplexml": "*",
+ "ext-json": "*"
+ },
+ "platform-dev": []
+}
diff --git a/config.default.ini.php b/config.default.ini.php
index 2d6fca1..b7d4fba 100644
--- a/config.default.ini.php
+++ b/config.default.ini.php
@@ -4,8 +4,20 @@
; 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).
+[system]
+
+; Defines the timezone used by RSS-Bridge
+; Find a list of supported timezones at
+; https://www.php.net/manual/en/timezones.php
+; timezone = "UTC" (default)
+timezone = "UTC"
+
[cache]
+; Defines the cache type used by RSS-Bridge
+; "file" = FileCache (default)
+type = "file"
+
; Allow users to specify custom timeout for specific requests.
; true = enabled
; false = disabled (default)
@@ -48,3 +60,12 @@ username = ""
; The password for authentication. Insert this password when prompted for login.
; Use a strong password to prevent others from guessing your login!
password = ""
+
+; --- Cache specific configuration ---------------------------------------------
+
+[SQLiteCache]
+file = "cache.sqlite"
+
+[MemcachedCache]
+host = "localhost"
+port = 11211
diff --git a/debian/changelog b/debian/changelog
index 4608f10..8ddfb05 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,18 @@
+rss-bridge (2019-09-12-1) unstable; urgency=medium
+
+ * New upstream release 2019-09-12
+ * debian/copyright: add entry for vendor/php-urljoin
+ * debian/rss-bridge.install: don't install LICENSE file
+ * use phpcomposer from pkg-php-tools
+ * bump dh compat to 12
+ * bump standards-version to 4.4.0 (no changes)
+ * install actions directory
+ * install whitelist.default.txt in /etc/rss-bridge/whitelist.txt
+ * debian/tests/general: adjust test for new front page
+ * change vcs-git to salsa
+
+ -- Johannes 'josch' Schauer <josch@debian.org> Wed, 25 Sep 2019 00:36:24 +0200
+
rss-bridge (2019-01-13-1) unstable; urgency=medium
* New upstream release 2019-01-13
diff --git a/debian/compat b/debian/compat
deleted file mode 100644
index b4de394..0000000
--- a/debian/compat
+++ /dev/null
@@ -1 +0,0 @@
-11
diff --git a/debian/control b/debian/control
index 41d8e74..8367cf2 100644
--- a/debian/control
+++ b/debian/control
@@ -3,16 +3,17 @@ Section: web
Priority: optional
Maintainer: Johannes 'josch' Schauer <josch@debian.org>
Homepage: https://github.com/RSS-Bridge/rss-bridge
-Vcs-Browser: https://browse.dgit.debian.org/rss-bridge.git/
-Vcs-Git: https://git.dgit.debian.org/rss-bridge
-Standards-Version: 4.1.3
-Build-Depends: debhelper (>= 11), php-cli
+Vcs-Browser: https://salsa.debian.org/debian/rss-bridge
+Vcs-Git: https://salsa.debian.org/debian/rss-bridge.git
+Standards-Version: 4.4.0
+Build-Depends: debhelper-compat (= 12), pkg-php-tools (>= 1.7~)
Rules-Requires-Root: binary-targets
Package: rss-bridge
Architecture: all
-Depends: ${misc:Depends}, php-curl, php-json, php-mbstring, php-xml
-Recommends: apache2 | nginx | httpd, libapache2-mod-php | php-fpm | php-cgi
+Depends: ${misc:Depends}, ${phpcomposer:Debian-require}, php-curl, php-json, php-mbstring, php-xml
+Recommends: ${phpcomposer:Debian-recommend}, apache2 | nginx | httpd, libapache2-mod-php | php-fpm | php-cgi
+Suggests: ${phpcomposer:Debian-suggest}
Description: web service generating ATOM feeds for websites that don't have them
Provides a PHP web service which generates ATOM feeds for facebook, twitter,
youtube, flickr, google, instagram, pinterest and more than 130 other web
diff --git a/debian/copyright b/debian/copyright
index 99c89e4..4662796 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -39,6 +39,10 @@ Copyright: S.C. Chen <me578022@gmail.com>
License: Expat
Comment: https://sourceforge.net/p/simplehtmldom/feature-requests/47/
+Files: vendor/php-urljoin/*
+Copyright: 2018 j. shagam <fluffy@beesbuzz.biz>
+License: Expat
+
License: Expat
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/debian/rss-bridge.install b/debian/rss-bridge.install
index 31b2869..72e84e9 100644
--- a/debian/rss-bridge.install
+++ b/debian/rss-bridge.install
@@ -4,7 +4,8 @@
./formats /usr/share/rss-bridge
./caches /usr/share/rss-bridge
./static /usr/share/rss-bridge
-./vendor /usr/share/rss-bridge
+./actions /usr/share/rss-bridge
+./vendor/simplehtmldom/simple_html_dom.php /usr/share/rss-bridge/vendor/simplehtmldom/
+./vendor/php-urljoin/src/urljoin.php /usr/share/rss-bridge/vendor/php-urljoin/src/
./config.default.ini.php /usr/share/rss-bridge
-debian/whitelist.txt /etc/rss-bridge
debian/config.ini.php /etc/rss-bridge
diff --git a/debian/rules b/debian/rules
index fbc17e0..27806c6 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,6 +1,6 @@
#!/usr/bin/make -f
%:
- dh $@
+ dh $@ --with phpcomposer
PHP_VERSION=$(shell phpquery -V | head -1)
@@ -10,6 +10,7 @@ override_dh_install:
mkdir -p $(CURDIR)/debian/rss-bridge/usr/share/doc/rss-bridge/examples
sed -e 's/@PHP_VERSION@/$(PHP_VERSION)/' \
< debian/nginx.conf.in > $(CURDIR)/debian/rss-bridge/usr/share/doc/rss-bridge/examples/nginx.conf
+ cp whitelist.default.txt $(CURDIR)/debian/rss-bridge/etc/rss-bridge
override_dh_fixperms:
dh_fixperms --exclude /var/cache/rss-bridge
diff --git a/debian/tests/control b/debian/tests/control
index 48f8f11..3c2fbae 100644
--- a/debian/tests/control
+++ b/debian/tests/control
@@ -1,7 +1,3 @@
Tests: general
Restrictions: allow-stderr, isolation-container, needs-root
Depends: @, curl, nginx, php-fpm
-
-Tests: whitelist
-Restrictions: allow-stderr
-Depends: @, php-cli
diff --git a/debian/tests/general b/debian/tests/general
index d56a7b7..961b6c4 100644
--- a/debian/tests/general
+++ b/debian/tests/general
@@ -8,4 +8,4 @@ ln -s ../sites-available/rss-bridge /etc/nginx/sites-enabled/rss-bridge
systemctl restart nginx
-curl --silent localhost | grep '<h1>RSS-Bridge</h1>'
+curl --silent localhost | grep '<title>RSS-Bridge</title>'
diff --git a/debian/tests/whitelist b/debian/tests/whitelist
deleted file mode 100755
index e5b8e99..0000000
--- a/debian/tests/whitelist
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/bin/sh
-
-set -exu
-
-# check whether the whitelist.txt supplied by the Debian package is the same
-# that would be generated by upstream's index.php
-#
-# we have to copy the code to a new location because otherwise we cannot remove
-# whitelist.txt and have no write access to the cache directory
-cp -a /usr/share/rss-bridge "$ADTTMP"
-rm "$ADTTMP/rss-bridge/whitelist.txt"
-rm "$ADTTMP/rss-bridge/cache"
-mkdir "$ADTTMP/rss-bridge/cache"
-php "$ADTTMP/rss-bridge/index.php"
-diff -u "$ADTTMP/rss-bridge/whitelist.txt" /usr/share/rss-bridge/whitelist.txt
diff --git a/formats/AtomFormat.php b/formats/AtomFormat.php
index bb5e30e..1159a61 100644
--- a/formats/AtomFormat.php
+++ b/formats/AtomFormat.php
@@ -1,21 +1,30 @@
<?php
/**
-* Atom
-* Documentation Source http://en.wikipedia.org/wiki/Atom_%28standard%29 and
-* http://tools.ietf.org/html/rfc4287
-*/
+ * AtomFormat - RFC 4287: The Atom Syndication Format
+ * https://tools.ietf.org/html/rfc4287
+ *
+ * Validator:
+ * https://validator.w3.org/feed/
+ */
class AtomFormat extends FormatAbstract{
+ const LIMIT_TITLE = 140;
+
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'] : '';
+ $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
+ $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
+ $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
+ $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
- $serverRequestUri = isset($_SERVER['REQUEST_URI']) ? $this->xml_encode($_SERVER['REQUEST_URI']) : '';
+ $feedUrl = $this->xml_encode($urlPrefix . $urlHost . $urlRequest);
$extraInfos = $this->getExtraInfos();
$title = $this->xml_encode($extraInfos['name']);
$uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
+ // since we can't guarantee that all items have an author,
+ // a global feed author is mandatory
+ $feedAuthor = 'RSS-Bridge';
+
$uriparts = parse_url($uri);
if(!empty($extraInfos['icon'])) {
$icon = $extraInfos['icon'];
@@ -27,11 +36,40 @@ class AtomFormat extends FormatAbstract{
$entries = '';
foreach($this->getItems() as $item) {
+ $entryTimestamp = $item->getTimestamp();
+ $entryTitle = $item->getTitle();
+ $entryContent = $item->getContent();
+ $entryUri = $item->getURI();
+ $entryID = '';
+
+ if (!empty($item->getUid()))
+ $entryID = 'urn:sha1:' . $item->getUid();
+
+ if (empty($entryID)) // Fallback to provided URI
+ $entryID = $this->xml_encode($entryUri);
+
+ if (empty($entryID)) // Fallback to title and content
+ $entryID = 'urn:sha1:' . hash('sha1', $entryTitle . $entryContent);
+
+ if (empty($entryTimestamp))
+ $entryTimestamp = $this->lastModified;
+
+ if (empty($entryTitle)) {
+ $entryTitle = str_replace("\n", ' ', strip_tags($entryContent));
+ if (strlen($entryTitle) > self::LIMIT_TITLE) {
+ $wrapPos = strpos(wordwrap($entryTitle, self::LIMIT_TITLE), "\n");
+ $entryTitle = substr($entryTitle, 0, $wrapPos) . '...';
+ }
+ }
+
+ if (empty($entryContent))
+ $entryContent = $entryTitle;
+
$entryAuthor = $this->xml_encode($item->getAuthor());
- $entryTitle = $this->xml_encode($item->getTitle());
- $entryUri = $this->xml_encode($item->getURI());
- $entryTimestamp = $this->xml_encode(date(DATE_ATOM, $item->getTimestamp()));
- $entryContent = $this->xml_encode($this->sanitizeHtml($item->getContent()));
+ $entryTitle = $this->xml_encode($entryTitle);
+ $entryUri = $this->xml_encode($entryUri);
+ $entryTimestamp = $this->xml_encode(gmdate(DATE_ATOM, $entryTimestamp));
+ $entryContent = $this->xml_encode($this->sanitizeHtml($entryContent));
$entryEnclosures = '';
foreach($item->getEnclosures() as $enclosure) {
@@ -49,16 +87,28 @@ class AtomFormat extends FormatAbstract{
. PHP_EOL;
}
+ $entryLinkAlternate = '';
+ if (!empty($entryUri)) {
+ $entryLinkAlternate = '<link rel="alternate" type="text/html" href="'
+ . $entryUri
+ . '"/>';
+ }
+
+ if (!empty($entryAuthor)) {
+ $entryAuthor = '<author><name>'
+ . $entryAuthor
+ . '</name></author>';
+ }
+
$entries .= <<<EOD
<entry>
- <author>
- <name>{$entryAuthor}</name>
- </author>
<title type="html">{$entryTitle}</title>
- <link rel="alternate" type="text/html" href="{$entryUri}" />
- <id>{$entryUri}</id>
+ <published>{$entryTimestamp}</published>
<updated>{$entryTimestamp}</updated>
+ <id>{$entryID}</id>
+ {$entryLinkAlternate}
+ {$entryAuthor}
<content type="html">{$entryContent}</content>
{$entryEnclosures}
{$entryCategories}
@@ -67,21 +117,24 @@ class AtomFormat extends FormatAbstract{
EOD;
}
- $feedTimestamp = date(DATE_ATOM, time());
- $charset = $this->getCharset();
+ $feedTimestamp = gmdate(DATE_ATOM, $this->lastModified);
+ $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">
+<feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">{$title}</title>
- <id>http{$https}://{$httpHost}{$httpInfo}/</id>
+ <id>{$feedUrl}</id>
<icon>{$icon}</icon>
<logo>{$icon}</logo>
<updated>{$feedTimestamp}</updated>
+ <author>
+ <name>{$feedAuthor}</name>
+ </author>
<link rel="alternate" type="text/html" href="{$uri}" />
- <link rel="self" href="http{$https}://{$httpHost}{$serverRequestUri}" />
+ <link rel="self" type="application/atom+xml" href="{$feedUrl}" />
{$entries}
</feed>
EOD;
diff --git a/formats/HtmlFormat.php b/formats/HtmlFormat.php
index 052bedc..ebb6b78 100644
--- a/formats/HtmlFormat.php
+++ b/formats/HtmlFormat.php
@@ -4,8 +4,21 @@ class HtmlFormat extends FormatAbstract {
$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']));
+
+ // Dynamically build buttons for all formats (except HTML)
+ $formatFac = new FormatFactory();
+ $formatFac->setWorkingDir(PATH_LIB_FORMATS);
+
+ $buttons = '';
+
+ foreach($formatFac->getFormatNames() as $format) {
+ if(strcasecmp($format, 'HTML') === 0) {
+ continue;
+ }
+
+ $query = str_replace('format=Html', 'format=' . $format, htmlentities($_SERVER['QUERY_STRING']));
+ $buttons .= $this->buildButton($format, $query) . PHP_EOL;
+ }
$entries = '';
foreach($this->getItems() as $item) {
@@ -82,18 +95,17 @@ EOD;
<html>
<head>
<meta charset="{$charset}">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{$title}</title>
<link href="static/HtmlFormat.css" rel="stylesheet">
- <link rel="alternate" type="application/atom+xml" title="Atom" href="./?{$atomquery}" />
- <link rel="alternate" type="application/rss+xml" title="RSS" href="/?{$mrssquery}" />
+ <link rel="icon" type="image/png" href="static/favicon.png">
<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>
+ {$buttons}
</div>
{$entries}
</body>
@@ -113,4 +125,10 @@ EOD;
return parent::display();
}
+
+ private function buildButton($format, $query) {
+ return <<<EOD
+<a href="./?{$query}"><button class="rss-feed">{$format}</button></a>
+EOD;
+ }
}
diff --git a/formats/JsonFormat.php b/formats/JsonFormat.php
index fafe7a5..5d09162 100644
--- a/formats/JsonFormat.php
+++ b/formats/JsonFormat.php
@@ -16,21 +16,22 @@ class JsonFormat extends FormatAbstract {
'content',
'enclosures',
'categories',
+ 'uid',
);
public function stringify(){
- $urlScheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
- $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
- $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
- $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
+ $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
+ $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
+ $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
+ $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
$extraInfos = $this->getExtraInfos();
$data = array(
- 'version' => 'https://jsonfeed.org/version/1',
- 'title' => (!empty($extraInfos['name'])) ? $extraInfos['name'] : $urlHost,
- 'home_page_url' => (!empty($extraInfos['uri'])) ? $extraInfos['uri'] : REPOSITORY,
- 'feed_url' => $urlScheme . $urlHost . $urlRequest
+ 'version' => 'https://jsonfeed.org/version/1',
+ 'title' => (!empty($extraInfos['name'])) ? $extraInfos['name'] : $urlHost,
+ 'home_page_url' => (!empty($extraInfos['uri'])) ? $extraInfos['uri'] : REPOSITORY,
+ 'feed_url' => $urlPrefix . $urlHost . $urlRequest
);
if (!empty($extraInfos['icon'])) {
@@ -42,20 +43,24 @@ class JsonFormat extends FormatAbstract {
foreach ($this->getItems() as $item) {
$entry = array();
- $entryAuthor = $item->getAuthor();
- $entryTitle = $item->getTitle();
- $entryUri = $item->getURI();
- $entryTimestamp = $item->getTimestamp();
- $entryContent = $this->sanitizeHtml($item->getContent());
- $entryEnclosures = $item->getEnclosures();
- $entryCategories = $item->getCategories();
+ $entryAuthor = $item->getAuthor();
+ $entryTitle = $item->getTitle();
+ $entryUri = $item->getURI();
+ $entryTimestamp = $item->getTimestamp();
+ $entryContent = $this->sanitizeHtml($item->getContent());
+ $entryEnclosures = $item->getEnclosures();
+ $entryCategories = $item->getCategories();
$vendorFields = $item->toArray();
foreach (self::VENDOR_EXCLUDES as $key) {
unset($vendorFields[$key]);
}
- $entry['id'] = $entryUri;
+ $entry['id'] = $item->getUid();
+
+ if (empty($entry['id'])) {
+ $entry['id'] = $entryUri;
+ }
if (!empty($entryTitle)) {
$entry['title'] = $entryTitle;
@@ -82,8 +87,8 @@ class JsonFormat extends FormatAbstract {
$entry['attachments'] = array();
foreach ($entryEnclosures as $enclosure) {
$entry['attachments'][] = array(
- 'url' => $enclosure,
- 'mime_type' => getMimeType($enclosure)
+ 'url' => $enclosure,
+ 'mime_type' => getMimeType($enclosure)
);
}
}
diff --git a/formats/MrssFormat.php b/formats/MrssFormat.php
index 34b9a92..836a361 100644
--- a/formats/MrssFormat.php
+++ b/formats/MrssFormat.php
@@ -1,18 +1,45 @@
<?php
/**
-* Mrss
-* Documentation Source http://www.rssboard.org/media-rss
-*/
+ * MrssFormat - RSS 2.0 + Media RSS
+ * http://www.rssboard.org/rss-specification
+ * http://www.rssboard.org/media-rss
+ *
+ * Validators:
+ * https://validator.w3.org/feed/
+ * http://www.rssboard.org/rss-validator/
+ *
+ * Notes about the implementation:
+ *
+ * - The item author is not supported as it needs to be an e-mail address to be
+ * valid.
+ * - The RSS specification does not explicitly allow to have more than one
+ * enclosure as every item is meant to provide one "story", thus having
+ * multiple enclosures per item may lead to unexpected behavior.
+ * On top of that, it requires to have a length specified, which RSS-Bridge
+ * can't provide.
+ * - The Media RSS extension comes in handy, since it allows to have multiple
+ * enclosures, even though they recommend to have only one enclosure because
+ * of the one-story-per-item reason. It only requires to specify the URL,
+ * everything else is optional.
+ * - Since the Media RSS extension has its own namespace, the output is a valid
+ * RSS 2.0 feed that works with feed readers that don't support the extension.
+ */
class MrssFormat extends FormatAbstract {
+ const ALLOWED_IMAGE_EXT = array(
+ '.gif', '.jpg', '.png'
+ );
+
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'] : '';
+ $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
+ $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
+ $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
+ $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
- $serverRequestUri = isset($_SERVER['REQUEST_URI']) ? $this->xml_encode($_SERVER['REQUEST_URI']) : '';
+ $feedUrl = $this->xml_encode($urlPrefix . $urlHost . $urlRequest);
$extraInfos = $this->getExtraInfos();
$title = $this->xml_encode($extraInfos['name']);
+ $icon = $extraInfos['icon'];
if(!empty($extraInfos['uri'])) {
$uri = $this->xml_encode($extraInfos['uri']);
@@ -20,34 +47,48 @@ class MrssFormat extends FormatAbstract {
$uri = REPOSITORY;
}
- $uriparts = parse_url($uri);
- $icon = $this->xml_encode($uriparts['scheme'] . '://' . $uriparts['host'] . '/favicon.ico');
-
$items = '';
foreach($this->getItems() as $item) {
- $itemAuthor = $this->xml_encode($item->getAuthor());
+ $itemTimestamp = $item->getTimestamp();
$itemTitle = $this->xml_encode($item->getTitle());
$itemUri = $this->xml_encode($item->getURI());
- $itemTimestamp = $this->xml_encode(date(DATE_RFC2822, $item->getTimestamp()));
$itemContent = $this->xml_encode($this->sanitizeHtml($item->getContent()));
+ $entryID = $item->getUid();
+ $isPermaLink = 'false';
+
+ if (empty($entryID) && !empty($itemUri)) { // Fallback to provided URI
+ $entryID = $itemUri;
+ $isPermaLink = 'true';
+ }
+
+ if (empty($entryID)) // Fallback to title and content
+ $entryID = hash('sha1', $itemTitle . $itemContent);
+
+ $entryTitle = '';
+ if (!empty($itemTitle))
+ $entryTitle = '<title>' . $itemTitle . '</title>';
+
+ $entryLink = '';
+ if (!empty($itemUri))
+ $entryLink = '<link>' . $itemUri . '</link>';
+
+ $entryPublished = '';
+ if (!empty($itemTimestamp)) {
+ $entryPublished = '<pubDate>'
+ . $this->xml_encode(gmdate(DATE_RFC2822, $itemTimestamp))
+ . '</pubDate>';
+ }
+
+ $entryDescription = '';
+ if (!empty($itemContent))
+ $entryDescription = '<description>' . $itemContent . '</description>';
- $entryEnclosuresWarning = '';
$entryEnclosures = '';
- if(!empty($item->getEnclosures())) {
- $entryEnclosures .= '<enclosure url="'
- . $this->xml_encode($item->getEnclosures()[0])
- . '" type="' . getMimeType($item->getEnclosures()[0]) . '" />';
-
- if(count($item->getEnclosures()) > 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->getEnclosures() as $enclosure) {
- $entryEnclosures .= '<atom:link rel="enclosure" href="'
- . $enclosure . '" type="' . getMimeType($enclosure) . '" />'
- . PHP_EOL;
- }
- }
+ foreach($item->getEnclosures() as $enclosure) {
+ $entryEnclosures .= '<media:content url="'
+ . $this->xml_encode($enclosure)
+ . '" type="' . getMimeType($enclosure) . '"/>'
+ . PHP_EOL;
}
$entryCategories = '';
@@ -60,12 +101,11 @@ Some media files might not be shown to you. Consider using the ATOM format inste
$items .= <<<EOD
<item>
- <title>{$itemTitle}</title>
- <link>{$itemUri}</link>
- <guid isPermaLink="true">{$itemUri}</guid>
- <pubDate>{$itemTimestamp}</pubDate>
- <description>{$itemContent}{$entryEnclosuresWarning}</description>
- <author>{$itemAuthor}</author>
+ {$entryTitle}
+ {$entryLink}
+ <guid isPermaLink="{$isPermaLink}">{$entryID}</guid>
+ {$entryPublished}
+ {$entryDescription}
{$entryEnclosures}
{$entryCategories}
</item>
@@ -75,22 +115,28 @@ EOD;
$charset = $this->getCharset();
- /* xml attributes need to have certain characters escaped to be w3c compliant */
- $imageTitle = htmlspecialchars($title, ENT_COMPAT);
+ $feedImage = '';
+ if (!empty($icon) && in_array(substr($icon, -4), self::ALLOWED_IMAGE_EXT)) {
+ $feedImage .= <<<EOD
+ <image>
+ <url>{$icon}</url>
+ <title>{$title}</title>
+ <link>{$uri}</link>
+ </image>
+EOD;
+ }
+
/* 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">
+<rss version="2.0" 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>
+ <link>{$uri}</link>
<description>{$title}</description>
- <image url="{$icon}" title="{$imageTitle}" link="{$uri}"/>
- <atom:link rel="alternate" type="text/html" href="{$uri}" />
- <atom:link rel="self" href="http{$https}://{$httpHost}{$serverRequestUri}" />
+ {$feedImage}
+ <atom:link rel="alternate" type="text/html" href="{$uri}"/>
+ <atom:link rel="self" href="{$feedUrl}" type="application/atom+xml"/>
{$items}
</channel>
</rss>
diff --git a/index.php b/index.php
index a95302a..666b9e4 100644
--- a/index.php
+++ b/index.php
@@ -6,8 +6,6 @@ Configuration::loadConfiguration();
Authentication::showPromptIfNeeded();
-date_default_timezone_set('UTC');
-
/*
Move the CLI arguments to the $_GET array, in order to be able to use
rss-bridge from the command line
@@ -29,309 +27,17 @@ define('USER_AGENT',
ini_set('user_agent', USER_AGENT);
-// default whitelist
-$whitelist_default = array(
- 'BandcampBridge',
- 'CryptomeBridge',
- 'DansTonChatBridge',
- 'DuckDuckGoBridge',
- 'FacebookBridge',
- 'FlickrBridge',
- 'GooglePlusPostBridge',
- 'GoogleSearchBridge',
- 'IdenticaBridge',
- 'InstagramBridge',
- 'OpenClassroomsBridge',
- 'PinterestBridge',
- 'ScmbBridge',
- 'TwitterBridge',
- 'WikipediaBridge',
- 'YoutubeBridge');
-
try {
- Bridge::setWhitelist($whitelist_default);
-
- $showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
- $action = array_key_exists('action', $params) ? $params['action'] : null;
- $bridge = array_key_exists('bridge', $params) ? $params['bridge'] : null;
-
- // Return list of bridges as JSON formatted text
- if($action === 'list') {
-
- $list = new StdClass();
- $list->bridges = array();
- $list->total = 0;
-
- foreach(Bridge::getBridgeNames() as $bridgeName) {
-
- $bridge = Bridge::create($bridgeName);
-
- if($bridge === false) { // Broken bridge, show as inactive
-
- $list->bridges[$bridgeName] = array(
- 'status' => 'inactive'
- );
-
- continue;
-
- }
-
- $status = Bridge::isWhitelisted($bridgeName) ? 'active' : 'inactive';
-
- $list->bridges[$bridgeName] = array(
- 'status' => $status,
- 'uri' => $bridge->getURI(),
- 'name' => $bridge->getName(),
- 'icon' => $bridge->getIcon(),
- 'parameters' => $bridge->getParameters(),
- 'maintainer' => $bridge->getMaintainer(),
- 'description' => $bridge->getDescription()
- );
-
- }
-
- $list->total = count($list->bridges);
-
- header('Content-Type: application/json');
- echo json_encode($list, JSON_PRETTY_PRINT);
-
- } elseif($action === 'detect') {
-
- $targetURL = $params['url']
- or returnClientError('You must specify a url!');
-
- $format = $params['format']
- or returnClientError('You must specify a format!');
-
- foreach(Bridge::getBridgeNames() as $bridgeName) {
-
- if(!Bridge::isWhitelisted($bridgeName)) {
- continue;
- }
-
- $bridge = Bridge::create($bridgeName);
-
- if($bridge === false) {
- continue;
- }
-
- $bridgeParams = $bridge->detectParameters($targetURL);
-
- if(is_null($bridgeParams)) {
- continue;
- }
-
- $bridgeParams['bridge'] = $bridgeName;
- $bridgeParams['format'] = $format;
-
- header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
- die();
-
- }
-
- returnClientError('No bridge found for given URL: ' . $targetURL);
-
- } elseif($action === 'display' && !empty($bridge)) {
-
- $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($bridge)) {
- throw new \Exception('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);
- }
-
- // Cache timeout
- $cache_timeout = -1;
- if(array_key_exists('_cache_timeout', $params)) {
-
- if(!CUSTOM_CACHE_TIMEOUT) {
- unset($params['_cache_timeout']);
- $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($params);
- header('Location: ' . $uri, true, 301);
- die();
- }
-
- $cache_timeout = filter_var($params['_cache_timeout'], FILTER_VALIDATE_INT);
-
- } else {
- $cache_timeout = $bridge->getCacheTimeout();
- }
-
- // Remove parameters that don't concern bridges
- $bridge_params = array_diff_key(
- $params,
- array_fill_keys(
- array(
- 'action',
- 'bridge',
- 'format',
- '_noproxy',
- '_cache_timeout',
- '_error_time'
- ), '')
- );
-
- // Remove parameters that don't concern caches
- $cache_params = array_diff_key(
- $params,
- array_fill_keys(
- array(
- 'action',
- 'format',
- '_noproxy',
- '_cache_timeout',
- '_error_time'
- ), '')
- );
-
- // Initialize cache
- $cache = Cache::create('FileCache');
- $cache->setPath(PATH_CACHE);
- $cache->purgeCache(86400); // 24 hours
- $cache->setParameters($cache_params);
-
- $items = array();
- $infos = array();
- $mtime = $cache->getTime();
-
- if($mtime !== false
- && (time() - $cache_timeout < $mtime)
- && !Debug::isEnabled()) { // Load cached data
-
- // Send "Not Modified" response if client supports it
- // Implementation based on https://stackoverflow.com/a/10847262
- if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
- $stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
-
- if($mtime <= $stime) { // Cached data is older or same
- header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304);
- die();
- }
- }
-
- $cached = $cache->loadData();
-
- if(isset($cached['items']) && isset($cached['extraInfos'])) {
- foreach($cached['items'] as $item) {
- $items[] = new \FeedItem($item);
- }
-
- $infos = $cached['extraInfos'];
- }
-
- } else { // Collect new data
-
- try {
- $bridge->setDatas($bridge_params);
- $bridge->collectData();
-
- $items = $bridge->getItems();
-
- // Transform "legacy" items to FeedItems if necessary.
- // Remove this code when support for "legacy" items ends!
- if(isset($items[0]) && is_array($items[0])) {
- $feedItems = array();
-
- foreach($items as $item) {
- $feedItems[] = new \FeedItem($item);
- }
-
- $items = $feedItems;
- }
-
- $infos = array(
- 'name' => $bridge->getName(),
- 'uri' => $bridge->getURI(),
- 'icon' => $bridge->getIcon()
- );
- } catch(Error $e) {
- error_log($e);
-
- $item = new \FeedItem();
-
- // Create "new" error message every 24 hours
- $params['_error_time'] = urlencode((int)(time() / 86400));
-
- // Error 0 is a special case (i.e. "trying to get property of non-object")
- if($e->getCode() === 0) {
- $item->setTitle('Bridge encountered an unexpected situation! (' . $params['_error_time'] . ')');
- } else {
- $item->setTitle('Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')');
- }
-
- $item->setURI(
- (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
- . '?'
- . http_build_query($params)
- );
-
- $item->setTimestamp(time());
- $item->setContent(buildBridgeException($e, $bridge));
-
- $items[] = $item;
- } catch(Exception $e) {
- error_log($e);
-
- $item = new \FeedItem();
-
- // Create "new" error message every 24 hours
- $params['_error_time'] = urlencode((int)(time() / 86400));
-
- $item->setURI(
- (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
- . '?'
- . http_build_query($params)
- );
-
- $item->setTitle('Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')');
- $item->setTimestamp(time());
- $item->setContent(buildBridgeException($e, $bridge));
-
- $items[] = $item;
- }
-
- // Store data in cache
- $cache->saveData(array(
- 'items' => array_map(function($i){ return $i->toArray(); }, $items),
- 'extraInfos' => $infos
- ));
-
- }
+ $actionFac = new \ActionFactory();
+ $actionFac->setWorkingDir(PATH_LIB_ACTIONS);
- // Data transformation
- try {
- $format = Format::create($format);
- $format->setItems($items);
- $format->setExtraInfos($infos);
- $format->setLastModified($cache->getTime());
- $format->display();
- } catch(Error $e) {
- error_log($e);
- header('Content-Type: text/html', true, $e->getCode());
- die(buildTransformException($e, $bridge));
- } catch(Exception $e) {
- error_log($e);
- header('Content-Type: text/html', true, $e->getCode());
- die(buildTransformException($e, $bridge));
- }
+ if(array_key_exists('action', $params)) {
+ $action = $actionFac->create($params['action']);
+ $action->setUserData($params);
+ $action->execute();
} else {
+ $showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
echo BridgeList::create($showInactive);
}
} catch(\Exception $e) {
diff --git a/lib/ActionAbstract.php b/lib/ActionAbstract.php
new file mode 100644
index 0000000..b925d60
--- /dev/null
+++ b/lib/ActionAbstract.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * An abstract class for action objects
+ */
+abstract class ActionAbstract implements ActionInterface {
+ /**
+ * Holds the user data.
+ *
+ * @var array
+ */
+ protected $userData = null;
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param array $userData {@inheritdoc}
+ */
+ public function setUserData($userData) {
+ $this->userData = $userData;
+ }
+}
diff --git a/lib/ActionFactory.php b/lib/ActionFactory.php
new file mode 100644
index 0000000..8146e54
--- /dev/null
+++ b/lib/ActionFactory.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * Factory for action objects.
+ */
+class ActionFactory extends FactoryAbstract {
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $name {@inheritdoc}
+ */
+ public function create($name) {
+ $filePath = $this->buildFilePath($name);
+
+ if(!file_exists($filePath)) {
+ throw new \Exception('File ' . $filePath . ' does not exist!');
+ }
+
+ require_once $filePath;
+
+ $class = $this->buildClassName($name);
+
+ if((new \ReflectionClass($class))->isInstantiable()) {
+ return new $class();
+ }
+
+ return false;
+ }
+
+ /**
+ * Build class name from action name
+ *
+ * The class name consists of the action name with prefix "Action". The first
+ * character of the class name must be uppercase.
+ *
+ * Example: 'display' => 'DisplayAction'
+ *
+ * @param string $name The action name.
+ * @return string The class name.
+ */
+ protected function buildClassName($name) {
+ return ucfirst(strtolower($name)) . 'Action';
+ }
+
+ /**
+ * Build file path to the action class.
+ *
+ * @param string $name The action name.
+ * @return string Path to the action class.
+ */
+ protected function buildFilePath($name) {
+ return $this->getWorkingDir() . $this->buildClassName($name) . '.php';
+ }
+}
diff --git a/lib/ActionInterface.php b/lib/ActionInterface.php
new file mode 100644
index 0000000..c38d057
--- /dev/null
+++ b/lib/ActionInterface.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * Interface for action objects.
+ */
+interface ActionInterface {
+ /**
+ * Set user data for the action to consume.
+ *
+ * @param array $userData An associative array of user data.
+ * @return void
+ */
+ function setUserData($userData);
+
+ /**
+ * Execute the action.
+ *
+ * Note: This function directly outputs data to the user.
+ *
+ * @return void
+ */
+ function execute();
+}
diff --git a/lib/BridgeAbstract.php b/lib/BridgeAbstract.php
index 13215a4..b4eb9ff 100644
--- a/lib/BridgeAbstract.php
+++ b/lib/BridgeAbstract.php
@@ -194,6 +194,11 @@ abstract class BridgeAbstract implements BridgeInterface {
*/
public function setDatas(array $inputs){
+ if(isset($inputs['context'])) { // Context hinting (optional)
+ $this->queriedContext = $inputs['context'];
+ unset($inputs['context']);
+ }
+
if(empty(static::PARAMETERS)) {
if(!empty($inputs)) {
@@ -218,8 +223,11 @@ abstract class BridgeAbstract implements BridgeInterface {
);
}
- // Guess the paramter context from input data
- $this->queriedContext = $validator->getQueriedContext($inputs, static::PARAMETERS);
+ // Guess the context from input data
+ if(empty($this->queriedContext)) {
+ $this->queriedContext = $validator->getQueriedContext($inputs, static::PARAMETERS);
+ }
+
if(is_null($this->queriedContext)) {
returnClientError('Required parameter(s) missing');
} elseif($this->queriedContext === false) {
diff --git a/lib/BridgeCard.php b/lib/BridgeCard.php
index a3493b7..c6f3822 100644
--- a/lib/BridgeCard.php
+++ b/lib/BridgeCard.php
@@ -48,13 +48,19 @@ final class BridgeCard {
* @param bool $isHttps If disabled, adds a warning to the form
* @return string The form header
*/
- private static function getFormHeader($bridgeName, $isHttps = false) {
+ private static function getFormHeader($bridgeName, $isHttps = false, $parameterName = '') {
$form = <<<EOD
<form method="GET" action="?">
<input type="hidden" name="action" value="display" />
<input type="hidden" name="bridge" value="{$bridgeName}" />
EOD;
+ if(!empty($parameterName)) {
+ $form .= <<<EOD
+ <input type="hidden" name="context" value="{$parameterName}" />
+EOD;
+ }
+
if(!$isHttps) {
$form .= '<div class="secure-warning">Warning :
This bridge is not fetching its content through a secure connection</div>';
@@ -80,7 +86,7 @@ This bridge is not fetching its content through a secure connection</div>';
$isHttps = false,
$parameterName = '',
$parameters = array()) {
- $form = self::getFormHeader($bridgeName, $isHttps);
+ $form = self::getFormHeader($bridgeName, $isHttps, $parameterName);
if(count($parameters) > 0) {
@@ -207,6 +213,11 @@ This bridge is not fetching its content through a secure connection</div>';
* @return string The list input field
*/
private static function getListInput($entry, $id, $name) {
+ if(isset($entry['required']) && $entry['required'] === true) {
+ Debug::log('The "required" attribute is not supported for lists.');
+ unset($entry['required']);
+ }
+
$list = '<select '
. self::getInputAttributes($entry)
. ' id="'
@@ -267,6 +278,11 @@ This bridge is not fetching its content through a secure connection</div>';
* @return string The checkbox input field
*/
private static function getCheckboxInput($entry, $id, $name) {
+ if(isset($entry['required']) && $entry['required'] === true) {
+ Debug::log('The "required" attribute is not supported for checkboxes.');
+ unset($entry['required']);
+ }
+
return '<input '
. self::getInputAttributes($entry)
. ' id="'
@@ -289,7 +305,10 @@ This bridge is not fetching its content through a secure connection</div>';
*/
static function displayBridgeCard($bridgeName, $formats, $isActive = true){
- $bridge = Bridge::create($bridgeName);
+ $bridgeFac = new \BridgeFactory();
+ $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
+
+ $bridge = $bridgeFac->create($bridgeName);
if($bridge == false)
return '';
diff --git a/lib/Bridge.php b/lib/BridgeFactory.php
index c9561c8..fea254f 100644
--- a/lib/Bridge.php
+++ b/lib/BridgeFactory.php
@@ -35,17 +35,7 @@
* $bridge = Bridge::create('GitHubIssue');
* ```
*/
-class Bridge {
-
- /**
- * Holds a path to the working directory.
- *
- * Do not access this property directly!
- * Use {@see Bridge::setWorkingDir()} and {@see Bridge::getWorkingDir()} instead.
- *
- * @var string|null
- */
- protected static $workingDir = null;
+class BridgeFactory extends FactoryAbstract {
/**
* Holds a list of whitelisted bridges.
@@ -55,18 +45,7 @@ class Bridge {
*
* @var array
*/
- protected static $whitelist = array();
-
- /**
- * Throws an exception when trying to create a new instance of this class.
- * Use {@see Bridge::create()} to instanciate a new bridge from the working
- * directory.
- *
- * @throws \LogicException if called.
- */
- public function __construct(){
- throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create bridge objects!');
- }
+ protected $whitelist = array();
/**
* Creates a new bridge object from the working directory.
@@ -77,13 +56,13 @@ class Bridge {
* @param string $name Name of the bridge object.
* @return object|bool The bridge object or false if the class is not instantiable.
*/
- public static function create($name){
- if(!self::isBridgeName($name)) {
+ public function create($name){
+ if(!$this->isBridgeName($name)) {
throw new \InvalidArgumentException('Bridge name invalid!');
}
- $name = self::sanitizeBridgeName($name) . 'Bridge';
- $filePath = self::getWorkingDir() . $name . '.php';
+ $name = $this->sanitizeBridgeName($name) . 'Bridge';
+ $filePath = $this->getWorkingDir() . $name . '.php';
if(!file_exists($filePath)) {
throw new \Exception('Bridge file ' . $filePath . ' does not exist!');
@@ -99,48 +78,6 @@ class Bridge {
}
/**
- * Sets the working directory.
- *
- * @param string $dir Path to the directory containing bridges.
- * @throws \LogicException if the provided path is not a valid string.
- * @throws \Exception if the provided path does not exist.
- * @throws \InvalidArgumentException if $dir is not a directory.
- * @return void
- */
- public static function setWorkingDir($dir){
- self::$workingDir = null;
-
- if(!is_string($dir)) {
- throw new \InvalidArgumentException('Working directory is not a valid string!');
- }
-
- if(!file_exists($dir)) {
- throw new \Exception('Working directory does not exist!');
- }
-
- if(!is_dir($dir)) {
- throw new \InvalidArgumentException('Working directory is not a directory!');
- }
-
- self::$workingDir = realpath($dir) . '/';
- }
-
- /**
- * Returns the working directory.
- * The working directory must be specified with {@see Bridge::setWorkingDir()}!
- *
- * @throws \LogicException if the working directory is not set.
- * @return string The current working directory.
- */
- public static function getWorkingDir(){
- if(is_null(self::$workingDir)) {
- throw new \LogicException('Working directory is not set!');
- }
-
- return self::$workingDir;
- }
-
- /**
* Returns true if the provided name is a valid bridge name.
*
* A valid bridge name starts with a capital letter ([A-Z]), followed by
@@ -149,7 +86,7 @@ class Bridge {
* @param string $name The bridge name.
* @return bool true if the name is a valid bridge name, false otherwise.
*/
- public static function isBridgeName($name){
+ public function isBridgeName($name){
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
@@ -160,12 +97,12 @@ class Bridge {
*
* @return array List of bridge names
*/
- public static function getBridgeNames(){
+ public function getBridgeNames(){
static $bridgeNames = array(); // Initialized on first call
if(empty($bridgeNames)) {
- $files = scandir(self::getWorkingDir());
+ $files = scandir($this->getWorkingDir());
if($files !== false) {
foreach($files as $file) {
@@ -185,14 +122,15 @@ class Bridge {
* @param string $name Name of the bridge.
* @return bool True if the bridge is whitelisted.
*/
- public static function isWhitelisted($name){
- return in_array(self::sanitizeBridgeName($name), self::getWhitelist());
+ public function isWhitelisted($name){
+ return in_array($this->sanitizeBridgeName($name), $this->getWhitelist());
}
/**
* Returns the whitelist.
*
- * On first call this function reads the whitelist from {@see WHITELIST}.
+ * On first call this function reads the whitelist from {@see WHITELIST} if
+ * the file exists, {@see WHITELIST_DEFAULT} otherwise.
* * Each line in the file specifies one bridge on the whitelist.
* * An empty file disables all bridges.
* * If the file only only contains `*`, all bridges are whitelisted.
@@ -204,30 +142,32 @@ class Bridge {
*
* @return array Array of whitelisted bridges
*/
- public static function getWhitelist() {
+ public function getWhitelist() {
static $firstCall = true; // Initialized on first call
if($firstCall) {
- // Create initial whitelist or load from disk
- if (!file_exists(WHITELIST) && !empty(self::$whitelist)) {
- file_put_contents(WHITELIST, implode("\n", self::$whitelist));
- } else {
-
+ if(file_exists(WHITELIST)) {
$contents = trim(file_get_contents(WHITELIST));
+ } elseif(file_exists(WHITELIST_DEFAULT)) {
+ $contents = trim(file_get_contents(WHITELIST_DEFAULT));
+ } else {
+ $contents = '';
+ }
- if($contents === '*') { // Whitelist all bridges
- self::$whitelist = self::getBridgeNames();
- } else {
- self::$whitelist = array_map('self::sanitizeBridgeName', explode("\n", $contents));
+ if($contents === '*') { // Whitelist all bridges
+ $this->whitelist = $this->getBridgeNames();
+ } else {
+ //$this->$whitelist = array_map('$this->sanitizeBridgeName', explode("\n", $contents));
+ foreach(explode("\n", $contents) as $bridgeName) {
+ $this->whitelist[] = $this->sanitizeBridgeName($bridgeName);
}
-
}
}
- return self::$whitelist;
+ return $this->whitelist;
}
@@ -245,8 +185,8 @@ class Bridge {
* @param array $default The whitelist as array of bridge names.
* @return void
*/
- public static function setWhitelist($default = array()) {
- self::$whitelist = array_map('self::sanitizeBridgeName', $default);
+ public function setWhitelist($default = array()) {
+ $this->whitelist = array_map('$this->sanitizeBridgeName', $default);
}
/**
@@ -266,7 +206,7 @@ class Bridge {
* @return string|null The sanitized bridge name if the provided name is
* valid, null otherwise.
*/
- protected static function sanitizeBridgeName($name) {
+ protected function sanitizeBridgeName($name) {
if(is_string($name)) {
@@ -280,10 +220,16 @@ class Bridge {
$name = $matches[1];
}
+ // Improve performance for correctly written bridge names
+ if(in_array($name, $this->getBridgeNames())) {
+ $index = array_search($name, $this->getBridgeNames());
+ return $this->getBridgeNames()[$index];
+ }
+
// The name is valid if a corresponding bridge file is found on disk
- if(in_array(strtolower($name), array_map('strtolower', self::getBridgeNames()))) {
- $index = array_search(strtolower($name), array_map('strtolower', self::getBridgeNames()));
- return self::getBridgeNames()[$index];
+ if(in_array(strtolower($name), array_map('strtolower', $this->getBridgeNames()))) {
+ $index = array_search(strtolower($name), array_map('strtolower', $this->getBridgeNames()));
+ return $this->getBridgeNames()[$index];
}
Debug::log('Invalid bridge name specified: "' . $name . '"!');
diff --git a/lib/BridgeList.php b/lib/BridgeList.php
index d79d72f..dc545de 100644
--- a/lib/BridgeList.php
+++ b/lib/BridgeList.php
@@ -33,6 +33,7 @@ final class BridgeList {
<meta name="description" content="RSS-Bridge" />
<title>RSS-Bridge</title>
<link href="static/style.css" rel="stylesheet">
+ <link rel="icon" type="image/png" href="static/favicon.png">
<script src="static/search.js"></script>
<script src="static/select.js"></script>
<noscript>
@@ -61,14 +62,19 @@ EOD;
$totalActiveBridges = 0;
$inactiveBridges = '';
- $bridgeList = Bridge::getBridgeNames();
- $formats = Format::getFormatNames();
+ $bridgeFac = new \BridgeFactory();
+ $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
+ $bridgeList = $bridgeFac->getBridgeNames();
+
+ $formatFac = new FormatFactory();
+ $formatFac->setWorkingDir(PATH_LIB_FORMATS);
+ $formats = $formatFac->getFormatNames();
$totalBridges = count($bridgeList);
foreach($bridgeList as $bridgeName) {
- if(Bridge::isWhitelisted($bridgeName)) {
+ if($bridgeFac->isWhitelisted($bridgeName)) {
$body .= BridgeCard::displayBridgeCard($bridgeName, $formats);
$totalActiveBridges++;
@@ -111,8 +117,7 @@ EOD;
return <<<EOD
<header>
- <h1>RSS-Bridge</h1>
- <h2>Reconnecting the Web</h2>
+ <div class="logo"></div>
{$warning}
</header>
EOD;
@@ -130,7 +135,7 @@ EOD;
<section class="searchbar">
<h3>Search</h3>
<input type="text" name="searchfield"
- id="searchfield" placeholder="Enter the bridge you want to search for"
+ id="searchfield" placeholder="Insert URL or bridge name"
onchange="search()" onkeyup="search()" value="{$query}">
</section>
EOD;
diff --git a/lib/Cache.php b/lib/CacheFactory.php
index a0d2ac7..9ce5c19 100644
--- a/lib/Cache.php
+++ b/lib/CacheFactory.php
@@ -31,29 +31,7 @@
* $cache = Cache::create('FileCache');
* ```
*/
-class Cache {
-
- /**
- * Holds a path to the working directory.
- *
- * Do not access this property directly!
- * Use {@see Cache::setWorkingDir()} and {@see Cache::getWorkingDir()} instead.
- *
- * @var string|null
- */
- protected static $workingDir = null;
-
- /**
- * Throws an exception when trying to create a new instance of this class.
- * Use {@see Cache::create()} to create a new cache object from the working
- * directory.
- *
- * @throws \LogicException if called.
- */
- public function __construct(){
- throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create cache objects!');
- }
-
+class CacheFactory extends FactoryAbstract {
/**
* Creates a new cache object from the working directory.
*
@@ -63,12 +41,14 @@ class Cache {
* @param string $name Name of the cache object.
* @return object|bool The cache object or false if the class is not instantiable.
*/
- public static function create($name){
- if(!self::isCacheName($name)) {
+ public function create($name){
+ $name = $this->sanitizeCacheName($name) . 'Cache';
+
+ if(!$this->isCacheName($name)) {
throw new \InvalidArgumentException('Cache name invalid!');
}
- $filePath = self::getWorkingDir() . $name . '.php';
+ $filePath = $this->getWorkingDir() . $name . '.php';
if(!file_exists($filePath)) {
throw new \Exception('Cache file ' . $filePath . ' does not exist!');
@@ -84,57 +64,86 @@ class Cache {
}
/**
- * Sets the working directory.
+ * Returns true if the provided name is a valid cache name.
+ *
+ * A valid cache name starts with a capital letter ([A-Z]), followed by
+ * zero or more alphanumeric characters or hyphen ([A-Za-z0-9-]).
*
- * @param string $dir Path to a directory containing cache classes
- * @throws \InvalidArgumentException if $dir is not a string.
- * @throws \Exception if the working directory doesn't exist.
- * @throws \InvalidArgumentException if $dir is not a directory.
- * @return void
+ * @param string $name The cache name.
+ * @return bool true if the name is a valid cache name, false otherwise.
*/
- public static function setWorkingDir($dir){
- self::$workingDir = null;
-
- if(!is_string($dir)) {
- throw new \InvalidArgumentException('Working directory is not a valid string!');
- }
-
- if(!file_exists($dir)) {
- throw new \Exception('Working directory does not exist!');
- }
-
- if(!is_dir($dir)) {
- throw new \InvalidArgumentException('Working directory is not a directory!');
- }
-
- self::$workingDir = realpath($dir) . '/';
+ public function isCacheName($name){
+ return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
/**
- * Returns the working directory.
- * The working directory must be set with {@see Cache::setWorkingDir()}!
+ * Returns a list of cache names from the working directory.
+ *
+ * The list is cached internally to allow for successive calls.
*
- * @throws \LogicException if the working directory is not set.
- * @return string The current working directory.
+ * @return array List of cache names
*/
- public static function getWorkingDir(){
- if(is_null(self::$workingDir)) {
- throw new \LogicException('Working directory is not set!');
+ public function getCacheNames(){
+
+ static $cacheNames = array(); // Initialized on first call
+
+ if(empty($cacheNames)) {
+ $files = scandir($this->getWorkingDir());
+
+ if($files !== false) {
+ foreach($files as $file) {
+ if(preg_match('/^([^.]+)Cache\.php$/U', $file, $out)) {
+ $cacheNames[] = $out[1];
+ }
+ }
+ }
}
- return self::$workingDir;
+ return $cacheNames;
}
/**
- * Returns true if the provided name is a valid cache name.
+ * Returns the sanitized cache name.
*
- * A valid cache name starts with a capital letter ([A-Z]), followed by
- * zero or more alphanumeric characters or hyphen ([A-Za-z0-9-]).
+ * The cache name can be specified in various ways:
+ * * The PHP file name (i.e. `FileCache.php`)
+ * * The PHP file name without file extension (i.e. `FileCache`)
+ * * The cache name (i.e. `file`)
*
- * @param string $name The cache name.
- * @return bool true if the name is a valid cache name, false otherwise.
+ * Casing is ignored (i.e. `FILE` and `fIlE` are the same).
+ *
+ * A cache file matching the given cache name must exist in the working
+ * directory!
+ *
+ * @param string $name The cache name
+ * @return string|null The sanitized cache name if the provided name is
+ * valid, null otherwise.
*/
- public static function isCacheName($name){
- return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
+ protected function sanitizeCacheName($name) {
+
+ if(is_string($name)) {
+
+ // Trim trailing '.php' if exists
+ if(preg_match('/(.+)(?:\.php)/', $name, $matches)) {
+ $name = $matches[1];
+ }
+
+ // Trim trailing 'Cache' if exists
+ if(preg_match('/(.+)(?:Cache)$/i', $name, $matches)) {
+ $name = $matches[1];
+ }
+
+ // The name is valid if a corresponding file is found on disk
+ if(in_array(strtolower($name), array_map('strtolower', $this->getCacheNames()))) {
+ $index = array_search(strtolower($name), array_map('strtolower', $this->getCacheNames()));
+ return $this->getCacheNames()[$index];
+ }
+
+ Debug::log('Invalid cache name specified: "' . $name . '"!');
+
+ }
+
+ return null; // Bad parameter
+
}
}
diff --git a/lib/CacheInterface.php b/lib/CacheInterface.php
index a74fc0d..091c5f0 100644
--- a/lib/CacheInterface.php
+++ b/lib/CacheInterface.php
@@ -13,38 +13,54 @@
/**
* The cache interface
- *
- * @todo Add missing function to the interface
- * @todo Explain parameters and return values in more detail
- * @todo Return self more often (to allow call chaining)
*/
interface CacheInterface {
/**
+ * Set scope of the current cache
+ *
+ * If $scope is an empty string, the cache is set to a global context.
+ *
+ * @param string $scope The scope the data is related to
+ */
+ public function setScope($scope);
+
+ /**
+ * Set key to assign the current data
+ *
+ * Since $key can be anything, the cache implementation must ensure to
+ * assign the related data reliably; most commonly by serializing and
+ * hashing the key in an appropriate way.
+ *
+ * @param array $key The key the data is related to
+ */
+ public function setKey($key);
+
+ /**
* Loads data from cache
*
- * @return mixed The cache data
+ * @return mixed The cached data or null
*/
public function loadData();
/**
* Stores data to the cache
*
- * @param mixed $datas The data to store
+ * @param mixed $data The data to store
* @return self The cache object
*/
- public function saveData($datas);
+ public function saveData($data);
/**
- * Returns the timestamp for the curent cache file
+ * Returns the timestamp for the curent cache data
*
- * @return int Timestamp
+ * @return int Timestamp or null
*/
public function getTime();
/**
- * Removes any data that is older than the specified duration from cache
+ * Removes any data that is older than the specified age from cache
*
- * @param int $duration The cache duration in seconds
+ * @param int $seconds The cache age in seconds
*/
- public function purgeCache($duration);
+ public function purgeCache($seconds);
}
diff --git a/lib/Configuration.php b/lib/Configuration.php
index 64d9dde..fc575d6 100644
--- a/lib/Configuration.php
+++ b/lib/Configuration.php
@@ -28,7 +28,7 @@ final class Configuration {
*
* @todo Replace this property by a constant.
*/
- public static $VERSION = '2019-01-13';
+ public static $VERSION = '2019-09-12';
/**
* Holds the configuration data.
@@ -80,35 +80,31 @@ final class Configuration {
// Check PHP version
if(version_compare(PHP_VERSION, '5.6.0') === -1)
- die('RSS-Bridge requires at least PHP version 5.6.0!');
+ self::reportError('RSS-Bridge requires at least PHP version 5.6.0!');
// extensions check
if(!extension_loaded('openssl'))
- die('"openssl" extension not loaded. Please check "php.ini"');
+ self::reportError('"openssl" extension not loaded. Please check "php.ini"');
if(!extension_loaded('libxml'))
- die('"libxml" extension not loaded. Please check "php.ini"');
+ self::reportError('"libxml" extension not loaded. Please check "php.ini"');
if(!extension_loaded('mbstring'))
- die('"mbstring" extension not loaded. Please check "php.ini"');
+ self::reportError('"mbstring" extension not loaded. Please check "php.ini"');
if(!extension_loaded('simplexml'))
- die('"simplexml" extension not loaded. Please check "php.ini"');
+ self::reportError('"simplexml" extension not loaded. Please check "php.ini"');
// Allow RSS-Bridge to run without curl module in CLI mode without root certificates
if(!extension_loaded('curl') && !(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo'))))
- die('"curl" extension not loaded. Please check "php.ini"');
+ self::reportError('"curl" extension not loaded. Please check "php.ini"');
if(!extension_loaded('json'))
- die('"json" extension not loaded. Please check "php.ini"');
+ self::reportError('"json" extension not loaded. Please check "php.ini"');
// Check cache folder permissions (write permissions required)
if(!is_writable(PATH_CACHE))
- die('RSS-Bridge does not have write permissions for ' . PATH_CACHE . '!');
-
- // Check whitelist file permissions
- if(!file_exists(WHITELIST) && !is_writable(dirname(WHITELIST)))
- die('RSS-Bridge does not have write permissions for ' . WHITELIST . '!');
+ self::reportError('RSS-Bridge does not have write permissions for ' . PATH_CACHE . '!');
}
@@ -118,15 +114,13 @@ final class Configuration {
* Returns an error message and aborts execution if the configuration is invalid.
*
* The RSS-Bridge configuration is split into two files:
- * - `config.default.ini.php`: The default configuration file that ships with
- * every release of RSS-Bridge (do not modify this file!).
- * - `config.ini.php`: The local configuration file that can be modified by
- * server administrators.
- *
- * The files must be located at {@see PATH_ROOT}
+ * - {@see FILE_CONFIG_DEFAULT} The default configuration file that ships
+ * with every release of RSS-Bridge (do not modify this file!).
+ * - {@see FILE_CONFIG} The local configuration file that can be modified
+ * by server administrators.
*
- * RSS-Bridge will first load `config.default.ini.php` into memory and then
- * replace parameters with the contents of `config.ini.php`. That way new
+ * RSS-Bridge will first load {@see FILE_CONFIG_DEFAULT} into memory and then
+ * replace parameters with the contents of {@see FILE_CONFIG}. That way new
* parameters are automatically initialized with default values and custom
* configurations can be reduced to the minimum set of parametes necessary
* (only the ones that changed).
@@ -140,16 +134,16 @@ final class Configuration {
*/
public static function loadConfiguration() {
- if(!file_exists(PATH_ROOT . 'config.default.ini.php'))
- die('The default configuration file "config.default.ini.php" is missing!');
+ if(!file_exists(FILE_CONFIG_DEFAULT))
+ self::reportError('The default configuration file is missing at ' . FILE_CONFIG_DEFAULT);
- Configuration::$config = parse_ini_file(PATH_ROOT . 'config.default.ini.php', true, INI_SCANNER_TYPED);
+ Configuration::$config = parse_ini_file(FILE_CONFIG_DEFAULT, true, INI_SCANNER_TYPED);
if(!Configuration::$config)
- die('Error parsing config.default.ini.php');
+ self::reportError('Error parsing ' . FILE_CONFIG_DEFAULT);
- if(file_exists(PATH_ROOT . 'config.ini.php')) {
+ if(file_exists(FILE_CONFIG)) {
// Replace default configuration with custom settings
- foreach(parse_ini_file(PATH_ROOT . 'config.ini.php', true, INI_SCANNER_TYPED) as $header => $section) {
+ foreach(parse_ini_file(FILE_CONFIG, 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])) {
@@ -159,8 +153,14 @@ final class Configuration {
}
}
+ if(!is_string(self::getConfig('system', 'timezone'))
+ || !in_array(self::getConfig('system', 'timezone'), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
+ self::reportConfigurationError('system', 'timezone');
+
+ date_default_timezone_set(self::getConfig('system', 'timezone'));
+
if(!is_string(self::getConfig('proxy', 'url')))
- die('Parameter [proxy] => "url" is not a valid string! Please check "config.ini.php"!');
+ self::reportConfigurationError('proxy', 'url', 'Is not a valid string');
if(!empty(self::getConfig('proxy', 'url'))) {
/** URL of the proxy server */
@@ -168,35 +168,38 @@ final class Configuration {
}
if(!is_bool(self::getConfig('proxy', 'by_bridge')))
- die('Parameter [proxy] => "by_bridge" is not a valid Boolean! Please check "config.ini.php"!');
+ self::reportConfigurationError('proxy', 'by_bridge', 'Is not a valid Boolean');
/** True if proxy usage can be enabled selectively for each bridge */
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"!');
+ self::reportConfigurationError('proxy', 'name', 'Is not a valid string');
/** Name of the proxy server */
define('PROXY_NAME', self::getConfig('proxy', 'name'));
+ if(!is_string(self::getConfig('cache', 'type')))
+ self::reportConfigurationError('cache', 'type', 'Is not a valid string');
+
if(!is_bool(self::getConfig('cache', 'custom_timeout')))
- die('Parameter [cache] => "custom_timeout" is not a valid Boolean! Please check "config.ini.php"!');
+ self::reportConfigurationError('cache', 'custom_timeout', 'Is not a valid Boolean');
/** True if the cache timeout can be specified by the user */
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"!');
+ self::reportConfigurationError('authentication', 'enable', 'Is not a valid Boolean');
if(!is_string(self::getConfig('authentication', 'username')))
- die('Parameter [authentication] => "username" is not a valid string! Please check "config.ini.php"!');
+ self::reportConfigurationError('authentication', 'username', 'Is not a valid string');
if(!is_string(self::getConfig('authentication', 'password')))
- die('Parameter [authentication] => "password" is not a valid string! Please check "config.ini.php"!');
+ self::reportConfigurationError('authentication', 'password', 'Is not a valid string');
if(!empty(self::getConfig('admin', 'email'))
&& !filter_var(self::getConfig('admin', 'email'), FILTER_VALIDATE_EMAIL))
- die('Parameter [admin] => "email" is not a valid email address! Please check "config.ini.php"!');
+ self::reportConfigurationError('admin', 'email', 'Is not a valid email address');
}
@@ -243,4 +246,46 @@ final class Configuration {
return Configuration::$VERSION;
}
+
+ /**
+ * Reports an configuration error for the specified section and key to the
+ * user and ends execution
+ *
+ * @param string $section The section name
+ * @param string $key The configuration key
+ * @param string $message An optional message to the user
+ *
+ * @return void
+ */
+ private static function reportConfigurationError($section, $key, $message = '') {
+
+ $report = "Parameter [{$section}] => \"{$key}\" is invalid!" . PHP_EOL;
+
+ if(file_exists(FILE_CONFIG)) {
+ $report .= 'Please check your configuration file at ' . FILE_CONFIG . PHP_EOL;
+ } elseif(!file_exists(FILE_CONFIG_DEFAULT)) {
+ $report .= 'The default configuration file is missing at ' . FILE_CONFIG_DEFAULT . PHP_EOL;
+ } else {
+ $report .= 'The default configuration file is broken.' . PHP_EOL
+ . 'Restore the original file from ' . REPOSITORY . PHP_EOL;
+ }
+
+ $report .= $message;
+ self::reportError($report);
+
+ }
+
+ /**
+ * Reports an error message to the user and ends execution
+ *
+ * @param string $message The error message
+ *
+ * @return void
+ */
+ private static function reportError($message) {
+
+ header('Content-Type: text/plain', true, 500);
+ die('Configuration error' . PHP_EOL . $message);
+
+ }
}
diff --git a/lib/Exceptions.php b/lib/Exceptions.php
index ac452d0..c749780 100644
--- a/lib/Exceptions.php
+++ b/lib/Exceptions.php
@@ -12,6 +12,15 @@
*/
/**
+ * Builds a GitHub search query to find open bugs for the current bridge
+ */
+function buildGitHubSearchQuery($bridgeName){
+ return REPOSITORY
+ . 'issues?q='
+ . urlencode('is:issue is:open ' . $bridgeName);
+}
+
+/**
* Returns an URL that automatically populates a new issue on GitHub based
* on the information provided
*
@@ -83,7 +92,8 @@ function buildBridgeException($e, $bridge){
. '`';
$body_html = nl2br($body);
- $link = buildGitHubIssueQuery($title, $body, 'bug report', $bridge->getMaintainer());
+ $link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer());
+ $searchQuery = buildGitHubSearchQuery($bridge::NAME);
$header = buildHeader($e, $bridge);
$message = <<<EOD
@@ -91,7 +101,7 @@ function buildBridgeException($e, $bridge){
remote website's content!<br>
{$body_html}
EOD;
- $section = buildSection($e, $bridge, $message, $link);
+ $section = buildSection($e, $bridge, $message, $link, $searchQuery);
return $section;
}
@@ -119,11 +129,12 @@ function buildTransformException($e, $bridge){
. (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '')
. '`';
- $link = buildGitHubIssueQuery($title, $body, 'bug report', $bridge->getMaintainer());
+ $link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer());
+ $searchQuery = buildGitHubSearchQuery($bridge::NAME);
$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);
+ $section = buildSection($e, $bridge, $message, $link, $searchQuery);
return buildPage($title, $header, $section);
}
@@ -154,11 +165,12 @@ EOD;
* @param object $bridge The bridge object
* @param string $message The message to display
* @param string $link The link to include in the anchor
+ * @param string $searchQuery A GitHub search query for the current bridge
* @return string The HTML section
*
* @todo This function belongs inside a class
*/
-function buildSection($e, $bridge, $message, $link){
+function buildSection($e, $bridge, $message, $link, $searchQuery){
return <<<EOD
<section>
<p class="exception-message">{$message}</p>
@@ -166,9 +178,13 @@ function buildSection($e, $bridge, $message, $link){
<ul class="advice">
<li>Press Return to check your input parameters</li>
<li>Press F5 to retry</li>
+ <li>Check if this issue was already reported on <a href="{$searchQuery}">GitHub</a> (give it a thumbs-up)</li>
<li>Open a <a href="{$link}">GitHub Issue</a> if this error persists</li>
</ul>
</div>
+ <a href="{$searchQuery}" title="Opens GitHub to search for similar issues">
+ <button>Search GitHub Issues</button>
+ </a>
<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>
diff --git a/lib/FactoryAbstract.php b/lib/FactoryAbstract.php
new file mode 100644
index 0000000..c91ae2e
--- /dev/null
+++ b/lib/FactoryAbstract.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * Abstract class for factories.
+ */
+abstract class FactoryAbstract {
+
+ /**
+ * Holds the working directory
+ *
+ * @var string
+ */
+ private $workingDir = null;
+
+ /**
+ * Set the working directory.
+ *
+ * @param string $dir The working directory.
+ * @return void
+ */
+ public function setWorkingDir($dir) {
+ $this->workingDir = null;
+
+ if(!is_string($dir)) {
+ throw new \InvalidArgumentException('Working directory must be a string!');
+ }
+
+ if(!file_exists($dir)) {
+ throw new \Exception('Working directory does not exist!');
+ }
+
+ if(!is_dir($dir)) {
+ throw new \InvalidArgumentException($dir . ' is not a directory!');
+ }
+
+ $this->workingDir = realpath($dir) . '/';
+ }
+
+ /**
+ * Get the working directory
+ *
+ * @return string The working directory.
+ */
+ public function getWorkingDir() {
+ if(is_null($this->workingDir)) {
+ throw new \LogicException('Working directory is not set!');
+ }
+
+ return $this->workingDir;
+ }
+
+ /**
+ * Creates a new instance for the object specified by name.
+ *
+ * @param string $name The name of the object to create.
+ * @return object The object instance
+ */
+ abstract public function create($name);
+}
diff --git a/lib/FeedExpander.php b/lib/FeedExpander.php
index b669351..665620a 100644
--- a/lib/FeedExpander.php
+++ b/lib/FeedExpander.php
@@ -265,7 +265,7 @@ abstract class FeedExpander extends BridgeAbstract {
//When "link" field is present, URL is more reliable than "id" field
if (count($feedItem->link) === 1) {
- $this->uri = (string)$feedItem->link[0]['href'];
+ $item['uri'] = (string)$feedItem->link[0]['href'];
} else {
foreach($feedItem->link as $link) {
if(strtolower($link['rel']) === 'alternate') {
diff --git a/lib/FeedItem.php b/lib/FeedItem.php
index 2812da6..9a43573 100644
--- a/lib/FeedItem.php
+++ b/lib/FeedItem.php
@@ -55,6 +55,9 @@ class FeedItem {
/** @var array List of category names or tags */
protected $categories = array();
+ /** @var string Unique ID for the current item */
+ protected $uid = null;
+
/** @var array Associative list of additional parameters */
protected $misc = array(); // Custom parameters
@@ -73,7 +76,7 @@ class FeedItem {
* $item['uri'] = 'https://www.github.com/rss-bridge/rss-bridge/';
* $item['title'] = 'Title';
* $item['timestamp'] = strtotime('now');
- * $item['autor'] = 'Unknown author';
+ * $item['author'] = 'Unknown author';
* $item['content'] = 'Hello World!';
* $item['enclosures'] = array('https://github.com/favicon.ico');
* $item['categories'] = array('php', 'rss-bridge', 'awesome');
@@ -344,7 +347,7 @@ class FeedItem {
$enclosure,
FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) {
Debug::log('Each enclosure must contain a scheme, host and path!');
- } else {
+ } elseif(!in_array($enclosure, $this->enclosures)) {
$this->enclosures[] = $enclosure;
}
}
@@ -392,6 +395,40 @@ class FeedItem {
}
/**
+ * Get unique id
+ *
+ * Use {@see FeedItem::setUid()} to set the unique id.
+ *
+ * @param string The unique id.
+ */
+ public function getUid() {
+ return $this->uid;
+ }
+
+ /**
+ * Set unique id.
+ *
+ * Use {@see FeedItem::getUid()} to get the unique id.
+ *
+ * @param string $uid A string that uniquely identifies the current item
+ * @return self
+ */
+ public function setUid($uid) {
+ $this->uid = null; // Clear previous data
+
+ if(!is_string($uid)) {
+ Debug::log('Unique id must be a string!');
+ } elseif (preg_match('/^[a-f0-9]{40}$/', $uid)) {
+ // keep id if it already is a SHA-1 hash
+ $this->uid = $uid;
+ } else {
+ $this->uid = sha1($uid);
+ }
+
+ return $this;
+ }
+
+ /**
* Add miscellaneous elements to the item.
*
* @param string $key Name of the element.
@@ -426,6 +463,7 @@ class FeedItem {
'content' => $this->content,
'enclosures' => $this->enclosures,
'categories' => $this->categories,
+ 'uid' => $this->uid,
), $this->misc
);
}
@@ -454,6 +492,7 @@ class FeedItem {
case 'content': $this->setContent($value); break;
case 'enclosures': $this->setEnclosures($value); break;
case 'categories': $this->setCategories($value); break;
+ case 'uid': $this->setUid($value); break;
default: $this->addMisc($name, $value);
}
}
@@ -476,6 +515,7 @@ class FeedItem {
case 'content': return $this->getContent();
case 'enclosures': return $this->getEnclosures();
case 'categories': return $this->getCategories();
+ case 'uid': return $this->getUid();
default:
if(array_key_exists($name, $this->misc))
return $this->misc[$name];
diff --git a/lib/Format.php b/lib/FormatFactory.php
index 061b1f2..28db759 100644
--- a/lib/Format.php
+++ b/lib/FormatFactory.php
@@ -31,29 +31,7 @@
* $format = Format::create('Atom');
* ```
*/
-class Format {
-
- /**
- * Holds a path to the working directory.
- *
- * Do not access this property directly!
- * Use {@see Format::setWorkingDir()} and {@see Format::getWorkingDir()} instead.
- *
- * @var string|null
- */
- protected static $workingDir = null;
-
- /**
- * Throws an exception when trying to create a new instance of this class.
- * Use {@see Format::create()} to create a new format object from the working
- * directory.
- *
- * @throws \LogicException if called.
- */
- public function __construct(){
- throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create cache objects!');
- }
-
+class FormatFactory extends FactoryAbstract {
/**
* Creates a new format object from the working directory.
*
@@ -63,13 +41,13 @@ class Format {
* @param string $name Name of the format object.
* @return object|bool The format object or false if the class is not instantiable.
*/
- public static function create($name){
- if(!self::isFormatName($name)) {
+ public function create($name){
+ if(!$this->isFormatName($name)) {
throw new \InvalidArgumentException('Format name invalid!');
}
- $name = $name . 'Format';
- $pathFormat = self::getWorkingDir() . $name . '.php';
+ $name = $this->sanitizeFormatName($name) . 'Format';
+ $pathFormat = $this->getWorkingDir() . $name . '.php';
if(!file_exists($pathFormat)) {
throw new \Exception('Format file ' . $filePath . ' does not exist!');
@@ -85,48 +63,6 @@ class Format {
}
/**
- * Sets the working directory.
- *
- * @param string $dir Path to a directory containing cache classes
- * @throws \InvalidArgumentException if $dir is not a string.
- * @throws \Exception if the working directory doesn't exist.
- * @throws \InvalidArgumentException if $dir is not a directory.
- * @return void
- */
- public static function setWorkingDir($dir){
- self::$workingDir = null;
-
- if(!is_string($dir)) {
- throw new \InvalidArgumentException('Dir format must be a string.');
- }
-
- if(!file_exists($dir)) {
- throw new \Exception('Working directory does not exist!');
- }
-
- if(!is_dir($dir)) {
- throw new \InvalidArgumentException('Working directory is not a directory!');
- }
-
- self::$workingDir = realpath($dir) . '/';
- }
-
- /**
- * Returns the working directory.
- * The working directory must be set with {@see Format::setWorkingDir()}!
- *
- * @throws \LogicException if the working directory is not set.
- * @return string The current working directory.
- */
- public static function getWorkingDir(){
- if(is_null(self::$workingDir)) {
- throw new \LogicException('Working directory is not set!');
- }
-
- return self::$workingDir;
- }
-
- /**
* Returns true if the provided name is a valid format name.
*
* A valid format name starts with a capital letter ([A-Z]), followed by
@@ -135,7 +71,7 @@ class Format {
* @param string $name The format name.
* @return bool true if the name is a valid format name, false otherwise.
*/
- public static function isFormatName($name){
+ public function isFormatName($name){
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
@@ -146,11 +82,11 @@ class Format {
*
* @return array List of format names
*/
- public static function getFormatNames(){
+ public function getFormatNames(){
static $formatNames = array(); // Initialized on first call
if(empty($formatNames)) {
- $files = scandir(self::getWorkingDir());
+ $files = scandir($this->getWorkingDir());
if($files !== false) {
foreach($files as $file) {
@@ -163,4 +99,55 @@ class Format {
return $formatNames;
}
+
+ /**
+ * Returns the sanitized format name.
+ *
+ * The format name can be specified in various ways:
+ * * The PHP file name (i.e. `AtomFormat.php`)
+ * * The PHP file name without file extension (i.e. `AtomFormat`)
+ * * The format name (i.e. `Atom`)
+ *
+ * Casing is ignored (i.e. `ATOM` and `atom` are the same).
+ *
+ * A format file matching the given format name must exist in the working
+ * directory!
+ *
+ * @param string $name The format name
+ * @return string|null The sanitized format name if the provided name is
+ * valid, null otherwise.
+ */
+ protected function sanitizeFormatName($name) {
+
+ if(is_string($name)) {
+
+ // Trim trailing '.php' if exists
+ if(preg_match('/(.+)(?:\.php)/', $name, $matches)) {
+ $name = $matches[1];
+ }
+
+ // Trim trailing 'Format' if exists
+ if(preg_match('/(.+)(?:Format)/i', $name, $matches)) {
+ $name = $matches[1];
+ }
+
+ // Improve performance for correctly written format names
+ if(in_array($name, $this->getFormatNames())) {
+ $index = array_search($name, $this->getFormatNames());
+ return $this->getFormatNames()[$index];
+ }
+
+ // The name is valid if a corresponding format file is found on disk
+ if(in_array(strtolower($name), array_map('strtolower', $this->getFormatNames()))) {
+ $index = array_search(strtolower($name), array_map('strtolower', $this->getFormatNames()));
+ return $this->getFormatNames()[$index];
+ }
+
+ Debug::log('Invalid format name: "' . $name . '"!');
+
+ }
+
+ return null; // Bad parameter
+
+ }
}
diff --git a/lib/ParameterValidator.php b/lib/ParameterValidator.php
index 91fe7c9..f740888 100644
--- a/lib/ParameterValidator.php
+++ b/lib/ParameterValidator.php
@@ -195,13 +195,14 @@ class ParameterValidator {
foreach($set as $id => $properties) {
if(isset($data[$id]) && !empty($data[$id])) {
$queriedContexts[$context] = true;
- } elseif(isset($properties['required'])
- && $properties['required'] === true) {
+ } elseif (isset($properties['type'])
+ && ($properties['type'] === 'checkbox' || $properties['type'] === 'list')) {
+ continue;
+ } elseif(isset($properties['required']) && $properties['required'] === true) {
$queriedContexts[$context] = false;
break;
}
}
-
}
// Abort if one of the globally required parameters is not satisfied
@@ -213,6 +214,7 @@ class ParameterValidator {
switch(array_sum($queriedContexts)) {
case 0: // Found no match, is there a context without parameters?
+ if(isset($data['context'])) return $data['context'];
foreach($queriedContexts as $context => $queried) {
if(is_null($queried)) {
return $context;
diff --git a/lib/contents.php b/lib/contents.php
index dc0ca51..8649c0b 100644
--- a/lib/contents.php
+++ b/lib/contents.php
@@ -45,16 +45,31 @@ function getContents($url, $header = array(), $opts = array()){
Debug::log('Reading contents from "' . $url . '"');
// Initialize cache
- $cache = Cache::create('FileCache');
- $cache->setPath(PATH_CACHE . 'server/');
+ $cacheFac = new CacheFactory();
+ $cacheFac->setWorkingDir(PATH_LIB_CACHES);
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope('server');
$cache->purgeCache(86400); // 24 hours (forced)
$params = [$url];
- $cache->setParameters($params);
+ $cache->setKey($params);
// Use file_get_contents if in CLI mode with no root certificates defined
if(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo'))) {
- $data = @file_get_contents($url);
+
+ $httpHeaders = '';
+
+ foreach ($header as $headerL) {
+ $httpHeaders .= $headerL . "\r\n";
+ }
+
+ $ctx = stream_context_create(array(
+ 'http' => array(
+ 'header' => $httpHeaders
+ )
+ ));
+
+ $data = @file_get_contents($url, 0, $ctx);
if($data === false) {
$errorCode = 500;
@@ -207,14 +222,15 @@ EOD
* @return string Contents as simplehtmldom object.
*/
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){
+ $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,
@@ -256,24 +272,27 @@ $defaultSpanText = DEFAULT_SPAN_TEXT){
* @return string Contents as simplehtmldom object.
*/
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){
+ $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){
+
Debug::log('Caching url ' . $url . ', duration ' . $duration);
// Initialize cache
- $cache = Cache::create('FileCache');
- $cache->setPath(PATH_CACHE . 'pages/');
+ $cacheFac = new CacheFactory();
+ $cacheFac->setWorkingDir(PATH_LIB_CACHES);
+ $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
+ $cache->setScope('pages');
$cache->purgeCache(86400); // 24 hours (forced)
$params = [$url];
- $cache->setParameters($params);
+ $cache->setKey($params);
// Determine if cached file is within duration
$time = $cache->getTime();
@@ -320,8 +339,8 @@ function parseResponseHeader($header) {
$header['http_code'] = $line;
} else {
- list ($key, $value) = explode(': ', $line);
- $header[$key] = $value;
+ list ($key, $value) = explode(':', $line);
+ $header[$key] = trim($value);
}
diff --git a/lib/html.php b/lib/html.php
index e49ca7a..13db97a 100644
--- a/lib/html.php
+++ b/lib/html.php
@@ -26,23 +26,13 @@
* already removes some of the tags (search for `remove_noise` in simple_html_dom.php).
*/
function sanitize($html,
-$tags_to_remove = array('script', 'iframe', 'input', 'form'),
-$attributes_to_keep = array('title', 'href', 'src'),
-$text_to_keep = array()){
+ $tags_to_remove = array('script', 'iframe', 'input', 'form'),
+ $attributes_to_keep = array('title', 'href', 'src'),
+ $text_to_keep = array()){
+
$htmlContent = str_get_html($html);
- /*
- * Notice: simple_html_dom currently doesn't support "->find(*)", which is a
- * known issue: https://sourceforge.net/p/simplehtmldom/bugs/157/
- *
- * A solution to this is to find all nodes WITHOUT a specific attribute. If
- * the attribute is very unlikely to appear in the DOM, this is essentially
- * returning all nodes.
- *
- * "*[!b38fd2b1fe7f4747d6b1c1254ccd055e]" is doing exactly that. The attrib
- * "b38fd2b1fe7f4747d6b1c1254ccd055e" is very unlikely to appear in any DOM.
- */
- foreach($htmlContent->find('*[!b38fd2b1fe7f4747d6b1c1254ccd055e]') as $element) {
+ foreach($htmlContent->find('*') as $element) {
if(in_array($element->tag, $text_to_keep)) {
$element->outertext = $element->plaintext;
} elseif(in_array($element->tag, $tags_to_remove)) {
@@ -89,18 +79,7 @@ function backgroundToImg($htmlContent) {
$regex = '/background-image[ ]{0,}:[ ]{0,}url\([\'"]{0,}(.*?)[\'"]{0,}\)/';
$htmlContent = str_get_html($htmlContent);
- /*
- * Notice: simple_html_dom currently doesn't support "->find(*)", which is a
- * known issue: https://sourceforge.net/p/simplehtmldom/bugs/157/
- *
- * A solution to this is to find all nodes WITHOUT a specific attribute. If
- * the attribute is very unlikely to appear in the DOM, this is essentially
- * returning all nodes.
- *
- * "*[!b38fd2b1fe7f4747d6b1c1254ccd055e]" is doing exactly that. The attrib
- * "b38fd2b1fe7f4747d6b1c1254ccd055e" is very unlikely to appear in any DOM.
- */
- foreach($htmlContent->find('*[!b38fd2b1fe7f4747d6b1c1254ccd055e]') as $element) {
+ foreach($htmlContent->find('*') as $element) {
if(preg_match($regex, $element->style, $matches) > 0) {
diff --git a/lib/rssbridge.php b/lib/rssbridge.php
index dbeab26..a025f22 100644
--- a/lib/rssbridge.php
+++ b/lib/rssbridge.php
@@ -15,49 +15,65 @@
define('PATH_ROOT', __DIR__ . '/../');
/** Path to the core library */
-define('PATH_LIB', __DIR__ . '/../lib/'); // Path to core library
+define('PATH_LIB', PATH_ROOT . 'lib/');
/** Path to the vendor library */
-define('PATH_LIB_VENDOR', __DIR__ . '/../vendor/');
+define('PATH_LIB_VENDOR', PATH_ROOT . 'vendor/');
/** Path to the bridges library */
-define('PATH_LIB_BRIDGES', __DIR__ . '/../bridges/');
+define('PATH_LIB_BRIDGES', PATH_ROOT . 'bridges/');
/** Path to the formats library */
-define('PATH_LIB_FORMATS', __DIR__ . '/../formats/');
+define('PATH_LIB_FORMATS', PATH_ROOT . 'formats/');
/** Path to the caches library */
-define('PATH_LIB_CACHES', __DIR__ . '/../caches/');
+define('PATH_LIB_CACHES', PATH_ROOT . 'caches/');
+
+/** Path to the actions library */
+define('PATH_LIB_ACTIONS', PATH_ROOT . 'actions/');
/** Path to the cache folder */
-define('PATH_CACHE', __DIR__ . '/../cache/');
+define('PATH_CACHE', PATH_ROOT . 'cache/');
/** Path to the whitelist file */
-define('WHITELIST', __DIR__ . '/../whitelist.txt');
+define('WHITELIST', PATH_ROOT . 'whitelist.txt');
+
+/** Path to the default whitelist file */
+define('WHITELIST_DEFAULT', PATH_ROOT . 'whitelist.default.txt');
+
+/** Path to the configuration file */
+define('FILE_CONFIG', PATH_ROOT . 'config.ini.php');
+
+/** Path to the default configuration file */
+define('FILE_CONFIG_DEFAULT', PATH_ROOT . 'config.default.ini.php');
/** URL to the RSS-Bridge repository */
define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/');
// Interfaces
+require_once PATH_LIB . 'ActionInterface.php';
require_once PATH_LIB . 'BridgeInterface.php';
require_once PATH_LIB . 'CacheInterface.php';
require_once PATH_LIB . 'FormatInterface.php';
// Classes
+require_once PATH_LIB . 'FactoryAbstract.php';
require_once PATH_LIB . 'FeedItem.php';
require_once PATH_LIB . 'Debug.php';
require_once PATH_LIB . 'Exceptions.php';
-require_once PATH_LIB . 'Format.php';
+require_once PATH_LIB . 'FormatFactory.php';
require_once PATH_LIB . 'FormatAbstract.php';
-require_once PATH_LIB . 'Bridge.php';
+require_once PATH_LIB . 'BridgeFactory.php';
require_once PATH_LIB . 'BridgeAbstract.php';
require_once PATH_LIB . 'FeedExpander.php';
-require_once PATH_LIB . 'Cache.php';
+require_once PATH_LIB . 'CacheFactory.php';
require_once PATH_LIB . 'Authentication.php';
require_once PATH_LIB . 'Configuration.php';
require_once PATH_LIB . 'BridgeCard.php';
require_once PATH_LIB . 'BridgeList.php';
require_once PATH_LIB . 'ParameterValidator.php';
+require_once PATH_LIB . 'ActionFactory.php';
+require_once PATH_LIB . 'ActionAbstract.php';
// Functions
require_once PATH_LIB . 'html.php';
@@ -65,16 +81,6 @@ require_once PATH_LIB . 'error.php';
require_once PATH_LIB . 'contents.php';
// Vendor
+define('MAX_FILE_SIZE', 10000000); /* Allow larger files for simple_html_dom */
require_once PATH_LIB_VENDOR . 'simplehtmldom/simple_html_dom.php';
require_once PATH_LIB_VENDOR . 'php-urljoin/src/urljoin.php';
-
-// Initialize static members
-try {
- Bridge::setWorkingDir(PATH_LIB_BRIDGES);
- Format::setWorkingDir(PATH_LIB_FORMATS);
- Cache::setWorkingDir(PATH_LIB_CACHES);
-} catch(Exception $e) {
- error_log($e);
- header('Content-type: text/plain', true, 500);
- die($e->getMessage());
-}
diff --git a/static/HtmlFormat.css b/static/HtmlFormat.css
index e17e325..4ebc79e 100644
--- a/static/HtmlFormat.css
+++ b/static/HtmlFormat.css
@@ -32,7 +32,6 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
width: 60%;
margin: 30px auto;
padding: 15px 15px;
- text-align: center;
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.09);
border-radius: 4px;
}
@@ -60,6 +59,18 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
section > div.content, section > div.attachments {
padding: 10px;
}
+ section h1, section h2, section h3, section b, section strong {
+ font-weight: bold;
+}
+ section i, section em {
+ font-style: italic;
+}
+ section p:not(:last-child) {
+ margin-bottom: 1em;
+}
+ section li {
+ margin-left: 1em;
+}
section > div.attachments > li.enclosure {
list-style-type: circle;
list-style-position: inside;
@@ -84,4 +95,21 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
}
button:hover {
background: #49afff;
-} \ No newline at end of file
+}
+
+@media screen and (max-width: 767px) {
+
+ section {
+ width: 100%;
+ padding: 0;
+
+ }
+
+ button {
+ display: inline-block;
+ width: 40%;
+ padding: 5px auto;
+ margin: 3px auto 0;
+ }
+
+}
diff --git a/static/favicon.png b/static/favicon.png
new file mode 100644
index 0000000..0a072e0
--- /dev/null
+++ b/static/favicon.png
Binary files differ
diff --git a/static/favicon.svg b/static/favicon.svg
new file mode 100644
index 0000000..0870bf6
--- /dev/null
+++ b/static/favicon.svg
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="200mm"
+ height="200mm"
+ viewBox="0 0 200 200"
+ version="1.1"
+ id="svg871"
+ inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
+ sodipodi:docname="favicon_rssbridge.svg">
+ <defs
+ id="defs865" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.35"
+ inkscape:cx="495.71429"
+ inkscape:cy="542.85714"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1366"
+ inkscape:window-height="705"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata868">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Calque 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-97)">
+ <g
+ id="g2147"
+ transform="matrix(1.7746042,0,0,1.7746042,3145.263,-3080.4079)"
+ inkscape:export-xdpi="61.620663"
+ inkscape:export-ydpi="61.620663">
+ <rect
+ inkscape:export-ydpi="68"
+ inkscape:export-xdpi="68"
+ ry="17.993027"
+ rx="17.993027"
+ y="1803.3181"
+ x="-1759.36"
+ height="86.856956"
+ width="86.856956"
+ id="rect2098"
+ style="opacity:1;vector-effect:none;fill:#ff6600;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4.00000008, 8.00000016;stroke-dashoffset:0;stroke-opacity:1" />
+ <flowRoot
+ inkscape:export-filename="C:\Users\Gyrev\Dropbox\4 - Obs\01 - Com - media\03 - infographies\logo_glasses.png"
+ transform="matrix(0.5968306,0,0,0.5968306,-1834.59,1688.6939)"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.61466217"
+ id="flowRoot2106"
+ xml:space="preserve"
+ inkscape:export-xdpi="68.183243"
+ inkscape:export-ydpi="68.183243"><flowRegion
+ id="flowRegion2102"
+ style="fill:#ffffff;fill-opacity:1;stroke-width:2.61466217"><rect
+ y="275.93942"
+ x="140.55341"
+ height="65.626038"
+ width="253.20284"
+ id="rect2100"
+ style="fill:#ffffff;fill-opacity:1;stroke-width:2.61466217" /></flowRegion><flowPara
+ style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2 Semi-Bold';letter-spacing:-1px;fill:#ffffff;fill-opacity:1;stroke-width:2.61466217"
+ id="flowPara2104">Bridge</flowPara></flowRoot> <g
+ style="stroke-width:2.99397564"
+ inkscape:export-ydpi="68"
+ inkscape:export-xdpi="68"
+ id="g2118"
+ transform="matrix(0.33400405,0,0,0.33400405,-1609.4253,1569.2886)">
+ <flowRoot
+ inkscape:export-filename="C:\Users\Gyrev\Dropbox\4 - Obs\01 - Com - media\03 - infographies\logo_glasses.png"
+ transform="matrix(1.7868963,0,0,1.7868963,-620.90965,302.19806)"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.61466217"
+ id="flowRoot2116"
+ xml:space="preserve"
+ inkscape:export-xdpi="68.183243"
+ inkscape:export-ydpi="68.183243"><flowRegion
+ id="flowRegion2112"
+ style="fill:#ffffff;fill-opacity:1;stroke-width:2.61466217"><rect
+ y="275.93942"
+ x="140.55341"
+ height="65.626038"
+ width="253.20284"
+ id="rect2110"
+ style="fill:#ffffff;fill-opacity:1;stroke-width:2.61466217" /></flowRegion><flowPara
+ style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2 Semi-Bold';letter-spacing:-1px;fill:#ffffff;fill-opacity:1;stroke-width:2.61466217"
+ id="flowPara2114">rss</flowPara></flowRoot> </g>
+ <path
+ inkscape:export-ydpi="68"
+ inkscape:export-xdpi="68"
+ inkscape:connector-curvature="0"
+ id="path2120"
+ d="m -1719.2669,1838.3599 c -5.0205,-1.3144 -8.8786,-5.3846 -10.0117,-10.5624 -0.335,-1.5309 -0.2541,-4.567 0.1641,-6.161 1.284,-4.8933 4.9001,-8.573 9.7609,-9.9326 1.5509,-0.4338 4.2673,-0.512 6.1345,-0.1767 4.1633,0.7477 8.2284,4.1132 9.8663,8.1681 0.7975,1.9744 0.9807,2.9793 0.9762,5.3548 0,1.8242 -0.075,2.499 -0.3818,3.58 -1.3733,4.8468 -5.1589,8.5581 -9.9346,9.7395 -1.8168,0.4494 -4.837,0.445 -6.5739,-0.01 z m 2.2445,-7.5051 c -0.2717,-0.1738 -3.5329,-0.3553 -3.6195,-0.2014 -0.1223,0.2172 1.1312,3.2066 1.6601,3.9591 0.2581,0.3672 0.8046,0.9237 1.2144,1.2364 l 0.745,0.5688 0.083,-2.7282 c 0.053,-1.7295 0.022,-2.7672 -0.083,-2.8347 z m 3.4669,4.5392 c 0.6573,-0.6108 1.0277,-1.2558 1.7728,-3.087 0.3973,-0.9763 0.5822,-1.631 0.4789,-1.6949 -0.1723,-0.1065 -2.753,0.054 -3.429,0.2138 -0.3689,0.087 -0.3747,0.1285 -0.3747,2.7067 0,1.4401 0.05,2.6687 0.1116,2.73 0.1328,0.1328 0.7879,-0.2623 1.4404,-0.8686 z m -7.1654,-0.2901 c 0.057,-0.057 -0.07,-0.4503 -0.2823,-0.8742 -0.5012,-1.0013 -0.9314,-2.3267 -1.0704,-3.2976 l -0.1107,-0.7741 -1.0768,-0.2197 c -0.5923,-0.1208 -1.5232,-0.3704 -2.0688,-0.5546 -0.5455,-0.1842 -1.0215,-0.3053 -1.0578,-0.269 -0.1007,0.1007 0.5958,1.4049 1.2973,2.4294 0.7668,1.1198 1.9378,2.2559 3.1536,3.0595 0.9557,0.6316 1.047,0.6692 1.2159,0.5003 z m 11.6482,-1.1869 c 0.4342,-0.3121 1.1176,-0.9425 1.5185,-1.4009 0.7228,-0.8263 2.2126,-3.2507 2.0793,-3.3839 -0.081,-0.081 -3.4631,0.9745 -3.9405,1.23 -0.1894,0.1013 -0.3734,0.436 -0.4412,0.8027 -0.064,0.346 -0.4063,1.3351 -0.7607,2.1982 -0.3543,0.863 -0.6045,1.6335 -0.5558,1.7122 0.096,0.1552 0.8564,-0.2642 2.1004,-1.1583 z m -7.7321,-7.5308 c 0.028,-1.5854 -0.012,-2.9457 -0.089,-3.0227 -0.077,-0.077 -1.1425,-0.1909 -2.3677,-0.2529 l -2.2276,-0.1128 0.1048,2.6632 c 0.058,1.4647 0.1506,2.7824 0.2066,2.9281 0.068,0.1771 0.4702,0.3241 1.2128,0.4429 1.0818,0.1732 2.6072,0.3083 2.9427,0.2606 0.1015,-0.014 0.1865,-1.1506 0.2176,-2.9064 z m 4.7233,2.6386 c 0.6339,-0.097 1.1476,-0.2656 1.1959,-0.3916 0.047,-0.1215 0.1731,-0.9804 0.281,-1.9087 0.1853,-1.5935 0.1233,-3.2044 -0.1388,-3.6068 -0.084,-0.1292 -0.7256,-0.1333 -2.241,-0.014 l -2.1231,0.1669 v 2.9435 c 0,1.6188 0.039,2.9822 0.086,3.0297 0.1077,0.1077 1.4775,0.01 2.9397,-0.2189 z m -10.6043,-0.8169 c -0.1446,-0.4559 -0.1658,-4.6514 -0.026,-5.08 0.1024,-0.3129 -0.039,-0.3779 -1.7622,-0.8077 -1.0305,-0.2571 -1.9525,-0.4987 -2.0489,-0.5369 -0.2228,-0.088 -0.7405,2.5619 -0.7405,3.7909 0,1.3374 0.5346,1.8093 2.914,2.572 1.5268,0.4894 1.8022,0.4996 1.6632,0.062 z m 15.5636,-0.1064 c 1.2002,-0.3876 2.3124,-1.419 2.5141,-2.3313 0.1656,-0.7493 0.01,-1.8941 -0.4444,-3.2707 -0.2107,-0.6382 -0.3209,-0.7776 -0.5391,-0.6817 -0.1509,0.066 -1.0236,0.3247 -1.9395,0.5742 -1.1985,0.3265 -1.6453,0.5175 -1.5943,0.6816 0.1128,0.3635 0.051,4.4177 -0.076,4.9964 l -0.1154,0.5253 0.6589,-0.1053 c 0.3623,-0.058 1.0533,-0.2328 1.5355,-0.3885 z m -9.7252,-6.5935 c 0.045,-0.1165 0.061,-1.9335 0.036,-4.038 l -0.045,-3.8262 -1.1336,1.0763 c -0.8596,0.8162 -1.2919,1.3997 -1.7889,2.4145 -0.6761,1.3806 -1.4646,4.0002 -1.2644,4.2005 0.3609,0.3609 4.0646,0.5135 4.1953,0.1729 z m 5.1134,0.062 c 0.4359,-0.072 0.84,-0.1778 0.8978,-0.2357 0.1638,-0.1638 -0.9127,-3.4126 -1.4982,-4.5217 -0.5028,-0.9524 -2.3179,-3.085 -2.6256,-3.085 -0.1694,0 -0.205,7.7131 -0.036,7.8817 0.1489,0.1489 2.2742,0.1233 3.2624,-0.039 z m -10.6939,-1.057 c 0.4232,-1.6148 0.7365,-2.617 1.2494,-3.9963 0.2979,-0.8014 0.5037,-1.457 0.4573,-1.457 -0.3843,0 -2.3317,1.2786 -3.2089,2.1068 -1.2568,1.1867 -2.3208,2.8017 -2.0344,3.0881 0.1665,0.1666 2.6682,0.9242 3.1493,0.9537 0.111,0.01 0.2852,-0.306 0.3873,-0.6953 z m 14.5638,0.4564 c 1.3057,-0.275 2.0815,-0.556 2.0815,-0.754 0,-0.3409 -1.7648,-2.7156 -2.5545,-3.4374 -0.7298,-0.667 -2.662,-1.8848 -2.9904,-1.8848 -0.048,0 0.095,0.3754 0.3159,0.8343 0.4315,0.8944 1.071,2.9784 1.392,4.5358 0.1085,0.5266 0.2856,0.9561 0.3936,0.9545 0.108,0 0.7209,-0.1133 1.3619,-0.2484 z"
+ style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.00000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4.00000004, 8.0000001;stroke-dashoffset:0;stroke-opacity:1" />
+ </g>
+ </g>
+</svg>
diff --git a/static/logo.svg b/static/logo.svg
new file mode 100644
index 0000000..0096d79
--- /dev/null
+++ b/static/logo.svg
@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="300mm"
+ height="100mm"
+ viewBox="0 0 300 100"
+ version="1.1"
+ id="svg1551"
+ inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
+ sodipodi:docname="logo_rssbridge.svg">
+ <defs
+ id="defs1545" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.35"
+ inkscape:cx="517.88898"
+ inkscape:cy="397.65625"
+ inkscape:document-units="mm"
+ inkscape:current-layer="g1492"
+ showgrid="false"
+ inkscape:window-width="1366"
+ inkscape:window-height="705"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata1548">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Calque 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-197)">
+ <g
+ style="fill:#1182db;fill-opacity:1"
+ id="g1492"
+ transform="matrix(1.063066,0,0,1.063066,31.239097,-1662.8034)"
+ inkscape:export-xdpi="84.084839"
+ inkscape:export-ydpi="84.084839">
+ <flowRoot
+ inkscape:export-ydpi="68.183243"
+ inkscape:export-xdpi="68.183243"
+ xml:space="preserve"
+ id="flowRoot1458"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:5.40849686"
+ transform="matrix(0.2885294,0,0,0.2885294,11.385677,1734.3629)"
+ inkscape:export-filename="C:\Users\Gyrev\Dropbox\4 - Obs\01 - Com - media\03 - infographies\logo_glasses.png"><flowRegion
+ style="fill:#1182db;fill-opacity:1;stroke-width:5.40849686"
+ id="flowRegion1454"><rect
+ style="fill:#1182db;fill-opacity:1;stroke-width:5.40849686"
+ id="rect1452"
+ width="555.50842"
+ height="60.796875"
+ x="140.55341"
+ y="275.93942" /></flowRegion><flowPara
+ id="flowPara1456"
+ style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2 Semi-Bold';letter-spacing:3.48859px;fill:#1182db;fill-opacity:1;stroke-width:5.40849686">reconnecting the web</flowPara></flowRoot> <g
+ id="g1468"
+ style="fill:#1182db;fill-opacity:1;stroke-width:1.69267535"
+ transform="matrix(0.59078074,0,0,0.59078074,36.380377,1356.4656)">
+ <flowRoot
+ inkscape:export-ydpi="68.183243"
+ inkscape:export-xdpi="68.183243"
+ xml:space="preserve"
+ id="flowRoot1466"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:1.47822654"
+ transform="matrix(1.7868963,0,0,1.7868963,-131.08627,186.65131)"
+ inkscape:export-filename="C:\Users\Gyrev\Dropbox\4 - Obs\01 - Com - media\03 - infographies\logo_glasses.png"><flowRegion
+ style="fill:#1182db;fill-opacity:1;stroke-width:1.47822654"
+ id="flowRegion1462"><rect
+ style="fill:#1182db;fill-opacity:1;stroke-width:1.47822654"
+ id="rect1460"
+ width="253.20284"
+ height="65.626038"
+ x="140.55341"
+ y="275.93942" /></flowRegion><flowPara
+ id="flowPara1464"
+ style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2 Semi-Bold';letter-spacing:-2.1810503px;fill:#1182db;fill-opacity:1;stroke-width:1.47822654">Bridge</flowPara></flowRoot> </g>
+ <g
+ transform="matrix(0.59078074,0,0,0.59078074,66.923727,1356.4656)"
+ style="fill:#1182db;fill-opacity:1;stroke-width:1.69267535"
+ id="g1478">
+ <flowRoot
+ inkscape:export-filename="C:\Users\Gyrev\Dropbox\4 - Obs\01 - Com - media\03 - infographies\logo_glasses.png"
+ transform="matrix(1.7868963,0,0,1.7868963,-294.87276,186.65131)"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:1.47822654"
+ id="flowRoot1476"
+ xml:space="preserve"
+ inkscape:export-xdpi="68.183243"
+ inkscape:export-ydpi="68.183243"><flowRegion
+ id="flowRegion1472"
+ style="fill:#1182db;fill-opacity:1;stroke-width:1.47822654"><rect
+ y="275.93942"
+ x="140.55341"
+ height="65.626038"
+ width="253.20284"
+ id="rect1470"
+ style="fill:#1182db;fill-opacity:1;stroke-width:1.47822654" /></flowRegion><flowPara
+ style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2 Semi-Bold';letter-spacing:-2.1810503px;fill:#1182db;fill-opacity:1;stroke-width:1.47822654"
+ id="flowPara1474">rss</flowPara></flowRoot> </g>
+ <rect
+ ry="7.3332076"
+ rx="7.3332076"
+ y="1763.6888"
+ x="3.8301878"
+ height="41.961315"
+ width="99.644852"
+ id="rect1480"
+ style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#1182db;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <g
+ id="g1490"
+ transform="matrix(0.42065629,0,0,0.42065629,481.55091,842.16455)"
+ style="fill:#1182db;fill-opacity:1;stroke-width:2.3772378">
+ <g
+ style="fill:#1182db;fill-opacity:1;stroke-width:3.88193178"
+ transform="matrix(0.61238525,0,0,0.61238525,-1199.6119,2184.4357)"
+ id="g1488">
+ <path
+ style="opacity:1;vector-effect:none;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:29.34373856;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:58.68747418, 117.37494829;stroke-dashoffset:0;stroke-opacity:1"
+ d="m 541.51249,525.34565 c -10.41315,-4.00956 -14.78996,-6.8636 -21.21485,-13.83378 -9.51065,-10.31784 -12.16446,-17.61997 -12.22497,-33.63782 -0.0451,-11.93786 0.4192,-14.53892 3.85734,-21.60913 4.60063,-9.46079 15.51168,-20.18661 24.98644,-24.56227 5.4253,-2.50553 9.38434,-3.11239 20.31033,-3.11325 11.66691,-9.2e-4 14.67404,0.52181 21.42857,3.7249 9.99443,4.73949 19.32279,14.0885 24.07789,24.13117 5.25545,11.09941 5.95363,27.48145 1.65888,38.9238 -4.02442,10.72212 -14.87839,22.6043 -25.31059,27.7083 -10.28725,5.03309 -27.6637,6.08212 -37.56904,2.26808 z"
+ id="path1482"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)" />
+ <path
+ style="opacity:1;vector-effect:none;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:29.34373856;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:58.68747418, 117.37494829;stroke-dashoffset:0;stroke-opacity:1"
+ d="m 683.89345,526.92166 c -0.52382,-0.5238 -0.96246,-2.93451 -0.97477,-5.35714 -0.0352,-6.92667 -5.18979,-29.62173 -10.01089,-44.07679 C 648.75042,405.05698 592.21848,359.91722 516.2625,352.40923 l -9.75001,-0.96376 0.31432,-35.60003 c 0.17288,-19.58002 0.82532,-36.11103 1.44987,-36.73558 1.81708,-1.81708 35.29533,1.40642 53.50247,5.15156 47.00787,9.66933 86.72265,29.83563 117.9638,59.89942 22.60831,21.7563 38.25173,44.31995 51.33931,74.05042 11.72719,26.64016 21.65262,66.64856 24.10447,97.16279 l 1.00439,12.5 h -35.67264 c -19.61996,0 -36.10123,-0.42858 -36.62503,-0.95239 z"
+ id="path1484"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)" />
+ <path
+ style="opacity:1;vector-effect:none;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:29.34373856;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:58.68747418, 117.37494829;stroke-dashoffset:0;stroke-opacity:1"
+ d="m 823.42333,526.81959 -8.80344,-0.50396 -0.79067,-6.72078 c -0.43486,-3.69645 -1.19383,-10.89938 -1.68659,-16.00652 -4.34718,-45.05596 -23.32007,-100.71198 -47.77185,-140.13638 -22.38739,-36.09589 -54.4142,-68.56278 -89.46856,-90.69793 -41.25297,-26.04928 -95.0629,-43.89344 -148.38973,-49.20819 -9.42857,-0.93969 -17.94643,-1.93722 -18.92857,-2.21675 -1.37004,-0.38992 -1.78572,-9.19669 -1.78572,-37.83338 v -37.32517 l 15.35715,0.93347 c 45.68823,2.77713 80.32407,8.98439 117.5,21.05774 91.16555,29.60718 160.46184,87.77122 202.6182,170.06818 24.63558,48.09321 41.9844,114.7859 45.52317,175.00127 l 0.86054,14.64286 -27.71524,-0.27526 c -15.24339,-0.15139 -31.6768,-0.50203 -36.51869,-0.7792 z"
+ id="path1486"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)" />
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/static/logo_300px.png b/static/logo_300px.png
new file mode 100644
index 0000000..87a4ba4
--- /dev/null
+++ b/static/logo_300px.png
Binary files differ
diff --git a/static/logo_600px.png b/static/logo_600px.png
new file mode 100644
index 0000000..47660dc
--- /dev/null
+++ b/static/logo_600px.png
Binary files differ
diff --git a/static/style.css b/static/style.css
index fa70f2d..974aa78 100644
--- a/static/style.css
+++ b/static/style.css
@@ -43,14 +43,11 @@ header {
color: #1182DB;
}
-header > h1 {
- font-size: 500%;
- font-weight: bold;
-}
-
-header > h2 {
- margin-left: 1em;
- font-size: 200%;
+header > div.logo {
+ background-image: url(logo_600px.png);
+ width: 599px;
+ height: 177px;
+ margin: auto;
}
header > section.warning {
@@ -84,6 +81,12 @@ input[type="number"]:focus {
border-color: #888;
}
+input:focus::-webkit-input-placeholder { opacity: 0; }
+input:focus::-moz-placeholder { opacity: 0; }
+input:focus::placeholder { opacity: 0; }
+input:focus:-moz-placeholder { opacity: 0; }
+input:focus:-ms-input-placeholder { opacity: 0; }
+
.searchbar {
width: 40%;
margin: 40px auto 100px;
@@ -101,13 +104,6 @@ input[type="number"]:focus {
text-align: center;
}
-.searchbar input[type="text"]:focus::-webkit-input-placeholder,
-.searchbar input[type="text"]:focus::-moz-placeholder,
-.searchbar input[type="text"]:focus:-moz-placeholder,
-.searchbar input[type="text"]:focus:-ms-input-placeholder {
- opacity: 0;
-}
-
.searchbar > h3 {
font-size: 200%;
font-weight: bold;
@@ -200,6 +196,7 @@ form {
.parameters label {
text-align: right;
+ line-height: 1.5em;
}
.parameters label::before {
@@ -304,3 +301,60 @@ h5 {
.advice > li {
text-align: left;
}
+
+@media screen and (max-width: 767px) {
+ body {
+ font-size: 75%;
+ }
+
+ header > div.logo {
+ background-image: url(logo_300px.png);
+ width: 300px;
+ height: 89px;
+ }
+
+ header > section.warning {
+ width: 90%;
+ }
+
+ header > section.critical-warning {
+ width: 90%;
+ }
+
+ .searchbar {
+ width: 90%;
+ margin: 0 auto;
+ }
+
+ section {
+ width: 90%;
+ margin: 10px auto;
+ overflow: hidden;
+ }
+
+ button {
+ display: inline-block;
+ width: 40%;
+ padding: 5px auto;
+ margin: 3px auto 0;
+ }
+
+ @supports (display: grid) {
+
+ .parameters {
+ grid-template-columns: auto auto;
+ grid-column-gap: 5px;
+ }
+
+ .parameters label {
+ line-height: 2em;
+ word-break: break-word;
+ }
+
+ } /* @supports (display: grid) */
+
+ .secure-warning {
+ width: 100%;
+ }
+
+} \ No newline at end of file
diff --git a/vendor/simplehtmldom/LICENSE b/vendor/simplehtmldom/LICENSE
new file mode 100644
index 0000000..6040f77
--- /dev/null
+++ b/vendor/simplehtmldom/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 S.C. Chen, John Schlick, logmanoriginal
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+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 OR COPYRIGHT HOLDERS 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. \ No newline at end of file
diff --git a/vendor/simplehtmldom/simple_html_dom.php b/vendor/simplehtmldom/simple_html_dom.php
index 43a1172..d30b018 100644
--- a/vendor/simplehtmldom/simple_html_dom.php
+++ b/vendor/simplehtmldom/simple_html_dom.php
@@ -1,188 +1,140 @@
<?php
/**
* Website: http://sourceforge.net/projects/simplehtmldom/
- * Additional projects that may be used: http://sourceforge.net/projects/debugobject/
+ * Additional projects: 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.
+ * Licensed under The MIT License
+ * See the LICENSE file in the project root for more information.
*
- * 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.
+ * Authors:
+ * S.C. Chen
+ * John Schlick
+ * Rus Carroll
+ * logmanoriginal
*
- * Licensed under The MIT License
- * Redistributions of files must retain the above copyright notice.
+ * Contributors:
+ * Yousuke Kumakura
+ * Vadim Voituk
+ * Antcs
*
- * @author S.C. Chen <me578022@gmail.com>
- * @author John Schlick
- * @author Rus Carroll
- * @version Rev. 1.7 (214)
- * @package PlaceLocalInclude
- * @subpackage simple_html_dom
+ * Version Rev. 1.9 (290)
*/
-/**
- * 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_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);
-
-/** Contents between curly braces "{" and "}" are interpreted as text */
+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);
+
+defined('DEFAULT_TARGET_CHARSET') || define('DEFAULT_TARGET_CHARSET', 'UTF-8');
+defined('DEFAULT_BR_TEXT') || define('DEFAULT_BR_TEXT', "\r\n");
+defined('DEFAULT_SPAN_TEXT') || define('DEFAULT_SPAN_TEXT', ' ');
+defined('MAX_FILE_SIZE') || define('MAX_FILE_SIZE', 600000);
define('HDOM_SMARTY_AS_TEXT', 1);
-// 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 = 0, $maxLen=-1, $lowercase = true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
+function file_get_html(
+ $url,
+ $use_include_path = false,
+ $context = null,
+ $offset = 0,
+ $maxLen = -1,
+ $lowercase = true,
+ $forceTagsClosed = true,
+ $target_charset = DEFAULT_TARGET_CHARSET,
+ $stripRN = true,
+ $defaultBRText = DEFAULT_BR_TEXT,
+ $defaultSpanText = DEFAULT_SPAN_TEXT)
{
- // Ensure maximum length is greater than zero
if($maxLen <= 0) { $maxLen = MAX_FILE_SIZE; }
- // 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 retrieve_url_contents line 2 lines down if it is not already done.
- $contents = file_get_contents($url, $use_include_path, $context, $offset, $maxLen);
- // 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) > $maxLen)
- {
+ $dom = new simple_html_dom(
+ null,
+ $lowercase,
+ $forceTagsClosed,
+ $target_charset,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText
+ );
+
+ /**
+ * For sourceforge users: uncomment the next line and comment the
+ * retrieve_url_contents line 2 lines down if it is not already done.
+ */
+ $contents = file_get_contents(
+ $url,
+ $use_include_path,
+ $context,
+ $offset,
+ $maxLen
+ );
+ // $contents = retrieve_url_contents($url);
+
+ if (empty($contents) || strlen($contents) > $maxLen) {
+ $dom->clear();
return false;
}
- // The second parameter can force the selectors to all be lowercase.
- $dom->load($contents, $lowercase, $stripRN);
- return $dom;
+
+ return $dom->load($contents, $lowercase, $stripRN);
}
-// 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)
+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 = 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;
+
+ return $dom->load($str, $lowercase, $stripRN);
}
-// dump html dom tree
-function dump_html_tree($node, $show_attr=true, $deep=0)
+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
{
- /**
- * Node type
- *
- * Default is {@see HDOM_TYPE_TEXT}
- *
- * @var int
- */
public $nodetype = HDOM_TYPE_TEXT;
-
- /**
- * Tag name
- *
- * Default is 'text'
- *
- * @var string
- */
public $tag = 'text';
-
- /**
- * List of attributes
- *
- * @var array
- */
public $attr = array();
-
- /**
- * List of child node objects
- *
- * @var array
- */
public $children = array();
public $nodes = array();
-
- /**
- * The parent node object
- *
- * @var object|null
- */
public $parent = null;
-
- // The "info" array - see HDOM_INFO_... for what each element contains.
public $_ = array();
-
- /**
- * Start position of the tag in the document
- *
- * @var int
- */
public $tag_start = 0;
-
- /**
- * The DOM object
- *
- * @var object|null
- */
private $dom = null;
- /**
- * Construct new node object
- *
- * Adds itself to the list of DOM Nodes {@see simple_html_dom::$nodes}
- */
function __construct($dom)
{
$this->dom = $dom;
@@ -199,7 +151,6 @@ class simple_html_dom_node
return $this->outertext();
}
- // clean up memory due to php5 circular references memory leak...
function clear()
{
$this->dom = null;
@@ -208,109 +159,86 @@ class simple_html_dom_node
$this->children = null;
}
- // dump node's tree
- function dump($show_attr=true, $deep=0)
+ function dump($show_attr = true, $depth = 0)
{
- $lead = str_repeat(' ', $deep);
+ echo str_repeat("\t", $depth) . $this->tag;
- echo $lead.$this->tag;
- if ($show_attr && count($this->attr)>0)
- {
+ if ($show_attr && count($this->attr) > 0) {
echo '(';
- foreach ($this->attr as $k=>$v)
- echo "[$k]=>\"".$this->$k.'", ';
+ foreach ($this->attr as $k => $v) {
+ echo "[$k]=>\"$v\", ";
+ }
echo ')';
}
+
echo "\n";
- if ($this->nodes)
- {
- foreach ($this->nodes as $c)
- {
- $c->dump($show_attr, $deep+1);
+ if ($this->nodes) {
+ foreach ($this->nodes as $node) {
+ $node->dump($show_attr, $depth + 1);
}
}
}
-
- // Debugging function to dump a single dom node with a bunch of information about it.
- function dump_node($echo=true)
+ function dump_node($echo = true)
{
-
$string = $this->tag;
- if (count($this->attr)>0)
- {
+
+ if (count($this->attr) > 0) {
$string .= '(';
- foreach ($this->attr as $k=>$v)
- {
- $string .= "[$k]=>\"".$this->$k.'", ';
+ foreach ($this->attr as $k => $v) {
+ $string .= "[$k]=>\"$v\", ";
}
$string .= ')';
}
- if (count($this->_)>0)
- {
+
+ if (count($this->_) > 0) {
$string .= ' $_ (';
- foreach ($this->_ as $k=>$v)
- {
- if (is_array($v))
- {
+ foreach ($this->_ as $k => $v) {
+ if (is_array($v)) {
$string .= "[$k]=>(";
- foreach ($v as $k2=>$v2)
- {
- $string .= "[$k2]=>\"".$v2.'", ';
+ foreach ($v as $k2 => $v2) {
+ $string .= "[$k2]=>\"$v2\", ";
}
- $string .= ")";
+ $string .= ')';
} else {
- $string .= "[$k]=>\"".$v.'", ';
+ $string .= "[$k]=>\"$v\", ";
}
}
- $string .= ")";
+ $string .= ')';
}
- if (isset($this->text))
- {
- $string .= " text: (" . $this->text . ")";
+ 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 .= ' 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 .= ' children: ' . count($this->children);
+ $string .= ' nodes: ' . count($this->nodes);
+ $string .= ' tag_start: ' . $this->tag_start;
$string .= "\n";
- if ($echo)
- {
+ if ($echo) {
echo $string;
return;
- }
- else
- {
+ } else {
return $string;
}
}
- /**
- * Return or set parent node
- *
- * @param object|null $parent (optional) The parent node, `null` to return
- * the current parent node.
- * @return object|null The parent node
- */
- function parent($parent=null)
+ 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)
- {
+ // 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;
@@ -319,272 +247,214 @@ class simple_html_dom_node
return $this->parent;
}
- /**
- * @return bool True if the node has at least one child node
- */
function has_child()
{
return !empty($this->children);
}
- /**
- * Get child node at specified index
- *
- * @param int $idx The index of the child node to return, `-1` to return all
- * child nodes.
- * @return object|array|null The child node at the specified index, all child
- * nodes or null if the index is invalid.
- */
- function children($idx=-1)
+ function children($idx = -1)
{
- if ($idx===-1)
- {
+ if ($idx === -1) {
return $this->children;
}
- if (isset($this->children[$idx]))
- {
+
+ if (isset($this->children[$idx])) {
return $this->children[$idx];
}
+
return null;
}
- /**
- * Get first child node
- *
- * @return object|null The first child node or null if the current node has
- * no child nodes.
- *
- * @todo Use `empty()` instead of `count()` to improve performance on large
- * arrays.
- */
function first_child()
{
- if (count($this->children)>0)
- {
+ if (count($this->children) > 0) {
return $this->children[0];
}
return null;
}
- /**
- * Get last child node
- *
- * @return object|null The last child node or null if the current node has
- * no child nodes.
- *
- * @todo Use `end()` to slightly improve performance on large arrays.
- */
function last_child()
{
- if (($count=count($this->children))>0)
- {
- return $this->children[$count-1];
+ if (count($this->children) > 0) {
+ return end($this->children);
}
return null;
}
- /**
- * Get next sibling node
- *
- * @return object|null The sibling node or null if the current node has no
- * sibling nodes.
- */
function next_sibling()
{
- if ($this->parent===null)
- {
+ 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;
+ $idx = array_search($this, $this->parent->children, true);
+
+ if ($idx !== false && isset($this->parent->children[$idx + 1])) {
+ return $this->parent->children[$idx + 1];
}
- return $this->parent->children[$idx];
+
+ return null;
}
- /**
- * Get previous sibling node
- *
- * @return object|null The sibling node or null if the current node has no
- * sibling nodes.
- */
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];
+ if ($this->parent === null) {
+ return null;
+ }
+
+ $idx = array_search($this, $this->parent->children, true);
+
+ if ($idx !== false && $idx > 0) {
+ return $this->parent->children[$idx - 1];
+ }
+
+ return null;
}
- /**
- * Traverse ancestors to the first matching tag.
- *
- * @param string $tag Tag to find
- * @return object|null First matching node in the DOM tree or null if no
- * match was found.
- *
- * @todo Null is returned implicitly by calling ->parent on the root node.
- * This behaviour could change at any time, rendering this function invalid.
- */
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;
+ if ($this->parent === null) {
+ return null;
+ }
+
+ $ancestor = $this->parent;
- while (!is_null($returnDom))
- {
- if (is_object($debug_object)) { $debug_object->debug_log(2, "Current tag is: " . $returnDom->tag); }
+ while (!is_null($ancestor)) {
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(2, 'Current tag is: ' . $ancestor->tag);
+ }
- if ($returnDom->tag == $tag)
- {
+ if ($ancestor->tag === $tag) {
break;
}
- $returnDom = $returnDom->parent;
+
+ $ancestor = $ancestor->parent;
}
- return $returnDom;
+
+ return $ancestor;
}
- /**
- * Get node's inner text (everything inside the opening and closing tags)
- *
- * @return string
- */
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]);
+ 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)
+
+ foreach ($this->nodes as $n) {
$ret .= $n->outertext();
+ }
+
return $ret;
}
- /**
- * Get node's outer text (everything including the opening and closing tags)
- *
- * @return string
- */
function outertext()
{
global $debug_object;
- if (is_object($debug_object))
- {
+
+ if (is_object($debug_object)) {
$text = '';
- if ($this->tag == 'text')
- {
- if (!empty($this->text))
- {
- $text = " with text: " . $this->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();
+ if ($this->tag === 'root') {
+ return $this->innertext();
+ }
- // trigger callback
- if ($this->dom && $this->dom->callback!==null)
- {
+ // todo: What is the use of this callback? Remove?
+ 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]);
+ if (isset($this->_[HDOM_INFO_OUTER])) {
+ return $this->_[HDOM_INFO_OUTER];
+ }
- // render begin tag
- if ($this->dom && $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]])
- {
+ if (isset($this->_[HDOM_INFO_TEXT])) {
+ return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
+ }
+
+ $ret = '';
+
+ 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")
- {
+ if (isset($this->_[HDOM_INFO_INNER])) {
+ // todo: <br> should either never have HDOM_INFO_INNER or always
+ if ($this->tag !== 'br') {
$ret .= $this->_[HDOM_INFO_INNER];
}
- } else {
- if ($this->nodes)
- {
- foreach ($this->nodes as $n)
- {
- $ret .= $this->convert_text($n->outertext());
- }
+ } elseif ($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.'>';
+ if (isset($this->_[HDOM_INFO_END]) && $this->_[HDOM_INFO_END] != 0) {
+ $ret .= '</' . $this->tag . '>';
+ }
+
return $ret;
}
- /**
- * Get node's plain text (everything excluding all tags)
- *
- * @return string
- */
function text()
{
- if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER];
- switch ($this->nodetype)
- {
+ 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 '';
+
+ 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.
+
+ // 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)
- {
+ if (!is_null($this->nodes)) {
+ foreach ($this->nodes as $n) {
// Start paragraph after a blank line
- if ($n->tag == 'p')
- {
- $ret .= "\n\n";
+ if ($n->tag === 'p') {
+ $ret = trim($ret) . "\n\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 ($n->tag == "span")
- {
+ // 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 ($n->tag === 'span') {
$ret .= $this->dom->default_span_text;
}
}
}
- return trim($ret);
+ return $ret;
}
- /**
- * Get node's xml text (inner text as a CDATA section)
- *
- * @return string
- */
function xmltext()
{
$ret = $this->innertext();
@@ -593,76 +463,82 @@ class simple_html_dom_node
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]);
+ if (isset($this->_[HDOM_INFO_TEXT])) {
+ return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
+ }
- $ret = '<'.$this->tag;
+ $ret = '<' . $this->tag;
$i = -1;
- foreach ($this->attr as $key=>$val)
- {
+ foreach ($this->attr as $key => $val) {
++$i;
// skip removed attribute
- if ($val===null || $val===false)
- continue;
+ if ($val === null || $val === false) { continue; }
$ret .= $this->_[HDOM_INFO_SPACE][$i][0];
+
//no value attr: nowrap, checked selected...
- if ($val===true)
+ if ($val === true) {
$ret .= $key;
- else {
+ } 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 .= $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)
+ function find($selector, $idx = null, $lowercase = false)
{
$selectors = $this->parse_selector($selector);
- if (($count=count($selectors))===0) return array();
+ 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
+ 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();
+ if (($levle = count($selectors[$c])) === 0) { return array(); }
+ if (!isset($this->_[HDOM_INFO_BEGIN])) { return array(); }
- $head = array($this->_[HDOM_INFO_BEGIN]=>1);
+ $head = array($this->_[HDOM_INFO_BEGIN] => 1);
+ $cmd = ' '; // Combinator
// handle descendant selectors, no recursive!
- for ($l=0; $l<$levle; ++$l)
- {
+ for ($l = 0; $l < $levle; ++$l) {
$ret = array();
- foreach ($head as $k=>$v)
- {
- $n = ($k===-1) ? $this->dom->root : $this->dom->nodes[$k];
+
+ 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);
+ $n->seek($selectors[$c][$l], $ret, $cmd, $lowercase);
}
+
$head = $ret;
+ $cmd = $selectors[$c][$l][4]; // Next Combinator
}
- foreach ($head as $k=>$v)
- {
- if (!isset($found_keys[$k]))
- {
+ foreach ($head as $k => $v) {
+ if (!isset($found_keys[$k])) {
$found_keys[$k] = 1;
}
}
@@ -672,192 +548,421 @@ class simple_html_dom_node
ksort($found_keys);
$found = array();
- foreach ($found_keys as $k=>$v)
+ 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;
+ if (is_null($idx)) { return $found; }
+ elseif ($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)
+ protected function seek($selector, &$ret, $parent_cmd, $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;
- }
+ list($tag, $id, $class, $attributes, $cmb) = $selector;
+ $nodes = array();
+
+ if ($parent_cmd === ' ') { // Descendant Combinator
+ // Find parent closing tag if the current element doesn't have a closing
+ // tag (i.e. void element)
+ $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];
}
- 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;
+ // Get list of target nodes
+ $nodes_start = $this->_[HDOM_INFO_BEGIN] + 1;
+ $nodes_count = $end - $nodes_start;
+ $nodes = array_slice($this->dom->nodes, $nodes_start, $nodes_count, true);
+ } elseif ($parent_cmd === '>') { // Child Combinator
+ $nodes = $this->children;
+ } elseif ($parent_cmd === '+'
+ && $this->parent
+ && in_array($this, $this->parent->children)) { // Next-Sibling Combinator
+ $index = array_search($this, $this->parent->children, true) + 1;
+ if ($index < count($this->parent->children))
+ $nodes[] = $this->parent->children[$index];
+ } elseif ($parent_cmd === '~'
+ && $this->parent
+ && in_array($this, $this->parent->children)) { // Subsequent Sibling Combinator
+ $index = array_search($this, $this->parent->children, true);
+ $nodes = array_slice($this->parent->children, $index);
+ }
+
+ // Go throgh each element starting at this element until the end tag
+ // Note: If this element is a void tag, any previous void element is
+ // skipped.
+ foreach($nodes as $node) {
+ $pass = true;
+
+ // Skip root nodes
+ if(!$node->parent) {
+ $pass = false;
}
- $end += $parent->_[HDOM_INFO_END];
- }
- for ($i=$this->_[HDOM_INFO_BEGIN]+1; $i<$end; ++$i) {
- $node = $this->dom->nodes[$i];
+ // Skip if node isn't a child node (i.e. text nodes)
+ if($pass && !in_array($node, $node->parent->children, true)) {
+ $pass = false;
+ }
- $pass = true;
+ // Skip if tag doesn't match
+ if ($pass && $tag !== '' && $tag !== $node->tag && $tag !== '*') {
+ $pass = false;
+ }
- if ($tag==='*' && !$key) {
- if (in_array($node, $this->children, true))
- $ret[$i] = 1;
- continue;
+ // Skip if ID doesn't exist
+ if ($pass && $id !== '' && !isset($node->attr['id'])) {
+ $pass = false;
}
- // 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;
- }
+ // Check if ID matches
+ if ($pass && $id !== '' && isset($node->attr['id'])) {
+ // Note: Only consider the first ID (as browsers do)
+ $node_id = explode(' ', trim($node->attr['id']))[0];
+
+ if($id !== $node_id) { $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));
+ // Check if all class(es) exist
+ if ($pass && $class !== '' && is_array($class) && !empty($class)) {
+ if (isset($node->attr['class'])) {
+ $node_classes = explode(' ', $node->attr['class']);
+
+ if ($lowercase) {
+ $node_classes = array_map('strtolower', $node_classes);
+ }
+
+ foreach($class as $c) {
+ if(!in_array($c, $node_classes)) {
+ $pass = false;
+ break;
+ }
+ }
} else {
- $check = $this->match($exp, $val, $nodeKeyValue);
+ $pass = false;
}
- 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);
+ }
+
+ // Check attributes
+ if ($pass
+ && $attributes !== ''
+ && is_array($attributes)
+ && !empty($attributes)) {
+ foreach($attributes as $a) {
+ list (
+ $att_name,
+ $att_expr,
+ $att_val,
+ $att_inv,
+ $att_case_sensitivity
+ ) = $a;
+
+ // Handle indexing attributes (i.e. "[2]")
+ /**
+ * Note: This is not supported by the CSS Standard but adds
+ * the ability to select items compatible to XPath (i.e.
+ * the 3rd element within it's parent).
+ *
+ * Note: This doesn't conflict with the CSS Standard which
+ * doesn't work on numeric attributes anyway.
+ */
+ if (is_numeric($att_name)
+ && $att_expr === ''
+ && $att_val === '') {
+ $count = 0;
+
+ // Find index of current element in parent
+ foreach ($node->parent->children as $c) {
+ if ($c->tag === $node->tag) ++$count;
+ if ($c === $node) break;
+ }
+
+ // If this is the correct node, continue with next
+ // attribute
+ if ($count === (int)$att_name) continue;
+ }
+
+ // Check attribute availability
+ if ($att_inv) { // Attribute should NOT be set
+ if (isset($node->attr[$att_name])) {
+ $pass = false;
+ break;
+ }
+ } else { // Attribute should be set
+ // todo: "plaintext" is not a valid CSS selector!
+ if ($att_name !== 'plaintext'
+ && !isset($node->attr[$att_name])) {
+ $pass = false;
+ break;
}
- if ($check) break;
+ }
+
+ // Continue with next attribute if expression isn't defined
+ if ($att_expr === '') continue;
+
+ // If they have told us that this is a "plaintext"
+ // search then we want the plaintext of the node - right?
+ // todo "plaintext" is not a valid CSS selector!
+ if ($att_name === 'plaintext') {
+ $nodeKeyValue = $node->text();
+ } else {
+ $nodeKeyValue = $node->attr[$att_name];
+ }
+
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(2,
+ 'testing node: '
+ . $node->tag
+ . ' for attribute: '
+ . $att_name
+ . $att_expr
+ . $att_val
+ . ' where nodes value is: '
+ . $nodeKeyValue
+ );
+ }
+
+ // If lowercase is set, do a case insensitive test of
+ // the value of the selector.
+ if ($lowercase) {
+ $check = $this->match(
+ $att_expr,
+ strtolower($att_val),
+ strtolower($nodeKeyValue),
+ $att_case_sensitivity
+ );
+ } else {
+ $check = $this->match(
+ $att_expr,
+ $att_val,
+ $nodeKeyValue,
+ $att_case_sensitivity
+ );
+ }
+
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(2,
+ 'after match: '
+ . ($check ? 'true' : 'false')
+ );
+ }
+
+ if (!$check) {
+ $pass = false;
+ break;
}
}
- }
- if (!$check) $pass = false;
}
- if ($pass) $ret[$i] = 1;
+
+ // Found a match. Add to list and clear node
+ if ($pass) $ret[$node->_[HDOM_INFO_BEGIN]] = 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);}
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(1, 'EXIT - ret: ', $ret);
+ }
}
- protected function match($exp, $pattern, $value) {
+ protected function match($exp, $pattern, $value, $case_sensitivity)
+ {
global $debug_object;
if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
+ if ($case_sensitivity === 'i') {
+ $pattern = strtolower($pattern);
+ $value = strtolower($value);
+ }
+
switch ($exp) {
case '=':
- return ($value===$pattern);
+ return ($value === $pattern);
case '!=':
- return ($value!==$pattern);
+ return ($value !== $pattern);
case '^=':
- return preg_match("/^".preg_quote($pattern,'/')."/", $value);
+ return preg_match('/^' . preg_quote($pattern, '/') . '/', $value);
case '$=':
- return preg_match("/".preg_quote($pattern,'/')."$/", $value);
+ return preg_match('/' . preg_quote($pattern, '/') . '$/', $value);
case '*=':
- if ($pattern[0]=='/') {
- return preg_match($pattern, $value);
- }
- return preg_match("/".$pattern."/i", $value);
+ return preg_match('/' . preg_quote($pattern, '/') . '/', $value);
+ case '|=':
+ /**
+ * [att|=val]
+ *
+ * Represents an element with the att attribute, its value
+ * either being exactly "val" or beginning with "val"
+ * immediately followed by "-" (U+002D).
+ */
+ return strpos($value, $pattern) === 0;
+ case '~=':
+ /**
+ * [att~=val]
+ *
+ * Represents an element with the att attribute whose value is a
+ * whitespace-separated list of words, one of which is exactly
+ * "val". If "val" contains whitespace, it will never represent
+ * anything (since the words are separated by spaces). Also if
+ * "val" is the empty string, it will never represent anything.
+ */
+ return in_array($pattern, explode(' ', trim($value)), true);
}
return false;
}
- protected function parse_selector($selector_string) {
+ protected function parse_selector($selector_string)
+ {
global $debug_object;
- if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
+ 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);}
+ /**
+ * Pattern of CSS selectors, modified from mootools (https://mootools.net/)
+ *
+ * Paperg: Add the colon to the attribute, so that it properly finds
+ * <tag attr:ibute="something" > like google does.
+ *
+ * Note: if you try to look at this attribute, you MUST use getAttribute
+ * since $dom->x:y will fail the php syntax check.
+ *
+ * Notice the \[ starting the attribute? 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.
+ *
+ * Matches selectors in this order:
+ *
+ * [0] - full match
+ *
+ * [1] - tag name
+ * ([\w:\*-]*)
+ * Matches the tag name consisting of zero or more words, colons,
+ * asterisks and hyphens.
+ *
+ * [2] - id name
+ * (?:\#([\w-]+))
+ * Optionally matches a id name, consisting of an "#" followed by
+ * the id name (one or more words and hyphens).
+ *
+ * [3] - class names (including dots)
+ * (?:\.([\w\.-]+))?
+ * Optionally matches a list of classs, consisting of an "."
+ * followed by the class name (one or more words and hyphens)
+ * where multiple classes can be chained (i.e. ".foo.bar.baz")
+ *
+ * [4] - attributes
+ * ((?:\[@?(?:!?[\w:-]+)(?:(?:[!*^$|~]?=)[\"']?(?:.*?)[\"']?)?(?:\s*?(?:[iIsS])?)?\])+)?
+ * Optionally matches the attributes list
+ *
+ * [5] - separator
+ * ([\/, >+~]+)
+ * Matches the selector list separator
+ */
+ // phpcs:ignore Generic.Files.LineLength
+ $pattern = "/([\w:\*-]*)(?:\#([\w-]+))?(?:|\.([\w\.-]+))?((?:\[@?(?:!?[\w:-]+)(?:(?:[!*^$|~]?=)[\"']?(?:.*?)[\"']?)?(?:\s*?(?:[iIsS])?)?\])+)?([\/, >+~]+)/is";
+
+ preg_match_all(
+ $pattern,
+ trim($selector_string) . ' ', // Add final ' ' as pseudo separator
+ $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])===',') {
+
+ // Skip NoOps
+ if ($m[0] === '' || $m[0] === '/' || $m[0] === '//') { continue; }
+
+ // Convert to lowercase
+ if ($this->dom->lowercase) {
+ $m[1] = strtolower($m[1]);
+ }
+
+ // Extract classes
+ if ($m[3] !== '') { $m[3] = explode('.', $m[3]); }
+
+ /* Extract attributes (pattern based on the pattern above!)
+
+ * [0] - full match
+ * [1] - attribute name
+ * [2] - attribute expression
+ * [3] - attribute value
+ * [4] - case sensitivity
+ *
+ * Note: Attributes can be negated with a "!" prefix to their name
+ */
+ if($m[4] !== '') {
+ preg_match_all(
+ "/\[@?(!?[\w:-]+)(?:([!*^$|~]?=)[\"']?(.*?)[\"']?)?(?:\s+?([iIsS])?)?\]/is",
+ trim($m[4]),
+ $attributes,
+ PREG_SET_ORDER
+ );
+
+ // Replace element by array
+ $m[4] = array();
+
+ foreach($attributes as $att) {
+ // Skip empty matches
+ if(trim($att[0]) === '') { continue; }
+
+ $inverted = (isset($att[1][0]) && $att[1][0] === '!');
+ $m[4][] = array(
+ $inverted ? substr($att[1], 1) : $att[1], // Name
+ (isset($att[2])) ? $att[2] : '', // Expression
+ (isset($att[3])) ? $att[3] : '', // Value
+ $inverted, // Inverted Flag
+ (isset($att[4])) ? strtolower($att[4]) : '', // Case-Sensitivity
+ );
+ }
+ }
+
+ // Sanitize Separator
+ if ($m[5] !== '' && trim($m[5]) === '') { // Descendant Separator
+ $m[5] = ' ';
+ } else { // Other Separator
+ $m[5] = trim($m[5]);
+ }
+
+ // Clear Separator if it's a Selector List
+ if ($is_list = ($m[5] === ',')) { $m[5] = ''; }
+
+ // Remove full match before adding to results
+ array_shift($m);
+ $result[] = $m;
+
+ if ($is_list) { // Selector List
$selectors[] = $result;
$result = array();
}
}
- if (count($result)>0)
- $selectors[] = $result;
+
+ if (count($result) > 0) { $selectors[] = $result; }
return $selectors;
}
function __get($name)
{
- if (isset($this->attr[$name]))
- {
+ if (isset($this->attr[$name])) {
return $this->convert_text($this->attr[$name]);
}
- switch ($name)
- {
+ switch ($name) {
case 'outertext': return $this->outertext();
case 'innertext': return $this->innertext();
case 'plaintext': return $this->text();
@@ -869,27 +974,28 @@ class simple_html_dom_node
function __set($name, $value)
{
global $debug_object;
- if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
- switch ($name)
- {
+ switch ($name) {
case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value;
case 'innertext':
- if (isset($this->_[HDOM_INFO_TEXT])) return $this->_[HDOM_INFO_TEXT] = $value;
+ if (isset($this->_[HDOM_INFO_TEXT])) {
+ return $this->_[HDOM_INFO_TEXT] = $value;
+ }
return $this->_[HDOM_INFO_INNER] = $value;
}
- if (!isset($this->attr[$name]))
- {
+
+ 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)
- {
+ switch ($name) {
case 'outertext': return true;
case 'innertext': return true;
case 'plaintext': return true;
@@ -898,51 +1004,54 @@ class simple_html_dom_node
return (array_key_exists($name, $this->attr)) ? true : isset($this->attr[$name]);
}
- function __unset($name) {
- if (isset($this->attr[$name]))
- unset($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);}
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
$converted_text = $text;
- $sourceCharset = "";
- $targetCharset = "";
+ $sourceCharset = '';
+ $targetCharset = '';
- if ($this->dom)
- {
+ 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))
- {
+ 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)))
- {
+ if ((strcasecmp($targetCharset, 'UTF-8') == 0)
+ && ($this->is_utf8($text))) {
$converted_text = $text;
- }
- else
- {
+ } 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")
- {
+ 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")
- {
+
+ if (substr($converted_text, -3) === "\xef\xbb\xbf") {
$converted_text = substr($converted_text, 0, -3);
}
}
@@ -950,57 +1059,33 @@ class simple_html_dom_node
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)
- {
+ $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;
+ $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;
@@ -1008,57 +1093,54 @@ class simple_html_dom_node
$width = -1;
$height = -1;
- if ($this->tag !== 'img')
- {
+ if ($this->tag !== 'img') {
return false;
}
// See if there is aheight or width attribute in the tag itself.
- if (isset($this->attr['width']))
- {
+ if (isset($this->attr['width'])) {
$width = $this->attr['width'];
}
- if (isset($this->attr['height']))
- {
+ if (isset($this->attr['height'])) {
$height = $this->attr['height'];
}
// Now look for an inline style.
- if (isset($this->attr['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);
+
+ preg_match_all(
+ '/([\w-]+)\s*:\s*([^;]+)\s*;?/',
+ $this->attr['style'],
+ $matches,
+ PREG_SET_ORDER
+ );
+
foreach ($matches as $match) {
- $attributes[$match[1]] = $match[2];
+ $attributes[$match[1]] = $match[2];
}
// If there is a width in the style attributes:
- if (isset($attributes['width']) && $width == -1)
- {
+ if (isset($attributes['width']) && $width == -1) {
// check that the last two characters are px (pixels)
- if (strtolower(substr($attributes['width'], -2)) == 'px')
- {
+ 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))
- {
+ 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)
- {
+ if (isset($attributes['height']) && $height == -1) {
// check that the last two characters are px (pixels)
- if (strtolower(substr($attributes['height'], -2)) == 'px')
- {
+ 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))
- {
+ if (filter_var($proposed_height, FILTER_VALIDATE_INT)) {
$height = $proposed_height;
}
}
@@ -1067,324 +1149,337 @@ class simple_html_dom_node
}
// 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.
+ // 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.
+ // 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.
+ // 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
+ );
- $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;}
+ function save($filepath = '')
+ {
+ $ret = $this->outertext();
+
+ if ($filepath !== '') {
+ file_put_contents($filepath, $ret, LOCK_EX);
+ }
+
+ return $ret;
+ }
+
+ function addClass($class)
+ {
+ if (is_string($class)) {
+ $class = explode(' ', $class);
+ }
+
+ if (is_array($class)) {
+ foreach($class as $c) {
+ if (isset($this->class)) {
+ if ($this->hasClass($c)) {
+ continue;
+ } else {
+ $this->class .= ' ' . $c;
+ }
+ } else {
+ $this->class = $c;
+ }
+ }
+ } else {
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(2, 'Invalid type: ', gettype($class));
+ }
+ }
+ }
+
+ function hasClass($class)
+ {
+ if (is_string($class)) {
+ if (isset($this->class)) {
+ return in_array($class, explode(' ', $this->class), true);
+ }
+ } else {
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(2, 'Invalid type: ', gettype($class));
+ }
+ }
+
+ return false;
+ }
+
+ function removeClass($class = null)
+ {
+ if (!isset($this->class)) {
+ return;
+ }
+
+ if (is_null($class)) {
+ $this->removeAttribute('class');
+ return;
+ }
+
+ if (is_string($class)) {
+ $class = explode(' ', $class);
+ }
+
+ if (is_array($class)) {
+ $class = array_diff(explode(' ', $this->class), $class);
+ if (empty($class)) {
+ $this->removeAttribute('class');
+ } else {
+ $this->class = implode(' ', $class);
+ }
+ }
+ }
+
+ 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 remove()
+ {
+ if ($this->parent) {
+ $this->parent->removeChild($this);
+ }
+ }
+
+ function removeChild($node)
+ {
+ $nidx = array_search($node, $this->nodes, true);
+ $cidx = array_search($node, $this->children, true);
+ $didx = array_search($node, $this->dom->nodes, true);
+
+ if ($nidx !== false && $cidx !== false && $didx !== false) {
+
+ foreach($node->children as $child) {
+ $node->removeChild($child);
+ }
+
+ foreach($node->nodes as $entity) {
+ $enidx = array_search($entity, $node->nodes, true);
+ $edidx = array_search($entity, $node->dom->nodes, true);
+
+ if ($enidx !== false && $edidx !== false) {
+ unset($node->nodes[$enidx]);
+ unset($node->dom->nodes[$edidx]);
+ }
+ }
+
+ unset($this->nodes[$nidx]);
+ unset($this->children[$cidx]);
+ unset($this->dom->nodes[$didx]);
+
+ $node->clear();
+
+ }
+ }
+
+ 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
{
- /**
- * The root node of the document
- *
- * @var object
- */
public $root = null;
-
- /**
- * List of nodes in the current DOM
- *
- * @var array
- */
public $nodes = array();
-
- /**
- * Callback function to run for each element in the DOM.
- *
- * @var callable|null
- */
public $callback = null;
-
- /**
- * Indicates how tags and attributes are matched
- *
- * @var bool When set to **true** tags and attributes will be converted to
- * lowercase before matching.
- */
public $lowercase = false;
-
- /**
- * Original document size
- *
- * Holds the original document size.
- *
- * @var int
- */
public $original_size;
-
- /**
- * Current document size
- *
- * Holds the current document size. The document size is determined by the
- * string length of ({@see simple_html_dom::$doc}).
- *
- * _Note_: Using this variable is more efficient than calling `strlen($doc)`
- *
- * @var int
- * */
public $size;
- /**
- * Current position in the document
- *
- * @var int
- */
protected $pos;
-
- /**
- * The document
- *
- * @var string
- */
protected $doc;
-
- /**
- * Current character
- *
- * Holds the current character at position {@see simple_html_dom::$pos} in
- * the document {@see simple_html_dom::$doc}
- *
- * _Note_: Using this variable is more efficient than calling `substr($doc, $pos, 1)`
- *
- * @var string
- */
protected $char;
protected $cursor;
-
- /**
- * Parent node of the next node detected by the parser
- *
- * @var object
- */
protected $parent;
protected $noise = array();
-
- /**
- * Tokens considered blank in HTML
- *
- * @var string
- */
protected $token_blank = " \t\r\n";
-
- /**
- * Tokens to identify the equal sign for attributes, stopping either at the
- * closing tag ("/" i.e. "<html />") or the end of an opening tag (">" i.e.
- * "<html>")
- *
- * @var string
- */
protected $token_equal = ' =/>';
-
- /**
- * Tokens to identify the end of a tag name. A tag name either ends on the
- * ending slash ("/" i.e. "<html/>") or whitespace ("\s\r\n\t")
- *
- * @var string
- */
protected $token_slash = " />\r\n\t";
-
- /**
- * Tokens to identify the end of an attribute
- *
- * @var string
- */
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 = '';
- /**
- * Innertext for <br> elements
- *
- * @var string
- */
- protected $default_br_text = "";
+ protected $default_br_text = '';
- /**
- * Suffix for <span> elements
- *
- * @var string
- */
- public $default_span_text = "";
+ public $default_span_text = '';
- /**
- * Defines a list of self-closing tags (Void elements) according to the HTML
- * Specification
- *
- * _Remarks_:
- * - Use `isset()` instead of `in_array()` on array elements to boost
- * performance about 30%
- * - Sort elements by name for better readability!
- *
- * @link https://www.w3.org/TR/html HTML Specification
- * @link https://www.w3.org/TR/html/syntax.html#void-elements Void elements
- */
protected $self_closing_tags = array(
- 'area'=>1,
- 'base'=>1,
- 'br'=>1,
- 'col'=>1,
- 'embed'=>1,
- 'hr'=>1,
- 'img'=>1,
- 'input'=>1,
- 'link'=>1,
- 'meta'=>1,
- 'param'=>1,
- 'source'=>1,
- 'track'=>1,
- 'wbr'=>1
+ 'area' => 1,
+ 'base' => 1,
+ 'br' => 1,
+ 'col' => 1,
+ 'embed' => 1,
+ 'hr' => 1,
+ 'img' => 1,
+ 'input' => 1,
+ 'link' => 1,
+ 'meta' => 1,
+ 'param' => 1,
+ 'source' => 1,
+ 'track' => 1,
+ 'wbr' => 1
);
-
- /**
- * Defines a list of tags which - if closed - close all optional closing
- * elements within if they haven't been closed yet. (So, an element where
- * neither opening nor closing tag is omissible consistently closes every
- * optional closing element within)
- *
- * _Remarks_:
- * - Use `isset()` instead of `in_array()` on array elements to boost
- * performance about 30%
- * - Sort elements by name for better readability!
- */
protected $block_tags = array(
- 'body'=>1,
- 'div'=>1,
- 'form'=>1,
- 'root'=>1,
- 'span'=>1,
- 'table'=>1
+ 'body' => 1,
+ 'div' => 1,
+ 'form' => 1,
+ 'root' => 1,
+ 'span' => 1,
+ 'table' => 1
);
-
- /**
- * Defines elements whose end tag is omissible.
- *
- * * key = Name of an element whose end tag is omissible.
- * * value = Names of elements whose end tag is omissible, that are closed
- * by the current element.
- *
- * _Remarks_:
- * - Use `isset()` instead of `in_array()` on array elements to boost
- * performance about 30%
- * - Sort elements by name for better readability!
- *
- * **Example**
- *
- * An `li` element’s end tag may be omitted if the `li` element is immediately
- * followed by another `li` element. To do that, add following element to the
- * array:
- *
- * ```php
- * 'li' => array('li'),
- * ```
- *
- * With this, the following two examples are considered equal. Note that the
- * second example is missing the closing tags on `li` elements.
- *
- * ```html
- * <ul><li>First Item</li><li>Second Item</li></ul>
- * ```
- *
- * <ul><li>First Item</li><li>Second Item</li></ul>
- *
- * ```html
- * <ul><li>First Item<li>Second Item</ul>
- * ```
- *
- * <ul><li>First Item<li>Second Item</ul>
- *
- * @var array A two-dimensional array where the key is the name of an
- * element whose end tag is omissible and the value is an array of elements
- * whose end tag is omissible, that are closed by the current element.
- *
- * @link https://www.w3.org/TR/html/syntax.html#optional-tags Optional tags
- *
- * @todo The implementation of optional closing tags doesn't work in all cases
- * because it only consideres elements who close other optional closing
- * tags, not taking into account that some (non-blocking) tags should close
- * these optional closing tags. For example, the end tag for "p" is omissible
- * and can be closed by an "address" element, whose end tag is NOT omissible.
- * Currently a "p" element without closing tag stops at the next "p" element
- * or blocking tag, even if it contains other elements.
- *
- * @todo 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(
- 'b'=>array('b'=>1), // Not optional, see https://www.w3.org/TR/html/textlevel-semantics.html#the-b-element
- 'dd'=>array('dd'=>1, 'dt'=>1),
- 'dl'=>array('dd'=>1, 'dt'=>1), // Not optional, see https://www.w3.org/TR/html/grouping-content.html#the-dl-element
- 'dt'=>array('dd'=>1, 'dt'=>1),
- 'li'=>array('li'=>1),
- 'optgroup'=>array('optgroup'=>1, 'option'=>1),
- 'option'=>array('optgroup'=>1, 'option'=>1),
- 'p'=>array('p'=>1),
- 'rp'=>array('rp'=>1, 'rt'=>1),
- 'rt'=>array('rp'=>1, 'rt'=>1),
- 'td'=>array('td'=>1, 'th'=>1),
- 'th'=>array('td'=>1, 'th'=>1),
- 'tr'=>array('td'=>1, 'th'=>1, 'tr'=>1),
+ // Not optional, see
+ // https://www.w3.org/TR/html/textlevel-semantics.html#the-b-element
+ 'b' => array('b' => 1),
+ 'dd' => array('dd' => 1, 'dt' => 1),
+ // Not optional, see
+ // https://www.w3.org/TR/html/grouping-content.html#the-dl-element
+ 'dl' => array('dd' => 1, 'dt' => 1),
+ 'dt' => array('dd' => 1, 'dt' => 1),
+ 'li' => array('li' => 1),
+ 'optgroup' => array('optgroup' => 1, 'option' => 1),
+ 'option' => array('optgroup' => 1, 'option' => 1),
+ 'p' => array('p' => 1),
+ 'rp' => array('rp' => 1, 'rt' => 1),
+ 'rt' => array('rp' => 1, 'rt' => 1),
+ 'td' => array('td' => 1, 'th' => 1),
+ 'th' => array('td' => 1, 'th' => 1),
+ 'tr' => array('td' => 1, 'th' => 1, 'tr' => 1),
);
- function __construct($str=null, $lowercase=true, $forceTagsClosed=true, $target_charset=DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT, $options=0)
+ function __construct(
+ $str = null,
+ $lowercase = true,
+ $forceTagsClosed = true,
+ $target_charset = DEFAULT_TARGET_CHARSET,
+ $stripRN = true,
+ $defaultBRText = DEFAULT_BR_TEXT,
+ $defaultSpanText = DEFAULT_SPAN_TEXT,
+ $options = 0)
{
- if ($str)
- {
- if (preg_match("/^http:\/\//i",$str) || is_file($str))
- {
+ if ($str) {
+ if (preg_match('/^http:\/\//i', $str) || is_file($str)) {
$this->load_file($str);
- }
- else
- {
- $this->load($str, $lowercase, $stripRN, $defaultBRText, $defaultSpanText, $options);
+ } else {
+ $this->load(
+ $str,
+ $lowercase,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText,
+ $options
+ );
}
}
- // 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.
+ // 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->optional_closing_array = array();
}
+
$this->_target_charset = $target_charset;
}
@@ -1393,8 +1488,13 @@ class simple_html_dom
$this->clear();
}
- // load html from string
- function load($str, $lowercase=true, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT, $options=0)
+ function load(
+ $str,
+ $lowercase = true,
+ $stripRN = true,
+ $defaultBRText = DEFAULT_BR_TEXT,
+ $defaultSpanText = DEFAULT_SPAN_TEXT,
+ $options = 0)
{
global $debug_object;
@@ -1409,8 +1509,8 @@ class simple_html_dom
// strip out the \r \n's if we are told to.
if ($stripRN) {
- $this->doc = str_replace("\r", " ", $this->doc);
- $this->doc = str_replace("\n", " ", $this->doc);
+ $this->doc = str_replace("\r", ' ', $this->doc);
+ $this->doc = str_replace("\n", ' ', $this->doc);
// set the length of content since we have changed it.
$this->size = strlen($this->doc);
@@ -1440,83 +1540,89 @@ class simple_html_dom
// make load function chainable
return $this;
-
}
- // load html from file
function load_file()
{
$args = func_get_args();
- if($doc = call_user_func_array('file_get_contents', $args) !== false) {
+ if(($doc = call_user_func_array('file_get_contents', $args)) !== false) {
$this->load($doc, true);
} else {
return false;
}
}
- /**
- * Set the callback function
- *
- * @param callable $function_name Callback function to run for each element
- * in the DOM.
- * @return void
- */
function set_callback($function_name)
{
$this->callback = $function_name;
}
- /**
- * Remove callback function
- *
- * @return void
- */
function remove_callback()
{
$this->callback = null;
}
- // save dom as string
- function save($filepath='')
+ function save($filepath = '')
{
$ret = $this->root->innertext();
- if ($filepath!=='') file_put_contents($filepath, $ret, LOCK_EX);
+ 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)
+ 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);}
+ if (isset($this->nodes)) {
+ 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)
+ function dump($show_attr = true)
{
$this->root->dump($show_attr);
}
- // prepare HTML data and init everything
- protected function prepare($str, $lowercase=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
+ protected function prepare(
+ $str, $lowercase = true,
+ $defaultBRText = DEFAULT_BR_TEXT,
+ $defaultSpanText = DEFAULT_SPAN_TEXT)
{
$this->clear();
$this->doc = trim($str);
$this->size = strlen($this->doc);
- $this->original_size = $this->size; // Save the original size of the html that we got in. It might be useful to someone.
+ $this->original_size = $this->size; // original size of the html
$this->pos = 0;
$this->cursor = 1;
$this->noise = array();
@@ -1529,21 +1635,15 @@ class simple_html_dom
$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];
+ if ($this->size > 0) { $this->char = $this->doc[0]; }
}
- /**
- * Parse HTML content
- *
- * @return bool True on success
- */
protected function parse()
{
while (true) {
// Read next tag if there is no text between current position and the
// next opening tag.
- if (($s = $this->copy_until_char('<'))==='')
- {
+ if (($s = $this->copy_until_char('<')) === '') {
if($this->read_tag()) {
continue;
} else {
@@ -1559,174 +1659,241 @@ class simple_html_dom
}
}
- // 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'))
- {
+ 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)
- {
+ if ($success) {
$charset = $matches[1];
- if (is_object($debug_object)) {$debug_object->debug_log(2, 'header content-type found charset of: ' . $charset);}
+ 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, true);
- if (!empty($el))
- {
+ if (empty($charset)) {
+ // https://www.w3.org/TR/html/document-metadata.html#statedef-http-equiv-content-type
+ $el = $this->root->find('meta[http-equiv=Content-Type]', 0, true);
+
+ if (!empty($el)) {
$fullvalue = $el->content;
- if (is_object($debug_object)) {$debug_object->debug_log(2, 'meta content-type tag found' . $fullvalue);}
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(2,
+ 'meta content-type tag found'
+ . $fullvalue
+ );
+ }
- if (!empty($fullvalue))
- {
- $success = preg_match('/charset=(.+)/i', $fullvalue, $matches);
- if ($success)
- {
+ if (!empty($fullvalue)) {
+ $success = preg_match(
+ '/charset=(.+)/i',
+ $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.');}
+ } 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->doc . "ascii", $encoding_list = array( "UTF-8", "CP1252" ) );
- if (is_object($debug_object)) {$debug_object->debug_log(2, 'mb_detect found: ' . $charset);}
+ if (empty($charset)) {
+ // https://www.w3.org/TR/html/document-metadata.html#character-encoding-declaration
+ if ($meta = $this->root->find('meta[charset]', 0)) {
+ $charset = $meta->charset;
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(2, 'meta charset: ' . $charset);
+ }
+ }
+ }
+
+ if (empty($charset)) {
+ // Try to guess the charset based on the content
+ // Requires Multibyte String (mbstring) support (optional)
+ if (function_exists('mb_detect_encoding')) {
+ /**
+ * mb_detect_encoding() is not intended to distinguish between
+ * charsets, especially single-byte charsets. Its primary
+ * purpose is to detect which multibyte encoding is in use,
+ * i.e. UTF-8, UTF-16, shift-JIS, etc.
+ *
+ * -- https://bugs.php.net/bug.php?id=38138
+ *
+ * Adding both CP1251/ISO-8859-5 and CP1252/ISO-8859-1 will
+ * always result in CP1251/ISO-8859-5 and vice versa.
+ *
+ * Thus, only detect if it's either UTF-8 or CP1252/ISO-8859-1
+ * to stay compatible.
+ */
+ $encoding = mb_detect_encoding(
+ $this->doc,
+ array( 'UTF-8', 'CP1252', 'ISO-8859-1' )
+ );
+
+ if ($encoding === 'CP1252' || $encoding === 'ISO-8859-1') {
+ // Due to a limitation of mb_detect_encoding
+ // 'CP1251'/'ISO-8859-5' will be detected as
+ // 'CP1252'/'ISO-8859-1'. This will cause iconv to fail, in
+ // which case we can simply assume it is the other charset.
+ if (!@iconv('CP1252', 'UTF-8', $this->doc)) {
+ $encoding = 'CP1251';
+ }
+ }
+
+ if ($encoding !== false) {
+ $charset = $encoding;
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(2, 'mb_detect: ' . $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';
+ if (empty($charset)) {
+ // Assume it's UTF-8 as it is the most likely charset to be used
+ $charset = 'UTF-8';
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(2, 'No match found, assume ' . $charset);
}
}
- // 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');}
+ // Since CP1252 is a superset, if we get one of it's subsets, we want
+ // it instead.
+ if ((strtolower($charset) == 'iso-8859-1')
+ || (strtolower($charset) == 'latin1')
+ || (strtolower($charset) == 'latin-1')) {
$charset = 'CP1252';
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(2,
+ 'replacing ' . $charset . ' with CP1252 as its a superset'
+ );
+ }
}
- if (is_object($debug_object)) {$debug_object->debug_log(1, 'EXIT - ' . $charset);}
+ if (is_object($debug_object)) {
+ $debug_object->debug_log(1, 'EXIT - ' . $charset);
+ }
return $this->_charset = $charset;
}
- /**
- * Parse tag from current document position.
- *
- * @return bool True if a tag was found, false otherwise
- */
protected function read_tag()
{
// Set end position if no further tags found
- if ($this->char!=='<')
- {
+ 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
+ $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
+ if ($this->char === '/') {
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
// Skip whitespace in end tags (i.e. in "</ html>")
$this->skip($this->token_blank);
$tag = $this->copy_until_char('>');
// Skip attributes in end tags
- if (($pos = strpos($tag, ' '))!==false)
+ if (($pos = strpos($tag, ' ')) !== false) {
$tag = substr($tag, 0, $pos);
+ }
$parent_lower = strtolower($this->parent->tag);
$tag_lower = strtolower($tag);
// The end tag is supposed to close the parent tag. Handle situations
// when it doesn't
- if ($parent_lower!==$tag_lower)
- {
+ if ($parent_lower !== $tag_lower) {
// Parent tag does not have to be closed necessarily (optional closing tag)
// Current tag is a block tag, so it may close an ancestor
- if (isset($this->optional_closing_tags[$parent_lower]) && isset($this->block_tags[$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;
// Traverse ancestors to find a matching opening tag
// Stop at root node
- while (($this->parent->parent) && strtolower($this->parent->tag)!==$tag_lower)
+ while (($this->parent->parent)
+ && strtolower($this->parent->tag) !== $tag_lower
+ ){
$this->parent = $this->parent->parent;
+ }
// If we don't have a match add current tag as text node
- if (strtolower($this->parent->tag)!==$tag_lower) {
+ if (strtolower($this->parent->tag) !== $tag_lower) {
$this->parent = $org_parent; // restore origonal parent
- if ($this->parent->parent) $this->parent = $this->parent->parent;
+
+ if ($this->parent->parent) {
+ $this->parent = $this->parent->parent;
+ }
+
$this->parent->_[HDOM_INFO_END] = $this->cursor;
return $this->as_text_node($tag);
}
- }
- // Grandparent exists and current tag is a block tag, so our parent doesn't have an end tag
- else if (($this->parent->parent) && isset($this->block_tags[$tag_lower]))
- {
+ } elseif (($this->parent->parent)
+ && isset($this->block_tags[$tag_lower])
+ ) {
+ // Grandparent exists and current tag is a block tag, so our
+ // parent doesn't have an end tag
$this->parent->_[HDOM_INFO_END] = 0; // No end tag
$org_parent = $this->parent;
// Traverse ancestors to find a matching opening tag
// Stop at root node
- while (($this->parent->parent) && strtolower($this->parent->tag)!==$tag_lower)
+ while (($this->parent->parent)
+ && strtolower($this->parent->tag) !== $tag_lower
+ ) {
$this->parent = $this->parent->parent;
+ }
// If we don't have a match add current tag as text node
- if (strtolower($this->parent->tag)!==$tag_lower)
- {
+ 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);
}
- }
- // Grandparent exists and current tag closes it
- else if (($this->parent->parent) && strtolower($this->parent->parent->tag)===$tag_lower)
- {
+ } elseif (($this->parent->parent)
+ && strtolower($this->parent->parent->tag) === $tag_lower
+ ) { // Grandparent exists and current tag closes it
$this->parent->_[HDOM_INFO_END] = 0;
$this->parent = $this->parent->parent;
- }
- else // Random tag, add as text node
+ } else { // Random tag, add as text node
return $this->as_text_node($tag);
+ }
}
// Set end position of parent tag to current cursor position
$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
+ if ($this->parent->parent) {
+ $this->parent = $this->parent->parent;
+ }
+
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
return true;
}
@@ -1741,25 +1908,27 @@ class simple_html_dom
// <!DOCTYPE html>
// <![CDATA[ ... ]]>
// <!-- Comment -->
- if (isset($tag[0]) && $tag[0]==='!') {
+ if (isset($tag[0]) && $tag[0] === '!') {
$node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until_char('>');
- if (isset($tag[2]) && $tag[1]==='-' && $tag[2]==='-') { // Comment ("<!--")
+ if (isset($tag[2]) && $tag[1] === '-' && $tag[2] === '-') { // Comment ("<!--")
$node->nodetype = HDOM_TYPE_COMMENT;
$node->tag = 'comment';
} else { // Could be doctype or CDATA but we don't care
$node->nodetype = HDOM_TYPE_UNKNOWN;
$node->tag = 'unknown';
}
- if ($this->char==='>') $node->_[HDOM_INFO_TEXT].='>';
+
+ if ($this->char === '>') { $node->_[HDOM_INFO_TEXT] .= '>'; }
+
$this->link_nodes($node, true);
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
return true;
}
// The start tag cannot contain another start tag, if so add as text
// i.e. "<<html>"
- if ($pos=strpos($tag, '<')!==false) {
+ if ($pos = strpos($tag, '<') !== false) {
$tag = '<' . substr($tag, 0, -1);
$node->_[HDOM_INFO_TEXT] = $tag;
$this->link_nodes($node, false);
@@ -1768,19 +1937,19 @@ class simple_html_dom
}
// Handle invalid tag names (i.e. "<html#doc>")
- if (!preg_match("/^\w[\w:-]*$/", $tag)) {
+ if (!preg_match('/^\w[\w:-]*$/', $tag)) {
$node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until('<>');
// Next char is the beginning of a new tag, don't touch it.
- if ($this->char==='<') {
+ if ($this->char === '<') {
$this->link_nodes($node, false);
return true;
}
// Next char closes current tag, add and be done with it.
- if ($this->char==='>') $node->_[HDOM_INFO_TEXT].='>';
+ if ($this->char === '>') { $node->_[HDOM_INFO_TEXT] .= '>'; }
$this->link_nodes($node, false);
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
return true;
}
@@ -1790,11 +1959,9 @@ class simple_html_dom
$node->tag = ($this->lowercase) ? $tag_lower : $tag;
// handle optional closing tags
- if (isset($this->optional_closing_tags[$tag_lower]) )
- {
+ if (isset($this->optional_closing_tags[$tag_lower])) {
// Traverse ancestors to close all optional closing tags
- while (isset($this->optional_closing_tags[$tag_lower][strtolower($this->parent->tag)]))
- {
+ while (isset($this->optional_closing_tags[$tag_lower][strtolower($this->parent->tag)])) {
$this->parent->_[HDOM_INFO_END] = 0;
$this->parent = $this->parent->parent;
}
@@ -1802,271 +1969,238 @@ class simple_html_dom
}
$guard = 0; // prevent infinity loop
- $space = array($this->copy_skip($this->token_blank), '', ''); // [0] Space between tag and first attribute
+
+ // [0] Space between tag and first attribute
+ $space = array($this->copy_skip($this->token_blank), '', '');
// attributes
- do
- {
+ do {
// Everything until the first equal sign should be the attribute name
$name = $this->copy_until($this->token_equal);
- if ($name==='' && $this->char!==null && $space[0]==='')
- {
+ if ($name === '' && $this->char !== null && $space[0] === '') {
break;
}
- if ($guard===$this->pos) // Escape infinite loop
- {
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ if ($guard === $this->pos) { // Escape infinite loop
+ $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!=='>') { // Out of bounds before the tag ended
+ // Out of bounds before the tag ended
+ 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->_[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]=='<') { // Attributes cannot start after opening tag
+ // Attributes cannot start after opening tag
+ 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);
+ $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->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
$this->link_nodes($node, false);
return true;
}
- if ($name!=='/' && $name!=='') { // this is a attribute name
- $space[1] = $this->copy_skip($this->token_blank); // [1] Whitespace after attribute name
+ if ($name !== '/' && $name !== '') { // this is a attribute name
+ // [1] Whitespace after attribute name
+ $space[1] = $this->copy_skip($this->token_blank);
+
$name = $this->restore_noise($name); // might be a noisy name
- if ($this->lowercase) $name = strtolower($name);
- if ($this->char==='=') { // attribute with value
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+
+ if ($this->lowercase) { $name = strtolower($name); }
+
+ if ($this->char === '=') { // attribute with value
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
$this->parse_attr($node, $name, $space); // get attribute value
- }
- else {
+ } 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
+ if ($this->char != '>') { $this->char = $this->doc[--$this->pos]; } // prev
}
+
$node->_[HDOM_INFO_SPACE][] = $space;
- $space = array($this->copy_skip($this->token_blank), '', ''); // prepare for next attribute
- }
- else // no more attributes
+
+ // prepare for next attribute
+ $space = array(
+ $this->copy_skip($this->token_blank),
+ '',
+ ''
+ );
+ } else { // no more attributes
break;
- } while ($this->char!=='>' && $this->char!=='/'); // go until the tag ended
+ }
+ } while ($this->char !== '>' && $this->char !== '/'); // go until the tag ended
$this->link_nodes($node, true);
$node->_[HDOM_INFO_ENDSPACE] = $space[0];
// handle empty tags (i.e. "<div/>")
- if ($this->copy_until_char('>')==='/')
- {
+ if ($this->copy_until_char('>') === '/') {
$node->_[HDOM_INFO_ENDSPACE] .= '/';
$node->_[HDOM_INFO_END] = 0;
- }
- else
- {
+ } else {
// reset parent
- if (!isset($this->self_closing_tags[strtolower($node->tag)])) $this->parent = $node;
+ if (!isset($this->self_closing_tags[strtolower($node->tag)])) {
+ $this->parent = $node;
+ }
}
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+
+ $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")
- {
+ if ($node->tag === 'br') {
$node->_[HDOM_INFO_INNER] = $this->default_br_text;
}
return true;
}
- /**
- * Parse attribute from current document position
- *
- * @param object $node Node for the attributes
- * @param string $name Name of the current attribute
- * @param array $space Array for spacing information
- * @return void
- */
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 attention to the first one as opposed to the last one.
- // https://stackoverflow.com/a/26341866
- if (isset($node->attr[$name]))
- {
- return;
- }
+ $is_duplicate = isset($node->attr[$name]);
+
+ if (!$is_duplicate) // Copy whitespace between "=" and value
+ $space[2] = $this->copy_skip($this->token_blank);
- $space[2] = $this->copy_skip($this->token_blank); // [2] Whitespace between "=" and the value
switch ($this->char) {
- case '"': // value is anything between double quotes
- $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('"'));
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ case '"':
+ $quote_type = HDOM_QUOTE_DOUBLE;
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
+ $value = $this->copy_until_char('"');
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
break;
- case '\'': // value is anything between single quotes
- $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('\''));
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ case '\'':
+ $quote_type = HDOM_QUOTE_SINGLE;
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
+ $value = $this->copy_until_char('\'');
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
break;
- default: // value is anything until the first space or end tag
- $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_NO;
- $node->attr[$name] = $this->restore_noise($this->copy_until($this->token_attr));
+ default:
+ $quote_type = HDOM_QUOTE_NO;
+ $value = $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]);
+
+ $value = $this->restore_noise($value);
+
+ // PaperG: Attributes should not have \r or \n in them, that counts as
+ // html whitespace.
+ $value = str_replace("\r", '', $value);
+ $value = str_replace("\n", '', $value);
+
+ // 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') {
+ $value = trim($value);
+ }
+
+ if (!$is_duplicate) {
+ $node->_[HDOM_INFO_QUOTE][] = $quote_type;
+ $node->attr[$name] = $value;
}
}
- /**
- * Link node to parent node
- *
- * @param object $node Node to link to parent
- * @param bool $is_child True if the node is a child of parent
- * @return void
- */
- // link node's parent
protected function link_nodes(&$node, $is_child)
{
$node->parent = $this->parent;
$this->parent->nodes[] = $node;
- if ($is_child)
- {
+ if ($is_child) {
$this->parent->children[] = $node;
}
}
- /**
- * Add tag as text node to current node
- *
- * @param string $tag Tag name
- * @return bool True on success
- */
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
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
return true;
}
- /**
- * Seek from the current document position to the first occurrence of a
- * character not defined by the provided string. Update the current document
- * position to the new position.
- *
- * @param string $chars A string containing every allowed character.
- * @return void
- */
protected function skip($chars)
{
$this->pos += strspn($this->doc, $chars, $this->pos);
- $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
+ $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
}
- /**
- * Copy substring from the current document position to the first occurrence
- * of a character not defined by the provided string.
- *
- * @param string $chars A string containing every allowed character.
- * @return string Substring from the current document position to the first
- * occurrence of a character not defined by the provided string.
- */
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 '';
+ $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
+ if ($len === 0) { return ''; }
return substr($this->doc, $pos, $len);
}
- /**
- * Copy substring from the current document position to the first occurrence
- * of any of the provided characters.
- *
- * @param string $chars A string containing every character to stop at.
- * @return string Substring from the current document position to the first
- * occurrence of any of the provided characters.
- */
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
+ $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
return substr($this->doc, $pos, $len);
}
- /**
- * Copy substring from the current document position to the first occurrence
- * of the provided string.
- *
- * @param string $char The string to stop at.
- * @return string Substring from the current document position to the first
- * occurrence of the provided string.
- */
protected function copy_until_char($char)
{
- if ($this->char===null) return '';
+ if ($this->char === null) { return ''; }
- if (($pos = strpos($this->doc, $char, $this->pos))===false) {
- $ret = substr($this->doc, $this->pos, $this->size-$this->pos);
+ 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 '';
+ 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);
+ return substr($this->doc, $pos_old, $pos - $pos_old);
}
- /**
- * Remove noise from HTML content
- *
- * Noise is stored to {@see simple_html_dom::$noise}
- *
- * @param string $pattern The regex pattern used for finding noise
- * @param bool $remove_tag True to remove the entire match. Default is false
- * to only remove the captured data.
- */
- protected function remove_noise($pattern, $remove_tag=false)
+ 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);
+ $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);
+ }
- 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; // 0 = entire match, 1 = submatch
$this->noise[$key] = $matches[$i][$idx][0];
$this->doc = substr_replace($this->doc, $key, $matches[$i][$idx][1], strlen($matches[$i][$idx][0]));
@@ -2074,66 +2208,70 @@ class simple_html_dom
// reset the length of content
$this->size = strlen($this->doc);
- if ($this->size>0)
- {
+
+ if ($this->size > 0) {
$this->char = $this->doc[0];
}
}
- /**
- * Restore noise to HTML content
- *
- * Noise is restored from {@see simple_html_dom::$noise}
- *
- * @param string $text A subset of HTML containing noise
- * @return string The same content with noise restored
- */
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)
- { // todo: "___noise___1000" (or any number with four or more digits) in the DOM causes an infinite loop which could be utilized by malicious software
- $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);
+ 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...
+
+ // todo: "___noise___1000" (or any number with four or more digits)
+ // in the DOM causes an infinite loop which could be utilized by
+ // malicious software
+ 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);
}
- else
- {
+
+ 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);
+ $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);
+ } 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)
- {
+ foreach($this->noise as $noiseElement) {
+ if (strpos($noiseElement, $text) !== false) {
return $noiseElement;
}
}
}
+
function __toString()
{
return $this->root->innertext();
@@ -2141,8 +2279,7 @@ class simple_html_dom
function __get($name)
{
- switch ($name)
- {
+ switch ($name) {
case 'outertext':
return $this->root->innertext();
case 'innertext':
@@ -2156,17 +2293,54 @@ class simple_html_dom
}
}
- // 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);}
-}
+ 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>")->firstChild();
+ }
+
+ function createTextNode($value)
+ {
+ return @end(str_get_html($value)->nodes);
+ }
-?> \ No newline at end of file
+ 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);
+ }
+}
diff --git a/debian/whitelist.txt b/whitelist.default.txt
index 820c44e..6530c32 100644
--- a/debian/whitelist.txt
+++ b/whitelist.default.txt
@@ -4,7 +4,6 @@ DansTonChat
DuckDuckGo
Facebook
Flickr
-GooglePlusPost
GoogleSearch
Identica
Instagram
@@ -13,4 +12,4 @@ Pinterest
Scmb
Twitter
Wikipedia
-Youtube \ No newline at end of file
+Youtube