1
0
mirror of https://github.com/RSS-Bridge/rss-bridge.git synced 2025-08-26 09:34:46 +02:00

Compare commits

..

238 Commits

Author SHA1 Message Date
Eugene Molotov
ff50e4918c Bump version to dev.2020-11-10 2020-11-10 16:26:08 +05:00
Eugene Molotov
8e4d6d8fdb [README] Update list of contributors 2020-11-10 16:24:58 +05:00
ayacoo
cc548b16a8 [HeiseBridge] Check for article (#1790) 2020-11-10 11:14:09 +05:00
Roman Remizov
b66026e241 [TwitterBridge] Add support for querying by list ID (#1834) 2020-11-10 11:12:02 +05:00
Joseph
a23d4bd0e6 [BrutBridge] Add support for Spain edition and health category (#1833) 2020-11-09 18:14:13 +05:00
ymeister
bde4159a9e [README] Add "filter" to Requirements (#1827)
This extension is required by filter_var()
2020-11-08 12:25:06 +05:00
Niehztog
3ad138026d [BridgeXPathAbstract + BlizzardNewsBridge + XPathBridge] Add new abstract class + two example implementations (#1671) 2020-11-08 12:22:41 +05:00
Joshua Coales
d05a8b79fe [contents.php] Fix return type hints (#1824) 2020-11-08 12:19:18 +05:00
Joshua Coales
efe32aad22 [FacebookBridge] Permalink fix (#1838)
Also removing timestamp which does not work
2020-11-06 18:43:10 +05:00
Petr Kolář
0655b3cb39 [MallTvBridge] New bridge (#1819) 2020-10-31 22:05:13 +05:00
csisoap
59082368c7 [BleepingComputerBridge] New bridge (#1815) 2020-10-31 22:01:19 +05:00
msch
c8b2c1bf74 [OpenwrtSecurityBridge] Add new bridge (#1812) 2020-10-31 21:57:29 +05:00
Roliga
b48bc77c22 [TwitchBridge] Switch to unofficial GraphQL API (#1829)
* [TwitchBridge] Switch to unofficial GraphQL API

The GraphQL API that the twitch.tv website uses has a lot more
information available than the official APIs. Hopefully it'll be stable.
2020-10-30 13:50:36 +00:00
Joshua Coales
6af87b2f32 [FacebookBridge] Use touch.facebook.com for groups (#1817) 2020-10-29 08:42:49 +05:00
Eugene Molotov
93cdf5e342 [core] Fixed passive XSS vulnerability
Reference: https://www.openbugbounty.org/reports/1140367/
2020-10-26 15:08:11 +05:00
Joseph
164b407f28 [BridgeCard] Fix parameter layout issue (#1816)
Fixes parameter layout issue on small screens.
2020-10-26 12:11:58 +05:00
ORelio
2714c3d816 [WordPress] Limit feed to 20 items (#1801) 2020-10-21 14:59:04 +05:00
Eugene Molotov
364b5282a3 [GoogleSearch] Use other class for content retreiving (#1803) 2020-10-19 16:22:37 +05:00
Eugene Molotov
5e4f3c351e [NineGagBridge] Lint previous commit 2020-10-15 14:18:46 +05:00
Gregor Santner
a332a5a414 [NineGagBridge] In post URI replace scheme from "http" to "https" 2020-10-15 14:12:54 +05:00
Joshua Coales
45e2f385b3 [FacebookBridge] Handle mobile links and unify host validation (#1789) 2020-10-15 14:08:03 +05:00
Joseph
0a1ff10a52 [KoreusBridge + VarietyBridge] Use HTTPS when fetching feedburner feeds (#1797) 2020-10-15 13:03:51 +05:00
Eugene Molotov
645a8f62c6 [.travis] Fix several phpcs and phpunit errors (#1799) 2020-10-15 12:53:19 +05:00
ORelio
64ec488f70 [ZDNet] Fix article layout (#1793) 2020-10-13 21:46:58 +05:00
Petr Kolář
7b6ff78623 [CeskaTelevizeBridge] Add New bridge (#1784) 2020-10-12 11:35:06 +05:00
Corentin Garcia
82acbbb421 [DribbbleBridge] Fix picture parsing (#1787) 2020-10-10 00:46:40 +05:00
Ololbu
84d5daaa03 [FicbookBridge] Add getName implementation (#1771) 2020-10-10 00:39:35 +05:00
Joseph
712f60e910 [HeiseBridge] Fix multi-page article fetching (#1767)
Fixes multi-page article fetching by adding '&seite=all' to  article URL.
2020-10-09 23:55:28 +05:00
Alexander
55015f80cf [AlbionOnlineBridge] New bridge (#1769) 2020-10-09 23:48:40 +05:00
csisoap
f90c6b5bb9 [NasaApodBridge] Fix broken image link (#1778) 2020-10-09 23:33:54 +05:00
Corentin Garcia
ff98efe8dc [core] Use Parsedown for Markdown parsing (#1783) 2020-10-09 23:29:02 +05:00
Corentin Garcia
fe166d0216 [NasaApodBridge] Fix header being parsed as item (#1586) 2020-10-07 11:16:26 +05:00
triatic
d3455dd18a [TwitterBridge] Optimise regular expression code (#1768)
* [TwitterBridge] Optimise regular expression code

Optimise regular expression search code so adding new URLs is cleaner
2020-10-05 12:07:39 +05:00
ORelio
47dc26c775 [NextINpact] Fix subtitle extraction in #LeBrief (#1780)
The bridge was taking another article abstract as subtitle for #LeBrief articles
2020-10-05 11:57:13 +05:00
Ololbu
3df2de4c6f [FicbookBridge] Fix data getting and months (#1765) 2020-09-28 14:02:40 +05:00
sarnd
01985b7af7 [TwitterBridge] URL to js file with apikey changed again (#1764) 2020-09-28 10:01:37 +05:00
Joseph
80cc88ba78 [SoundcloudBridge] Fix bridge not returning tracks (#1757)
+ Use artwork for enclosure
2020-09-25 11:43:12 +05:00
Christian Schabesberger
2bb99c4448 [NordbayernBridge] Fix images and newsblock order (#1741) 2020-09-18 10:13:31 +05:00
Jason Ghent
3a29347e60 [ParameterValidator] Ignore cache-busting param (#1723) 2020-09-14 14:01:55 +05:00
Alexander
d299adb827 [EpicgamesBridge] Add pinned posts to list (#1736) 2020-09-14 13:14:35 +05:00
Michael Bemmerl
cf606a3a6b [OtrkeyFinderBridge] Add bridge for otrkeyfinder.com (#1712) 2020-09-11 11:48:03 +05:00
Eugene Molotov
6c244f4d9b [TwitterBridge] Skip advertisment tweets (#1673) 2020-09-11 11:44:28 +05:00
AxorPL
d6f277d029 [WorldCosplayBridge] Add new bridge (#1732) 2020-09-09 17:11:19 +05:00
sysadminstory
ab8e89a97f [AllocineFRBridge] Update CSS class (#1585)
Website has change one CSS class : updated the bridge to allow parsing.
2020-09-08 10:55:21 +05:00
Joseph
747bb6ad9c [WosckerBridge] Add bridge (#1643) 2020-09-03 11:18:15 +05:00
Joseph
d33e090fe1 [MastodonBridge] Update feed URL format (#1718)
Changes feed URL from `https://instance/users/username.atom` to `https://instance/@username.rss`.
2020-09-03 10:49:19 +05:00
Christian Jonak
fec52418d5 [FM4Bridge] Add new bridge for FM4 news page (#1719) 2020-09-03 10:46:35 +05:00
ggiessen
bb51a0d212 [MarktplaatsBridge] Improvements (#1722)
- sometimes $listing->imageUrls is empty so moved after the if statement on line 91 
- added price and location info
- added function getName
2020-09-03 10:44:32 +05:00
Bob
68dd2d745f [InstagramBridge] Change TAG_QUERY_HASH (#1727) 2020-09-02 11:02:34 +05:00
ORelio
46abc18e87 [Anidex] Fix content retrieval (#1693)
Anidex uses two separate domains, anidex.info and anidex.moe
anidex.info has ddos-guard so we need to request
anidex.moe with Host header set to anidex.info
2020-08-31 22:04:56 +05:00
ORelio
e00bbe353f * [ReleasesSwitch] Switch scene releases (#1694)
Separate bridge from Releases3DS that just has a different URL.
Inherits from Releases3DS so both bridges need to be present.

*  [Releases3DS] Fix PHP notices related to IGN
2020-08-31 22:02:25 +05:00
somini
c21a805cb4 [DiarioDeNoticiasBridge]: New Bridge (#1717) 2020-08-27 10:38:51 +05:00
Simon Alberny
3b36c413e5 [MondeDiplo] Switched to HTTPS + Title and content updated (#1714) 2020-08-27 10:28:59 +05:00
ggiessen
94576c3053 [MarktplaatsBridge] 'https:' added to img src url (#1713) 2020-08-24 10:30:59 +05:00
ronansalmon
25cff9c07b [TwitterBridge] Convert plain text URLs into HTML hyperlinks (#1627) 2020-08-21 17:55:11 +05:00
ORelio
07c71b3b36 [NextINpact] Upgrade for NextINpact v7 (#1708) 2020-08-20 17:49:26 +05:00
Alexander
859053ef7a [EpicgamesBridge] New bridge (#1709) 2020-08-20 10:36:11 +05:00
triatic
73287f536b [TwitterBridge] Add retweeter to retweeted tweets (#1679) 2020-08-20 10:00:27 +05:00
sysadminstory
0b1e592a5e [ZoneTelechargement] Update URL (#1710)
The bridge now shows links to the new URL.

It keeps the old one internally to bypass the Robot protection on the
new URL.
2020-08-19 17:35:19 +05:00
jannyba
ef54a78430 [InstagramBridge] Fix "Skip reviews" checkbox description (#1702) 2020-08-16 11:23:48 +05:00
Eugene Molotov
4b8c3b9d36 [Multi] Minor improvements for my bridges (#1507)
* [DarkReading] Hide dummy articles

* [FuturaSciences] Strip inline scripts from content

* [FeedExpander] Fix PHP notice on missing uri field

(guid is valid uri AND item uri is not valid)
 => (guid is valid uri AND item uri is empty or not valid)

* [NextInpact] Fix subtitle extraction

* [Markdown] Fix images with empty replacement text

* [TheHackerNews] Fix Author name cleanup

* [LeMondeInformatique] Remove encoding conversion

Was previously needed due to actual encoding on the page
being inconsistent with encoding specified in <meta> tag

* [AnimeUltime] Remove encoding conversion

Was previously needed due to encoding on the page being incorrect

* [FuturaSciences] Fix content extraction

* [FuturaSciences] Fix unneeded unset()

* [GBAtemp] Fix tutorial mode URL extraction

* [GBAtemp] Fix tutorial mode Title extraction
2020-08-14 10:30:31 +05:00
ORelio
c642652fea [GBAtemp] Fix tutorial mode Title extraction 2020-08-12 20:08:24 +02:00
ORelio
f0e6298cab [GBAtemp] Fix tutorial mode URL extraction 2020-08-12 20:08:24 +02:00
ORelio
45e247b9d0 [FuturaSciences] Fix unneeded unset() 2020-08-12 20:08:24 +02:00
ORelio
66a009b8fb [FuturaSciences] Fix content extraction 2020-08-12 20:08:24 +02:00
ORelio
efd1abfab1 [AnimeUltime] Remove encoding conversion
Was previously needed due to encoding on the page being incorrect
2020-08-12 20:08:24 +02:00
ORelio
8b173b8874 [LeMondeInformatique] Remove encoding conversion
Was previously needed due to actual encoding on the page
being inconsistent with encoding specified in <meta> tag
2020-08-12 20:08:24 +02:00
ORelio
90e9c9962a [TheHackerNews] Fix Author name cleanup 2020-08-12 20:08:24 +02:00
ORelio
01cc32a0cc [Markdown] Fix images with empty replacement text 2020-08-12 20:08:24 +02:00
Joseph
dc36b425cd [DevToBridge] Fix bridge (#1699)
Fixes full article option not working
2020-08-12 23:07:53 +05:00
triatic
268ddf1382 [TwitterBridge] URL to js file with apikey changed (#1698)
Fixes #1697
2020-08-12 10:32:34 +05:00
ORelio
8144488a9e [FeedExpander] Fix PHP notice on missing uri field
(guid is valid uri AND item uri is not valid)
 => (guid is valid uri AND item uri is empty or not valid)
2020-08-11 14:01:44 +02:00
ORelio
062dd7f8a5 [FuturaSciences] Strip inline scripts from content 2020-08-11 14:01:44 +02:00
Nemo
be089702f0 [docker] Install memcached PHP extension from PECL (#1473)
- Adapted from https://stackoverflow.com/a/41575677
- Tested by running `php -m`
- SimpleXML now comes pre-installed with PHP, so removed the extra step
- Adds --no-install-recommends

Signed-off-by: Nemo <me@captnemo.in>
2020-08-07 18:21:28 +05:00
sarnd
c71fad4a4a [TwitterBridge] URL to js file with apikey changed. (#1686)
Twitter has changed URL scheme back again (see PR#1647 / commit 78298385d0)

This patch will try both URL schemes now and throw a specific error when neither works
2020-08-06 10:22:17 +05:00
Eugene Molotov
5be251a66e [TwitterBridge] Fetch latest tweets on hashtag or keyword query (#1674) 2020-08-02 19:40:41 +05:00
Eugene Molotov
7709b8d662 [VkBridge] Correct fallback behavior, when trying to get direct video links (#1670) 2020-07-31 15:29:18 +05:00
triatic
f5916a2f74 [TwitterBridge] apikey fetched every time (#1663)
The apikey is fetched every time because $data is not an array. Update the condition to expire the api key at the same time as the guest token.
2020-07-30 09:54:16 +05:00
Eugene Molotov
a33088ca99 [GoogleSearch] Correct parsing uri of search result (#1601) 2020-07-27 11:44:07 +05:00
somini
78facbcb83 [TwitterBridge] Fix noretweet for users (#1608)
This also removes spurious retweets.
2020-07-26 11:26:39 +05:00
Thomas
d5a75a2545 [DribbbleBridge] regex fix and CSS selector update (#1657)
* [DribbbleBridge] Fixed regular expressions for quote replacement in JSON (previously invalid JSON was created if a property value contained colons or single quotes). Also updated two CSS selectors as Dribbble's HTML has changed.

* [DribbbleBridge] Added fix for relative dates in JSON

* [DribbbleBridge] Removed redundant whitespaces
2020-07-25 08:58:42 +05:00
triatic
25698d182c [TwitterBridge] Remove unused variable 2020-07-24 13:13:21 +05:00
Corentin Garcia
9e74cc64ed [RainbowSixSiegeBridge] Fix bridge (#1587) 2020-07-24 12:56:41 +05:00
sarnd
78298385d0 [TwitterBridge] guest token is returned via body again. (#1647)
* [TwitterBridge] guest token is returned via body again. This change will try to search fot token inside header and fallback to body

* Twitter changed the URL scheme for the API
2020-07-24 12:52:27 +05:00
somini
976445b490 Improve Soundcloud bridge (#1500)
* [SoundcloudBridge] Add playlist support, migrate to `api-v2`
2020-07-05 19:49:46 +02:00
triatic
3ad126cdf2 [core] Add headers to file_get_contents (#1623)
Add response headers to file_get_contents() method. Headers are used by some bridges.
2020-06-25 12:22:05 +02:00
sarnd
e87b868307 [TwitterBridge] Fix issue #1621 @<twitter_user> failed with error 429 (#1622)
* [TwitterBridge] Fix issue #1621 @<twitter_user> failed with error 429
2020-06-25 12:21:48 +02:00
triatic
23c61f5f84 [TwitterBridge] Expire guest token by time (#1606)
* [TwitterBridge] Expire guest token by time

In addition to fetching a new guest token after 100 uses, also expire token after 5 minutes (configurable).
2020-06-23 15:14:50 +02:00
somini
22a01f1093 [Twitter] Fix Twitter bridge images and add other media types (#1595)
* Keep old URI structure

Use the username, not the user ID.

* Fix Twitter bridge images

Credit to @kinoushe

See https://github.com/RSS-Bridge/rss-bridge/issues/1562#issuecomment-639393175

* Include Videos and "Animated GIF" as twit enclosures

Credit to @kinoushe for digging into the API docs.

https://github.com/RSS-Bridge/rss-bridge/issues/1562#issuecomment-640320688

* Calculate the highest bitrate video

Include that on the enclosure.

* Appease linter

* Appease linter, again

* Remove surrounding link from videos

Add it on a smaller link besides it.

See
https://github.com/RSS-Bridge/rss-bridge/pull/1595#issuecomment-640989208

* Include video poster on the enclosures.
2020-06-10 22:39:36 +02:00
Park0
98ff5a095c [Marktplaats] New Bridge (#1575) 2020-06-09 20:21:34 +02:00
Eugene Molotov
e4c4ae8245 [MemcachedCache] loadData now returns null instead of false (#1592)
FileCache and SQLiteCache returns null on cache miss. This is important if using strict comparing (for example when using "===")
2020-06-08 11:27:19 +02:00
Lyra
124631df73 [TwitterBridge] Fix caching policy, usernames as well as images 2020-06-08 11:18:24 +02:00
Lyra
06891ae35f [TwitterBridge] Fix the bridge using a brand new API 2020-06-05 10:17:53 +02:00
Michael Bemmerl
c4422bdbb5 [Core] Fix notice of undefined offset when in detached HEAD state. (#1569) 2020-05-27 23:08:06 +02:00
floviolleau
a1dd98ff82 [LesJoiesDuCodeBridge] Fix items not loading 2020-05-27 23:04:43 +02:00
floviolleau
25f0d3b877 [TheCodingLoveBridge] Fix not loading items (#1577) 2020-05-27 23:04:03 +02:00
Damien Calesse
9a66227a79 [SensCritique] Fix search display (#1567)
- Remove movies search. It appears the website changed their movies
  displays and data cannot be easily extracted for now.
- Fix some errors on items without proper description and/or original
  title.
2020-05-20 21:52:37 +02:00
Sandro
8047041963 [Core] Include Media RSS namespace for Atom feeds
Include Media RSS namespace for Atom feeds
Fix #1511
Fix #1499
2020-05-19 10:00:12 +02:00
Joseph
fa74d3728b [GizmodoBridge] Fix bridge (#1538)
* [GizmodoBridge] Update bridge
2020-05-17 20:35:34 +02:00
Fanch
8233497611 [AirBreizhBridge] Add new bridge (#1544)
* [AirBreizhBridge] Add new bridge
2020-05-17 20:33:14 +02:00
Joseph
71745116e1 [MozillaBugTrackerBridge] Fix bridge (#1550)
* [MozillaBugTrackerBridge] Fix bridge
2020-05-17 20:33:01 +02:00
Paroleen
36fc4822dd [UnraidCommunityApplicationsBridge] Add new bridge (#1534)
* [UnraidCommunityApplicationsBridge] Add new bridge
2020-05-17 20:22:04 +02:00
Eugene Molotov
868d3f600d [VkBridge] Fix one letter bug on titles (#1555) 2020-05-17 20:21:37 +02:00
sysadminstory
f4affe1833 [AuoJMBridge] Follow Website change (#1527)
* [AuoJMBridge] Follow Website change
2020-05-17 20:05:04 +02:00
Joseph
63a4db7e86 [DownDetectorBridge] Fix bridge (#1528) 2020-05-17 20:04:37 +02:00
Joseph
f48909b84e [ASRockNewsBridge] Add Bridge (#1526)
* [ASRockNewsBridge] Add Bridge
2020-05-17 20:00:52 +02:00
Paroleen
ca88096f1f [AwwwardsBridge] New bridge (#1524)
[AwwwardsBridge] New bridge (#1524)
2020-05-17 19:58:19 +02:00
Lyra
1044952987 [MediapartBlogsBridge] Lint
[MediapartBlogsBridge] Lint
2020-05-17 19:49:00 +02:00
Eugene Molotov
119f4bdec5 [MediapartBlogsBridge] Lint 2020-05-10 17:35:21 +05:00
Lyra
e617d9f728 [MediapartBlogsBridge]: New Bridge
[MediapartBlogsBridge]: New Bridge
2020-04-03 10:02:47 +02:00
Lyra
5a43db4fb5 [FolhaDeSaoPauloBridge]: Improvements
[FolhaDeSaoPauloBridge]: Improvements
2020-04-03 09:53:42 +02:00
Lyra
badb5313b7 [NordBayernBridge] Add bridge (#1513)
* add Nordbayern bridge
2020-04-03 09:52:02 +02:00
Lyra
5eeda8dd52 Merge pull request #1515 from Dreckiger-Dan/patch-1
[HeiseBridge] add TechStage support
2020-04-03 09:50:08 +02:00
Lyra
413ae3cef6 [GithubTrendingBridge] Add bridge (#1492)
* Added GithubTrendingBridge
2020-04-03 09:48:31 +02:00
Christian Schabesberger
604d527ac7 add nordbayern bridge
fix intending
2020-04-02 12:44:10 +02:00
Dreckiger-Dan
cccd390b0f [HeiseBridge] add TechStage support 2020-03-31 23:47:57 +02:00
somini
223337d62d [FolhaDeSaoPauloBridge]: Improve URL
Remove the redirection.
2020-03-31 02:34:38 +01:00
somini
066e42e99a [FolhaDeSaoPauloBridge]: Improve HTML 2020-03-31 02:32:15 +01:00
Kirill Kotikov
fbfc82b0b7 Revert feed title 2020-03-26 21:37:19 +03:00
ORelio
00dd81a8aa [DarkReading] Hide dummy articles 2020-03-25 20:40:17 +01:00
somini
e0ac9972ee [MediapartBlogsBridge]: New Bridge
Fix #1468
2020-03-25 19:02:09 +00:00
Kirill Kotikov
f2de5aecc7 Change feed title 2020-03-24 19:07:23 +03:00
Kirill Kotikov
0fd7021030 Change cache time to 24hr (daily update time) 2020-03-23 10:35:02 +03:00
Kirill Kotikov
3ec32bb6c2 Fix title if language not set 2020-03-22 21:43:37 +03:00
Kirill Kotikov
ec7ef8f502 Update GithubTrendingBridge.php 2020-03-21 05:07:38 +03:00
Kirill Kotikov
7b73f3217f Fix page request 2020-03-21 05:01:45 +03:00
Kirill Kotikov
7c71377af0 Add additional languages + fix issues 2020-03-20 16:34:42 +03:00
Kirill Kotikov
c2559ff71f Add sdfsf 2020-03-16 19:25:28 +03:00
John Corser
366d2d66b3 [RobinhoodSnacks] Add bridge for Robinhood Snacks (#1460) 2020-02-26 23:32:57 +01:00
Lyra
7b63da522f [InstagramBridge] Use lowercase comparison when looking up user pk 2020-02-26 22:35:44 +01:00
Lyra
0705a2e7bb Bump version to dev.2020-02-26 2020-02-26 22:24:20 +01:00
Lyra
84616f53bf Update contributors 2020-02-26 22:23:30 +01:00
Eugene Molotov
a981450ae0 [Dockerfile] Build memcached extension (#1415) 2020-02-26 22:16:46 +01:00
somini
d39741c296 [GithubIssueBridgeIssue] Fix bridge (#1453)
* fix bridge according to website evolution
2020-02-26 22:15:50 +01:00
Lorenzo Stanco
3179c1e884 [InstagramBridge] Fixed item thumb on video entries (#1387) 2020-02-26 22:13:40 +01:00
sysadminstory
c9e5f6c9dd [AllocineFRBridge] Update Show List and parsing (#1407)
* [AllocineFRBridge] Update Show List and parsing
2020-02-26 22:12:25 +01:00
Julien Desgats
6b6974d115 [NewOnNetflix] Add new bridge (#1408) 2020-02-26 22:11:54 +01:00
Anchit Bajaj
96e58d4c94 Add bridge for Phoronix (#1412) 2020-02-26 22:10:54 +01:00
Anchit Bajaj
f0363ba03b [PcGamerBridge] - Add all articles, full content and images (#1420) 2020-02-26 22:10:09 +01:00
somini
90147fc45c [FirstLookMediaTech]: New Bridge (#1438) 2020-02-26 22:08:14 +01:00
John Corser
a3b4bd2d08 [DaveRamseyBlogBridge] Add new bridge (#1459) 2020-02-26 22:05:55 +01:00
St. John Johnson
e102353ab8 [GoComics] Update to new website structure (#1464)
GoComics.com has updated their website.  The image location is now a
data attribute in a div.
2020-02-26 21:56:52 +01:00
Joseph
a54eb88ee1 [DevToBridge] Fix bridge & add getName() (#1470) 2020-02-26 21:56:03 +01:00
somini
1584636e5b TinyLetter: New Bridge (#1469)
* TinyLetter: New Bridge
2020-02-26 21:50:25 +01:00
Joseph
fe83d763a3 [PornhubBridge] Fix travis issues (#1471)
* [PornhubBridge] Fix travis issues
2020-02-26 21:34:46 +01:00
Mitsukarenai
480694e819 [PornhubBridge] Add bridge 2020-02-15 00:03:29 +01:00
Tyler Kenney
8697e1e1a2 [RoosterTeethBridge] Add a new bridge (#1450)
* Added RoosterTeethBridge
2020-02-10 16:57:08 +01:00
Binnette
1ab7e493a8 [DonnonsBridge] Add a new bridge (#1441) 2020-02-10 16:56:40 +01:00
86423355844265459587182778
e5303efba3 [SoundcloudBridge] Fix returned URL and title (#1449) 2020-02-07 16:16:55 +01:00
Joseph
5bd07723ad [ScribdBridge] Add bridge (#1391) 2020-02-04 17:26:34 +01:00
Anchit Bajaj
00dbde2c24 [IGNBridge] Removed Ugly Nonworking Widgets (#1413) 2020-02-04 17:25:56 +01:00
floviolleau
a00e75b71c [AtmoOccitanieBridge] Add new bridge for air quality in cities in Occitanie (#1422)
* Add new bridge for Air Quality in cities supported by Atmo Occitanie
2020-02-04 17:24:42 +01:00
floviolleau
f040e4dc9c [AtmoNouvelleAquitaine] Change description (#1423)
* [AtmoNouvelleAquitaine] Change description
2020-02-04 17:22:42 +01:00
sysadminstory
182e9e7b41 [ZoneTelechargement] Update URL (#1425)
Website changed again his URL
2020-02-04 17:21:02 +01:00
somini
275662b8d4 [FolhaDeSaoPaulo]: Add new Bridge (#1426)
* [FolhaDeSaoPaulo]: Add new Bridge
2020-02-04 17:19:39 +01:00
Antoine Turmel
f52eb43f8c Update GithubSearchBridge.php (#1431)
Fixes #1430
2020-01-31 15:01:46 +01:00
sysadminstory
2450f80823 [ExtremeDownloadBridge] Update URL (#1429)
Website URL has changed again !
2020-01-31 15:00:17 +01:00
Corentin Garcia
45287e6853 [RainbowSixSiegeBridge] Fix bridge (#1433) 2020-01-31 14:51:59 +01:00
Eugene Molotov
830f57f607 [TwitterBridge] Use IE's user-agent (#1442)
Twitter will return pages with legacy design and frontend code, which bridge can deal with
2020-01-31 14:36:25 +01:00
Eugene Molotov
6a90a9d33f phpcs: fix new sudden violations (#1443) 2020-01-31 14:30:31 +01:00
Eugene Molotov
46b9879c08 [VkBridge] Correct post date calculating (#1417)
* [VkBridge] Correct post date calculating

Before this commit, post dates from december past year were
calculated as december current year.
2020-01-16 12:00:10 +01:00
Mitsukarenai
1343dbe97a [index] Bump spoofed user-agent version 2020-01-15 21:36:12 +01:00
Nono
2175a4d08b [MozillaSecurityBridge] source has been modified (#1394)
adjustement following source change
2020-01-10 14:22:58 +01:00
Joe Digilio
ad661c4c91 [RedditBridge] Fix typo prevents bridge from working (#1383) 2019-12-05 18:07:50 +01:00
Grégory T
ba8c4623ed [DisplayAction] Fix function call on a member (add ->) (#1379) 2019-12-04 18:34:26 +01:00
logmanoriginal
ba43c87952 [RevolutBridge] Remove bridge
An official RSS feed is available at https://blog.revolut.com/rss/

Note that there is also an invisible "RSS" button next to the Facebook
and Twitter icons at the menu bar.

References #1321
2019-12-04 18:23:13 +01:00
Grégory T
595b87946d [TorrentGalaxyBridge] Add new bridge (#1378) 2019-12-02 20:31:50 +01:00
logmanoriginal
99d4e1a43d Bump version to dev.2019-12-01 2019-12-01 13:40:17 +01:00
logmanoriginal
477de4e2df Bump version to 2019-12-01 2019-12-01 13:34:09 +01:00
logmanoriginal
246470da18 [README] Update list of contributors 2019-12-01 13:33:03 +01:00
LogMANOriginal
df9f7eb778 [FacebookBridge] Fix permalink issue (#1358)
Facebook has changed their strategy regarding permalinks, which
now include lots of unnecessary target data. Fortunately it also
contains the unique story id which we can utilize as URI.
2019-12-01 13:24:11 +01:00
David
375831f516 [NineGagBridge] Add filter option for animated content (#1374) 2019-12-01 13:07:25 +01:00
Roliga
e518936be7 [SoundcloudBridge] Automatically acquire client_id (#1375)
Also some slight refactoring, as well as adding Roliga as maintainer.
2019-12-01 12:42:53 +01:00
Jacob Mansfield
583dfb4958 [TheWhiteboard] Create new bridge for The Whiteboard (#1327) 2019-12-01 11:30:16 +01:00
Jacob Mansfield
2de45b163e [FurAffinityUser] Add new bridge (#1326) 2019-12-01 11:27:41 +01:00
Shuto Yano
48b0164676 [InstagramBridge] Fix instagram GraphSidecar output and Video embedding (#1361)
* [InstagramBridge] Fix GraphSidecar output

Fix following issues which related to output of the GraphSidecar type posts.
- The GraphSidecar post's media wasn't outputted except for first picture when searching by hashtag or location
- Video didn't embedded
NOTE:
The function getInstagramStory() which was called when the post type is GraphSidecar didn't seem to work just as one intended.
Because the web request called in that function is just to get the media of single post, NOT to get the media of Story.
But I don't have any idea to solve #694, so it seems be better to rename these function and member variable properly.
2019-12-01 11:25:20 +01:00
somini
4b3c3c58d2 [DiárioDoAlentejo] Add new bridge (#1360) 2019-12-01 11:18:45 +01:00
somini
60768b4885 [DisplayAction] Don't return redirect error codes (#1359)
This might lead to redirect loops.

See
https://github.com/RSS-Bridge/rss-bridge/pull/1071#issuecomment-515632848

Cherry-picked from eb21d6f.
2019-12-01 11:13:57 +01:00
Eugene Molotov
02dd778124 [VkBridge] Save internal links in posts and get hashtags before making item (#1363) 2019-11-18 10:51:35 +01:00
Eugene Molotov
5b63121e92 [VkBridge] Change access token (#1357)
Previous access token was revoked
2019-11-09 18:51:16 +01:00
St. John Johnson
49019a843f [ComicsKingdomBridge] Add new bridge (#1353) 2019-11-09 18:50:08 +01:00
Roliga
d65714fa47 [HtmlFormat] Add syndication links (#1348)
Adds <link> elements for each additional output format in the <head> of
HTML format output to allow RSS readers to find the actual feeds
directly from the HTML page.
2019-11-09 18:43:21 +01:00
Joseph
8161829ad5 [OpenwhydBridge] Add new bridge (#1338)
Rename WhydBridge to OpenwhydBridge
2019-11-09 18:36:50 +01:00
Nemo
7f35fc9f6b [AppleAppStoreBridge] Add new bridge (#1316)
This bridge allows you to follow iOS/iPad application updates
directly over RSS. This allows you to get notified of an application
update even if you don't have a iDevice. This may be useful in cases
where you want to be notified of a competitor's application for eg.

I built this after I got tired of waiting for WhatsApp to push their
security release on iOS. It shows up on the AppStore 7 days later
2019-11-09 18:28:00 +01:00
logmanoriginal
3bc8c9468a phpcs: Always use long array syntax
Most of the code in RSS-Bridge uses the long array syntax.
This commit adds a check to enforce using this syntax over
the short array syntax.

All failures have been fixed.
2019-11-01 18:06:55 +01:00
logmanoriginal
1df3598a74 [Dockerfile] Drop minimum security level back to TLS 1.0
Debian increased the minimum security level for OpenSSL from TLS 1.0
to TLS 1.2 [1] which also affects the Debian-based PHP image for Docker.

This change can break some bridges which have to connect to servers with
lower security level. Since all browsers still connect to these servers,
so should RSS-Bridge.

Note that according to [2] Mozilla, Firefox, Microsoft, Google and Apple
plan to increase the minimum security level to TLS 1.2 around March 2020.
At this time RSS-Bridge should follow the browser changes.

This commit updates the Dockerfile to automatically drop the minimum
security level back to TLS 1.0.

Based on the solution provided by @theScrabi in #1318

[1] https://wiki.debian.org/ContinuousIntegration/TriagingTips/openssl-1.1.1
[2] 553fc8e61f/debian/libssl1.1.NEWS
2019-11-01 17:12:45 +01:00
logmanoriginal
5f64fe2516 [BridgeAbstract] Fix broken assignment of defaultValue
setInputs() currently looks if the global array defines a 'value'
for a given parameter, but that isn't supported by the API. It
needs to be 'defaultValue'.
2019-11-01 15:29:16 +01:00
logmanoriginal
50eee7e7b3 [KununuBridge] Add feed item limit
This bridge currently takes a very long time to process
all news items on the page, when in many cases only one
or two had been added since the last check.

This commit adds a new parameter 'limit', which defines
the maximum number of items to add to the feed. This is
an optional paramter that defaults to 3.
2019-11-01 15:27:35 +01:00
logmanoriginal
c0df9815c7 [DesoutterBridge] Add feed item limit
This bridge currently takes a very long time to process
all news items on the page, when in many cases only one
or two had been added since the last check.

This commit adds a new parameter 'limit', which defines
the maximum number of items to add to the feed. This is
an optional paramter that defaults to 3.
2019-11-01 15:07:25 +01:00
Léo Maradan
46d5895d1d [RedditBridge] Add new bridge (#1213) 2019-11-01 13:54:03 +01:00
Anchit Bajaj
7c16aaf303 [VarietyBridge] Add new bridge (#1307) 2019-11-01 13:48:09 +01:00
LogMANOriginal
cdc1d9c9ba action: Add action to check bridge connectivity (#1147)
* action: Add action to check bridge connectivity

It is currently not simply possible to check if the remote
server for a bridge is reachable or not, which means some
of the bridges might no longer work because the server is
no longer on the internet.

In order to find those bridges we can either check each
bridge individually (which takes a lot of effort), or use
an automated script to do this for us.

If a server is no longer reachable it could mean that it is
temporarily unavailable, or shutdown permanently. The results
of this script will at least help identifying such servers.

* [Connectivity] Use Bootstrap container to properly display contents

* [Connectivity] Limit connectivity checks to debug mode

Connectivity checks take a long time to execute and can require a lot
of bandwidth. Therefore, administrators should be able to determine
when and who is able to utilize this action. The best way to prevent
regular users from accessing this action is by making it available in
debug mode only (public servers should never run in debug mode anyway).

* [Connectivity] Split implemenation into multiple files

* [Connectivity] Make web page responsive to user input

* [Connectivity] Make status message sticky

* [Connectivity] Add icon to the status message

* [contents] Add the ability for getContents to return header information

* [Connectivity] Add header information to the reply Json data

* [Connectivity] Add new status (blue) for redirected sites

Also adds titles to status icons (Successful, Redirected, Inactive, Failed)

* [Connectivity] Fix show doesn't work for inactive bridges

* [Connectivity] Fix typo

* [Connectivity] Catch errors in promise chains

* [Connectivity] Allow search by status and update dynamically

* [Connectivity] Add a progress bar

* [Connectivity] Use bridge factory

* [Connectivity] Import Bootstrap v4.3.1 CSS
2019-10-31 22:02:38 +01:00
LogMANOriginal
6bc83310b9 core: Add info button for input fields with title (#1173)
The current solution for titles on input boxes is not obvious to the
user as support varies between bridges. This commit adds an button to
all input boxes with titles in order to make it clear to the user that
additional information is available.
2019-10-31 21:09:44 +01:00
Roliga
c8d5c85c76 formats: Add getMimeType() function (#1299)
Allows getting the expected MIME type of the format's output. A
corresponding MIME_TYPE constant is also defined in FormatAbstract for
the format implementations to overwrite.
2019-10-31 19:00:12 +01:00
somini
d1e4bd7285 [YahtzeeDevDiary] Add new bridge (#1297) 2019-10-31 18:55:08 +01:00
LogMANOriginal
1022b5fdf9 core: Add an option to suppress error reporting (#1179)
Error reporting currently takes place for each error. This can result
in many error messages if a server has connectivity issues (i.e. when
it re-connects to the internet every 24 hours).

This commit adds a new option to the configuration file to define the
number of error reports to suppress before returning an error message
to the user.

Error reports are cached and therefore automatically purged after 24
hours. A successful bridge request does **not** clear the error count
as sporadic issues can be the result of actual problems on the server.

The implementation currently makes no assumption on the type of error,
which means it also suppresses bridge errors in debug mode. The default
value is, however, set to 1 which means all errors are reported.

References #994
2019-10-31 18:49:45 +01:00
LogMANOriginal
e8536ac1b2 core: Add an option to return errors in different formats (#1071)
Bridge errors are currently included as part of the feed to
notify users about erroneous bridges (before that, bridges
silently failed).

This solution, however, can produce a high load of error
messages if servers are down (see #994 for more details).

Admins may also not want to include error messages in feeds
in order to keep those kind of problems away from users or
simply to silently fail by choice.

This commit adds a new configuration section "error" with
one option "output" which can be set to following values:

"feed": To include error messages in the feed (default)
"http": To return a HTTP header for each error
"none": To disable error reporting

Note that errors are always logged to 'error.log' independent
of the settings above.

Closes #1066
2019-10-31 18:40:51 +01:00
Lyra
a0afe36d56 [DownDetectorBridge] Add per-website status fetch. Note that this only fetches the last downtime, as this is the only thing that the API provides. Moreover, the site uses a different ID for every company for every country, resulting in a very large array 2019-10-29 23:14:51 +01:00
Lyra
0b80f9d61c [DownDetectorBridge] Add bridge for DownDetector, and all local variants. Fixes
#1339.
2019-10-29 19:11:28 +01:00
somini
424075981f [EsquerdaNetBridge] Add new Bridge (#1296) 2019-10-29 18:58:12 +01:00
logmanoriginal
c334df91ec composer: Add all details to the composer file
The composer file currently lacks a lot of details, especially the
"name" and "description", but also "require-dev" and "suggest" info.

This commit adds many more details to the composer file and updates
composer.lock for this repository. Technically the project is ready
to be shipped as composer package.
2019-10-28 20:01:19 +01:00
Dominik Thiemermann
f2346fb33e [RevolutBridge] Add new bridge (#1321)
* [RevolutBridge] Add new bridge
2019-10-28 19:49:01 +01:00
Matt DeMoss
8a21fd1476 [BloombergBridge] Remove after site redesign and paywall. (#1238) 2019-10-28 19:27:56 +01:00
somini
2ac44172ac Facebook: Clarify Facebook bridges (#1221)
* Clarify Facebook bridges status

Distinguish between both Facebook bridges by their title.
This preserves all existing URLs.

* Update all URLs to secure HTTPS versions.
* Configure author name abbreviation
* Improve feed names

Use the correct feed name on each bridge.
Make sure the feed names don't repeat the "Facebook" name.
2019-10-28 19:01:04 +01:00
Roliga
4c78721f03 [ParameterValidator] Ensure context has all user provided parameters (#1211)
* [ParameterValidator] Ensure context has all fields

Previously if a bridge had a set of parameters like:

const PARAMETERS = array(
    'ContextA' => array(
        'Param1' => array(
            'name' => 'Param1',
            'required' => true
        )
    ),
    'ContextB' => array(
        'Param1' => array(
            'name' => 'Param1',
            'required' => true
        ),
        'Param2' => array(
            'name' => 'Param2',
            'required' => true
        )
    )
)

and a query specifying both Param1 and Param2 was provided a 'Mixed
context parameters' error would be returned. This change ensures
ContextA in the above example would not be considered a relevant context.
2019-10-28 17:50:55 +01:00
Christian Archer
04be85996d [BastaBridge] Fix PHP 7.4 crash (#1323)
* Inline the function
2019-10-24 21:57:14 +02:00
Joseph
59be6bded2 [GoogleSearchBridge] Replace 'div[id=ires]' with 'div[id=res]' (#1329) 2019-10-16 21:44:41 +02:00
Joseph
46873e14fe [GoogleSearchBridge] Use getURI() to build URLs (#1330)
* [GoogleSearchBridge] Use getURI() to build URLs
2019-10-16 21:44:28 +02:00
Joseph
0f01cc97a4 [StoriesIGBridge] Add timestamp to feed items (#1331) 2019-10-16 21:44:01 +02:00
Joseph
a70e00a76d [SuperbWallpapersBridge] Delete bridge (#1336) 2019-10-16 21:43:38 +02:00
Joseph
f0260c62c3 [StoriesIGBridge] Use getName() to create custom feed titles (#1332)
* [StoriesIGBridge] Use getName()
2019-10-16 21:41:47 +02:00
Roliga
fc5a1526ca [BandcampBridge] Add band and album feeds (#1317)
* [BandcampBridge] Add band and artist feeds

This can return a limited number of the most recent releases by a band,
or a single release/album. Each release may be given a unique article ID
depending on its track list with the "Releases, new one when track track
changes" option, which should make them show up as new articles when
tracks are added or removed. Releases may also be split up to individual
articles for each track with the "Individual tracks" option.

This uses and undocumented API from the Bandcamp Android app. It's much
faster than loading and parsing the website HTML, and seems to fail less
often with more relaxed rate limits. It's still far from perfect in that
regard though.

The "Individual tracks" option generates requests for each individual
track so that can quickly run into rate limits.

The "Individual tracks" option also has a quirk where tracks released
under e.g. a music label will have their artist set to the label instead
of the actual artist of the track. This is a limitation of the API.
2019-10-16 21:37:25 +02:00
Joseph
4c0e234479 [Bridges] Use HTTPS (#1337)
* [Rule34pahealBridge] Use HTTPS
* [KonachanBridge] Use HTTPS
* [Rule34Bridge] Use HTTPS
* [SafebooruBridge] Use HTTPS
* [TbibBridge] Use HTTPS
* [XbooruBridge] Use HTTPS
* [ScmbBridge] Use HTTPS
* [ReporterreBridge] Use HTTPS
* [BastaBridge] Use HTTPS
* [NiceMatinBridge] Use HTTPS
* [ScoopItBridge] Use HTTPS
* [TheCodingLoveBridge] Use HTTPS
* [Shimmie2Bridge] Use HTTPS
* [HDWallpapersBridge] Use HTTPS
* [GiphyBridge] Use HTTPS
* [PickyWallpapersBridge] Use HTTPS
* [ParuVenduImmoBridge] Use HTTPS
* [ElsevierBridge] Use HTTPS
* [CastorusBridge] Use HTTPS
* [CollegeDeFranceBridge] Use HTTPS
* [MangareaderBridge] Use HTTPS
2019-10-16 21:34:28 +02:00
somini
0eab63d728 Update Facebook URL detection (#1334)
* add detectParameters to FacebookBridge.php
2019-10-16 21:32:29 +02:00
floviolleau
b0884e9158 [VieDeMerdeBridge] Add new bridge for quotes from Vie de Merde (#1313)
* Add new bridge for quotes from Vie de Merde
2019-10-03 22:36:08 +02:00
Nicolas Delsaux
b4581418d4 [PlantUMLBridge] Added bridge for PlantUML. Fixes #1191
* Fixes #1191 by implementing the RSS feed of PlantUML releases
2019-10-03 22:30:22 +02:00
sysadminstory
af1566f40d [ZoneTelechargementBridge] URL and name change (#1302)
Annuaire Telechargement has change name again to go back to Annuaire
Telechargement. Fixes #1279
2019-10-03 22:27:10 +02:00
sysadminstory
529e0d0cca [ExtremeDownloadBridge] Update Website URL (#1303)
Website URL was changed.
2019-10-03 22:25:56 +02:00
Paróczai Olivér
a3532804ac [Readme] Small grammar fixes (#1312) 2019-10-03 22:25:05 +02:00
Anchit Bajaj
8c19146d29 [ListverseBridge - add new bridge (#1305) 2019-10-03 22:24:14 +02:00
Anchit Bajaj
2a3d5865ad [FreeCodeCampBridge] - rss feed for FreeCodeCamp (#1311) 2019-10-03 22:23:14 +02:00
Lyra
4d36c9dc30 Merge branch 'master' of github.com:RSS-Bridge/rss-bridge 2019-10-03 22:14:33 +02:00
Lyra
a2e47a88c3 [InstagramBridge] Add option to get direct links 2019-10-03 22:14:21 +02:00
Anchit Bajaj
b09f50853f [ViceBridge] - RSS feed for Vice Publications. (#1310)
* [ViceBridge] - RSS feed for Vice Publications.
2019-10-03 22:02:30 +02:00
lukasklinger
9b5bf565b3 [N26Bridge] Updated bridge to reflect changes on N26 blog (#1295)
N26 made some changes to their blog, this commit fixes the N26Bridge
2019-10-03 21:58:57 +02:00
Lyra
5cc956367f [core] Fix travis 2019-10-03 21:46:49 +02:00
Lyra
548e28249b [ThePirateBayBridge] Remove nested function 2019-10-03 21:46:24 +02:00
Lyra
684c69b0cd [Releases3DSBridge] Remove nested functions 2019-10-03 21:46:09 +02:00
Lyra
3dae4e0801 [JapanExpoBridge] Remove nested function 2019-10-03 21:45:51 +02:00
Lyra
4622d9be1e [ReadComicsBridge] Deleted bridge since website no longer exists 2019-10-03 21:41:22 +02:00
Nicolas Delsaux
76183dcd44 [GQMagazineBridge] Fix article body detection again (Fixes #1280) 2019-10-03 21:26:41 +02:00
Eugene Molotov
50b234d893 [VkBridge] Photo and timestamp fixes (#1287)
* [VkBridge] Correct parsing of photos, fix timestamp for old posts
2019-09-16 21:30:27 +02:00
Eugene Molotov
af48f36fd2 [VkBridge] Switch maintainer (#1288) 2019-09-16 21:29:45 +02:00
Eugene Molotov
7f6ca23e8f [PikabuBridge] Preserve links (#1286)
* [PikabuBridge] Preserve links
2019-09-16 21:28:41 +02:00
oratosquilla-oratoria
1daef22a3d [NFLRUSBridge] Add new bridge (#1285)
* [NFLRUSBridge] Add new bridge
2019-09-16 21:27:01 +02:00
killruana
c694810d9a [MediapartBridge] Fix article parsing
* Only process article item, fix issue #1292
2019-09-16 21:26:19 +02:00
ORelio
f12f6a2dba [DarkReading] Add DarkReading Bridge (#1289) 2019-09-16 21:25:28 +02:00
Lyra
b1be45df6c [Configuration] Bump version to dev.2019-09-12 2019-09-12 17:09:30 +02:00
185 changed files with 16974 additions and 1444 deletions

View File

@@ -3,9 +3,15 @@ FROM php:7-apache
ENV APACHE_DOCUMENT_ROOT=/app ENV APACHE_DOCUMENT_ROOT=/app
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \ RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
&& apt-get --yes update && apt-get --yes install libxml2-dev \ && apt-get --yes update \
&& docker-php-ext-install -j$(nproc) simplexml \ && apt-get --yes --no-install-recommends install \
zlib1g-dev \
libmemcached-dev \
&& pecl install memcached \
&& docker-php-ext-enable memcached \
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \ && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
&& sed -ri -e 's/(MinProtocol\s*=\s*)TLSv1\.2/\1None/' /etc/ssl/openssl.cnf \
&& sed -ri -e 's/(CipherString\s*=\s*DEFAULT)@SECLEVEL=2/\1/' /etc/ssl/openssl.cnf
COPY --chown=www-data:www-data ./ /app/ COPY --chown=www-data:www-data ./ /app/

View File

@@ -2,7 +2,7 @@
=== ===
[![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/) [![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. RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one. It can be used on webservers or as a stand-alone application in CLI mode.
**Important**: RSS-Bridge is __not__ a feed reader or feed aggregator, but a tool to generate feeds that are consumed by feed readers and feed aggregators. Find a list of feed aggregators on [Wikipedia](https://en.wikipedia.org/wiki/Comparison_of_feed_aggregators). **Important**: RSS-Bridge is __not__ a feed reader or feed aggregator, but a tool to generate feeds that are consumed by feed readers and feed aggregators. Find a list of feed aggregators on [Wikipedia](https://en.wikipedia.org/wiki/Comparison_of_feed_aggregators).
@@ -65,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) - [`simplexml`](https://secure.php.net/manual/en/book.simplexml.php)
- [`curl`](https://secure.php.net/manual/en/book.curl.php) - [`curl`](https://secure.php.net/manual/en/book.curl.php)
- [`json`](https://secure.php.net/manual/en/book.json.php) - [`json`](https://secure.php.net/manual/en/book.json.php)
- [`filter`](https://secure.php.net/manual/en/book.filter.php)
- [`sqlite3`](http://php.net/manual/en/book.sqlite3.php) (only when using SQLiteCache) - [`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) Find more information on our [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
@@ -76,7 +77,7 @@ RSS-Bridge allows you to take full control over which bridges are displayed to t
Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting)
**Notice**: By default RSS-Bridge will only show a small subset of bridges. Make sure to read up on [whitelisting](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) to unlock the full potential of RSS-Bridge! **Notice**: By default, RSS-Bridge will only show a small subset of bridges. Make sure to read up on [whitelisting](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) to unlock the full potential of RSS-Bridge!
Deploy Deploy
=== ===
@@ -118,19 +119,26 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [alex73](https://github.com/alex73) * [alex73](https://github.com/alex73)
* [alexAubin](https://github.com/alexAubin) * [alexAubin](https://github.com/alexAubin)
* [AmauryCarrade](https://github.com/AmauryCarrade) * [AmauryCarrade](https://github.com/AmauryCarrade)
* [AntoineTurmel](https://github.com/AntoineTurmel)
* [arnd-s](https://github.com/arnd-s)
* [ArthurHoaro](https://github.com/ArthurHoaro) * [ArthurHoaro](https://github.com/ArthurHoaro)
* [Astalaseven](https://github.com/Astalaseven) * [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42) * [Astyan-42](https://github.com/Astyan-42)
* [AxorPL](https://github.com/AxorPL)
* [ayacoo](https://github.com/ayacoo)
* [az5he6ch](https://github.com/az5he6ch) * [az5he6ch](https://github.com/az5he6ch)
* [azdkj532](https://github.com/azdkj532) * [azdkj532](https://github.com/azdkj532)
* [b1nj](https://github.com/b1nj) * [b1nj](https://github.com/b1nj)
* [benasse](https://github.com/benasse) * [benasse](https://github.com/benasse)
* [Binnette](https://github.com/Binnette)
* [captn3m0](https://github.com/captn3m0) * [captn3m0](https://github.com/captn3m0)
* [chemel](https://github.com/chemel) * [chemel](https://github.com/chemel)
* [ckiw](https://github.com/ckiw) * [ckiw](https://github.com/ckiw)
* [cnlpete](https://github.com/cnlpete) * [cnlpete](https://github.com/cnlpete)
* [corenting](https://github.com/corenting) * [corenting](https://github.com/corenting)
* [couraudt](https://github.com/couraudt) * [couraudt](https://github.com/couraudt)
* [csisoap](https://github.com/csisoap)
* [cyberjacob](https://github.com/cyberjacob)
* [da2x](https://github.com/da2x) * [da2x](https://github.com/da2x)
* [Daiyousei](https://github.com/Daiyousei) * [Daiyousei](https://github.com/Daiyousei)
* [dawidsowa](https://github.com/dawidsowa) * [dawidsowa](https://github.com/dawidsowa)
@@ -138,58 +146,80 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [DJCrashdummy](https://github.com/DJCrashdummy) * [DJCrashdummy](https://github.com/DJCrashdummy)
* [Djuuu](https://github.com/Djuuu) * [Djuuu](https://github.com/Djuuu)
* [DnAp](https://github.com/DnAp) * [DnAp](https://github.com/DnAp)
* [dominik-th](https://github.com/dominik-th)
* [Draeli](https://github.com/Draeli) * [Draeli](https://github.com/Draeli)
* [Dreckiger-Dan](https://github.com/Dreckiger-Dan) * [Dreckiger-Dan](https://github.com/Dreckiger-Dan)
* [em92](https://github.com/em92) * [em92](https://github.com/em92)
* [eMerzh](https://github.com/eMerzh) * [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM) * [EtienneM](https://github.com/EtienneM)
* [fanch317](https://github.com/fanch317)
* [floviolleau](https://github.com/floviolleau) * [floviolleau](https://github.com/floviolleau)
* [fluffy-critter](https://github.com/fluffy-critter) * [fluffy-critter](https://github.com/fluffy-critter)
* [Frenzie](https://github.com/Frenzie) * [Frenzie](https://github.com/Frenzie)
* [fulmeek](https://github.com/fulmeek) * [fulmeek](https://github.com/fulmeek)
* [ggiessen](https://github.com/ggiessen)
* [Ginko-Aloe](https://github.com/Ginko-Aloe) * [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [Glandos](https://github.com/Glandos) * [Glandos](https://github.com/Glandos)
* [gloony](https://github.com/gloony)
* [GregThib](https://github.com/GregThib) * [GregThib](https://github.com/GregThib)
* [griffaurel](https://github.com/griffaurel) * [griffaurel](https://github.com/griffaurel)
* [Grummfy](https://github.com/Grummfy) * [Grummfy](https://github.com/Grummfy)
* [gsantner](https://github.com/gsantner)
* [hunhejj](https://github.com/hunhejj) * [hunhejj](https://github.com/hunhejj)
* [husim0](https://github.com/husim0) * [husim0](https://github.com/husim0)
* [IceWreck](https://github.com/IceWreck) * [IceWreck](https://github.com/IceWreck)
* [j0k3r](https://github.com/j0k3r) * [j0k3r](https://github.com/j0k3r)
* [JackNUMBER](https://github.com/JackNUMBER) * [JackNUMBER](https://github.com/JackNUMBER)
* [jannyba](https://github.com/jannyba)
* [JasonGhent](https://github.com/JasonGhent)
* [jdesgats](https://github.com/jdesgats)
* [jdigilio](https://github.com/jdigilio) * [jdigilio](https://github.com/jdigilio)
* [JeremyRand](https://github.com/JeremyRand) * [JeremyRand](https://github.com/JeremyRand)
* [Jocker666z](https://github.com/Jocker666z) * [Jocker666z](https://github.com/Jocker666z)
* [johnnygroovy](https://github.com/johnnygroovy) * [johnnygroovy](https://github.com/johnnygroovy)
* [killruana](https://github.com/killruana) * [johnpc](https://github.com/johnpc)
* [joni1993](https://github.com/joni1993)
* [joshcoales](https://github.com/joshcoales)
* [klimplant](https://github.com/klimplant) * [klimplant](https://github.com/klimplant)
* [kolarcz](https://github.com/kolarcz)
* [kranack](https://github.com/kranack) * [kranack](https://github.com/kranack)
* [kraoc](https://github.com/kraoc) * [kraoc](https://github.com/kraoc)
* [l1n](https://github.com/l1n) * [l1n](https://github.com/l1n)
* [laBecasse](https://github.com/laBecasse) * [laBecasse](https://github.com/laBecasse)
* [lagaisse](https://github.com/lagaisse) * [lagaisse](https://github.com/lagaisse)
* [lalannev](https://github.com/lalannev) * [lalannev](https://github.com/lalannev)
* [Leomaradan](https://github.com/Leomaradan)
* [ldidry](https://github.com/ldidry) * [ldidry](https://github.com/ldidry)
* [Leomaradan](https://github.com/Leomaradan)
* [liamka](https://github.com/liamka)
* [Limero](https://github.com/Limero) * [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal) * [LogMANOriginal](https://github.com/LogMANOriginal)
* [lorenzos](https://github.com/lorenzos) * [lorenzos](https://github.com/lorenzos)
* [lukasklinger](https://github.com/lukasklinger)
* [m0zes](https://github.com/m0zes) * [m0zes](https://github.com/m0zes)
* [matthewseal](https://github.com/matthewseal) * [matthewseal](https://github.com/matthewseal)
* [mcbyte-it](https://github.com/mcbyte-it) * [mcbyte-it](https://github.com/mcbyte-it)
* [mdemoss](https://github.com/mdemoss) * [mdemoss](https://github.com/mdemoss)
* [melangue](https://github.com/melangue) * [melangue](https://github.com/melangue)
* [metaMMA](https://github.com/metaMMA) * [metaMMA](https://github.com/metaMMA)
* [mibe](https://github.com/mibe)
* [mightymt](https://github.com/mightymt)
* [mitsukarenai](https://github.com/mitsukarenai) * [mitsukarenai](https://github.com/mitsukarenai)
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours) * [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
* [mr-flibble](https://github.com/mr-flibble) * [mr-flibble](https://github.com/mr-flibble)
* [mro](https://github.com/mro) * [mro](https://github.com/mro)
* [mschwld](https://github.com/mschwld)
* [mxmehl](https://github.com/mxmehl) * [mxmehl](https://github.com/mxmehl)
* [nel50n](https://github.com/nel50n) * [nel50n](https://github.com/nel50n)
* [niawag](https://github.com/niawag) * [niawag](https://github.com/niawag)
* [Niehztog](https://github.com/Niehztog)
* [Nono-m0le](https://github.com/Nono-m0le) * [Nono-m0le](https://github.com/Nono-m0le)
* [ObsidianWitch](https://github.com/ObsidianWitch) * [ObsidianWitch](https://github.com/ObsidianWitch)
* [OliverParoczai](https://github.com/OliverParoczai)
* [Ololbu](https://github.com/Ololbu)
* [ORelio](https://github.com/ORelio) * [ORelio](https://github.com/ORelio)
* [otakuf](https://github.com/otakuf)
* [Park0](https://github.com/Park0)
* [Paroleen](https://github.com/Paroleen)
* [PaulVayssiere](https://github.com/PaulVayssiere) * [PaulVayssiere](https://github.com/PaulVayssiere)
* [pellaeon](https://github.com/pellaeon) * [pellaeon](https://github.com/pellaeon)
* [Piranhaplant](https://github.com/Piranhaplant) * [Piranhaplant](https://github.com/Piranhaplant)
@@ -199,27 +229,39 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [Pofilo](https://github.com/Pofilo) * [Pofilo](https://github.com/Pofilo)
* [prysme01](https://github.com/prysme01) * [prysme01](https://github.com/prysme01)
* [quentinus95](https://github.com/quentinus95) * [quentinus95](https://github.com/quentinus95)
* [RawkBob](https://github.com/RawkBob)
* [regisenguehard](https://github.com/regisenguehard) * [regisenguehard](https://github.com/regisenguehard)
* [Riduidel](https://github.com/Riduidel) * [Riduidel](https://github.com/Riduidel)
* [rogerdc](https://github.com/rogerdc) * [rogerdc](https://github.com/rogerdc)
* [Roliga](https://github.com/Roliga) * [Roliga](https://github.com/Roliga)
* [ronansalmon](https://github.com/ronansalmon)
* [rremizov](https://github.com/rremizov)
* [sebsauvage](https://github.com/sebsauvage) * [sebsauvage](https://github.com/sebsauvage)
* [shutosg](https://github.com/shutosg)
* [Simounet](https://github.com/Simounet)
* [somini](https://github.com/somini) * [somini](https://github.com/somini)
* [squeek502](https://github.com/squeek502) * [squeek502](https://github.com/squeek502)
* [stjohnjohnson](https://github.com/stjohnjohnson)
* [Strubbl](https://github.com/Strubbl) * [Strubbl](https://github.com/Strubbl)
* [sublimz](https://github.com/sublimz) * [sublimz](https://github.com/sublimz)
* [sunchaserinfo](https://github.com/sunchaserinfo)
* [SuperSandro2000](https://github.com/SuperSandro2000)
* [sysadminstory](https://github.com/sysadminstory) * [sysadminstory](https://github.com/sysadminstory)
* [tameroski](https://github.com/tameroski) * [tameroski](https://github.com/tameroski)
* [teromene](https://github.com/teromene) * [teromene](https://github.com/teromene)
* [tgkenney](https://github.com/tgkenney)
* [thefranke](https://github.com/thefranke) * [thefranke](https://github.com/thefranke)
* [ThePadawan](https://github.com/ThePadawan) * [ThePadawan](https://github.com/ThePadawan)
* [TheRadialActive](https://github.com/TheRadialActive) * [TheRadialActive](https://github.com/TheRadialActive)
* [theScrabi](https://github.com/theScrabi)
* [TitiTestScalingo](https://github.com/TitiTestScalingo)
* [triatic](https://github.com/triatic) * [triatic](https://github.com/triatic)
* [VerifiedJoseph](https://github.com/VerifiedJoseph) * [VerifiedJoseph](https://github.com/VerifiedJoseph)
* [WalterBarrett](https://github.com/WalterBarrett) * [WalterBarrett](https://github.com/WalterBarrett)
* [wtuuju](https://github.com/wtuuju) * [wtuuju](https://github.com/wtuuju)
* [xurxof](https://github.com/xurxof) * [xurxof](https://github.com/xurxof)
* [yardenac](https://github.com/yardenac) * [yardenac](https://github.com/yardenac)
* [ymeister](https://github.com/ymeister)
* [ZeNairolf](https://github.com/ZeNairolf) * [ZeNairolf](https://github.com/ZeNairolf)
Licenses Licenses
@@ -229,6 +271,7 @@ The source code for RSS-Bridge is [Public Domain](UNLICENSE).
RSS-Bridge uses third party libraries with their own license: RSS-Bridge uses third party libraries with their own license:
* [`Parsedown`](https://github.com/erusev/parsedown) licensed under the [MIT License](http://opensource.org/licenses/MIT)
* [`PHP Simple HTML DOM Parser`](http://simplehtmldom.sourceforge.net/) licensed under the [MIT License](http://opensource.org/licenses/MIT) * [`PHP Simple HTML DOM Parser`](http://simplehtmldom.sourceforge.net/) licensed under the [MIT License](http://opensource.org/licenses/MIT)
* [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](http://opensource.org/licenses/MIT) * [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](http://opensource.org/licenses/MIT)

View File

@@ -0,0 +1,136 @@
<?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
*/
/**
* Checks if the website for a given bridge is reachable.
*
* **Remarks**
* - This action is only available in debug mode.
* - Returns the bridge status as Json-formatted string.
* - Returns an error if the bridge is not whitelisted.
* - Returns a responsive web page that automatically checks all whitelisted
* bridges (using JavaScript) if no bridge is specified.
*/
class ConnectivityAction extends ActionAbstract {
public function execute() {
if(!Debug::isEnabled()) {
returnError('This action is only available in debug mode!');
}
if(!isset($this->userData['bridge'])) {
$this->returnEntryPage();
return;
}
$bridgeName = $this->userData['bridge'];
$this->reportBridgeConnectivity($bridgeName);
}
/**
* Generates a report about the bridge connectivity status and sends it back
* to the user.
*
* The report is generated as Json-formatted string in the format
* {
* "bridge": "<bridge-name>",
* "successful": true/false
* }
*
* @param string $bridgeName Name of the bridge to generate the report for
* @return void
*/
private function reportBridgeConnectivity($bridgeName) {
$bridgeFac = new \BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
if(!$bridgeFac->isWhitelisted($bridgeName)) {
header('Content-Type: text/html');
returnServerError('Bridge is not whitelisted!');
}
header('Content-Type: text/json');
$retVal = array(
'bridge' => $bridgeName,
'successful' => false,
'http_code' => 200,
);
$bridge = $bridgeFac->create($bridgeName);
if($bridge === false) {
echo json_encode($retVal);
return;
}
$curl_opts = array(
CURLOPT_CONNECTTIMEOUT => 5
);
try {
$reply = getContents($bridge::URI, array(), $curl_opts, true);
if($reply) {
$retVal['successful'] = true;
if (isset($reply['header'])) {
if (strpos($reply['header'], 'HTTP/1.1 301 Moved Permanently') !== false) {
$retVal['http_code'] = 301;
}
}
}
} catch(Exception $e) {
$retVal['successful'] = false;
}
echo json_encode($retVal);
}
private function returnEntryPage() {
echo <<<EOD
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="static/bootstrap.min.css">
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"
integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/"
crossorigin="anonymous">
<link rel="stylesheet" href="static/connectivity.css">
<script src="static/connectivity.js" type="text/javascript"></script>
</head>
<body>
<div id="main-content" class="container">
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div id="status-message" class="sticky-top alert alert-primary alert-dismissible fade show" role="alert">
<i id="status-icon" class="fas fa-sync"></i>
<span>...</span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close" onclick="stopConnectivityChecks()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<input type="text" class="form-control" id="search" onkeyup="search()" placeholder="Search for bridge..">
</div>
</body>
</html>
EOD;
}
}

View File

@@ -12,6 +12,15 @@
*/ */
class DisplayAction extends ActionAbstract { class DisplayAction extends ActionAbstract {
private function get_return_code($error) {
$returnCode = $error->getCode();
if ($returnCode === 301 || $returnCode === 302) {
# Don't pass redirect codes to the exterior
$returnCode = 508;
}
return $returnCode;
}
public function execute() { public function execute() {
$bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null; $bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null;
@@ -146,63 +155,77 @@ class DisplayAction extends ActionAbstract {
} catch(Error $e) { } catch(Error $e) {
error_log($e); error_log($e);
$item = new \FeedItem(); if(logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) {
if(Configuration::getConfig('error', 'output') === 'feed') {
$item = new \FeedItem();
// Create "new" error message every 24 hours // Create "new" error message every 24 hours
$this->userData['_error_time'] = urlencode((int)(time() / 86400)); $this->userData['_error_time'] = urlencode((int)(time() / 86400));
// Error 0 is a special case (i.e. "trying to get property of non-object") // Error 0 is a special case (i.e. "trying to get property of non-object")
if($e->getCode() === 0) { if($e->getCode() === 0) {
$item->setTitle( $item->setTitle(
'Bridge encountered an unexpected situation! (' 'Bridge encountered an unexpected situation! ('
. $this->userData['_error_time'] . $this->userData['_error_time']
. ')' . ')'
); );
} else { } else {
$item->setTitle( $item->setTitle(
'Bridge returned error ' 'Bridge returned error '
. $e->getCode() . $e->getCode()
. '! (' . '! ('
. $this->userData['_error_time'] . $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;
} elseif(Configuration::getConfig('error', 'output') === 'http') {
header('Content-Type: text/html', true, $this->get_return_code($e));
die(buildTransformException($e, $bridge));
}
} }
$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) { } catch(Exception $e) {
error_log($e); error_log($e);
$item = new \FeedItem(); if(logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) {
if(Configuration::getConfig('error', 'output') === 'feed') {
$item = new \FeedItem();
// Create "new" error message every 24 hours // Create "new" error message every 24 hours
$this->userData['_error_time'] = urlencode((int)(time() / 86400)); $this->userData['_error_time'] = urlencode((int)(time() / 86400));
$item->setURI( $item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '') (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?' . '?'
. http_build_query($this->userData) . http_build_query($this->userData)
); );
$item->setTitle( $item->setTitle(
'Bridge returned error ' 'Bridge returned error '
. $e->getCode() . $e->getCode()
. '! (' . '! ('
. $this->userData['_error_time'] . $this->userData['_error_time']
. ')' . ')'
); );
$item->setTimestamp(time()); $item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge)); $item->setContent(buildBridgeException($e, $bridge));
$items[] = $item; $items[] = $item;
} elseif(Configuration::getConfig('error', 'output') === 'http') {
header('Content-Type: text/html', true, $this->get_return_code($e));
die(buildTransformException($e, $bridge));
}
}
} }
// Store data in cache // Store data in cache

View File

@@ -0,0 +1,57 @@
<?php
class ASRockNewsBridge extends BridgeAbstract {
const NAME = 'ASRock News Bridge';
const URI = 'https://www.asrock.com';
const DESCRIPTION = 'Returns latest news articles';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array();
const CACHE_TIMEOUT = 3600; // 1 hour
public function collectData() {
$html = getSimpleHTMLDOM(self::URI . '/news/index.asp')
or returnServerError('Could not request: ' . self::URI . '/news/index.asp');
$html = defaultLinkTo($html, self::URI . '/news/');
foreach($html->find('div.inner > a') as $index => $a) {
$item = array();
$articlePath = $a->href;
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, self::CACHE_TIMEOUT)
or returnServerError('Could not request: ' . $articlePath);
$articlePageHtml = defaultLinkTo($articlePageHtml, self::URI);
$contents = $articlePageHtml->find('div.Contents', 0);
$item['uri'] = $articlePath;
$item['title'] = $contents->find('h5', 0)->innertext;
$contents->find('h5', 0)->outertext = '';
$item['content'] = $contents->innertext;
$item['timestamp'] = $this->extractDate($a->plaintext);
$item['enclosures'][] = $a->find('img', 0)->src;
$this->items[] = $item;
if (count($this->items) >= 10) {
break;
}
}
}
private function extractDate($text) {
$dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/';
$text = trim($text);
if (preg_match($dateRegex, $text, $matches)) {
return $matches[1];
}
return '';
}
}

View File

@@ -0,0 +1,54 @@
<?php
class AirBreizhBridge extends BridgeAbstract {
const MAINTAINER = 'fanch317';
const NAME = 'Air Breizh';
const URI = 'https://www.airbreizh.asso.fr/';
const DESCRIPTION = 'Returns newests publications on Air Breizh';
const PARAMETERS = array(
'Publications' => array(
'theme' => array(
'name' => 'Thematique',
'type' => 'list',
'values' => array(
'Tout' => '',
'Rapport d\'activite' => 'rapport-dactivite',
'Etude' => 'etudes',
'Information' => 'information',
'Autres documents' => 'autres-documents',
'Plan Régional de Surveillance de la qualité de lair' => 'prsqa',
'Transport' => 'transport'
)
)
)
);
public function getIcon() {
return 'https://www.airbreizh.asso.fr/voy_content/uploads/2017/11/favicon.png';
}
public function collectData(){
$html = '';
$html = getSimpleHTMLDOM(static::URI . 'publications/?fwp_publications_thematiques=' . $this->getInput('theme'))
or returnClientError('No results for this query.');
foreach ($html->find('article') as $article) {
$item = array();
// Title
$item['title'] = $article->find('h2', 0)->plaintext;
// Author
$item['author'] = 'Air Breizh';
// Image
$imagelink = $article->find('.card__image', 0)->find('img', 0)->getAttribute('src');
// Content preview
$item['content'] = '<img src="' . $imagelink . '" />
<br/>'
. $article->find('.card__text', 0)->plaintext;
// URL
$item['uri'] = $article->find('.publi__buttons', 0)->find('a', 0)->getAttribute('href');
// ID
$item['id'] = $article->find('.publi__buttons', 0)->find('a', 0)->getAttribute('href');
$this->items[] = $item;
}
}
}

View File

@@ -0,0 +1,74 @@
<?php
class AlbionOnlineBridge extends BridgeAbstract {
const NAME = 'Albion Online Changelog';
const MAINTAINER = 'otakuf';
const URI = 'https://albiononline.com';
const DESCRIPTION = 'Returns the changes made to the Albion Online';
const CACHE_TIMEOUT = 3600; // 60min
const PARAMETERS = array( array(
'postcount' => array(
'name' => 'Limit',
'type' => 'number',
'title' => 'Maximum number of items to return',
'defaultValue' => 5,
),
'language' => array(
'name' => 'Language',
'type' => 'list',
'values' => array(
'English' => 'en',
'Deutsch' => 'de',
'Polski' => 'pl',
'Français' => 'fr',
'Русский' => 'ru',
'Português' => 'pt',
'Español' => 'es',
),
'title' => 'Language of changelog posts',
'defaultValue' => 'en',
),
'full' => array(
'name' => 'Full changelog',
'type' => 'checkbox',
'required' => false,
'title' => 'Enable to receive the full changelog post for each item'
),
));
public function collectData() {
$api = 'https://albiononline.com/';
// Example: https://albiononline.com/en/changelog/1/5
$url = $api . $this->getInput('language') . '/changelog/1/' . $this->getInput('postcount');
$html = getSimpleHTMLDOM($url)
or returnServerError('Unable to get changelog data from "' . $url . '"!');
foreach ($html->find('li') as $data) {
$item = array();
$item['uri'] = self::URI . $data->find('a', 0)->getAttribute('href');
$item['title'] = trim(explode('|', $data->find('span', 0)->plaintext)[0]);
// Time below work only with en lang. Need to think about solution. May be separate request like getFullChangelog, but to english list for all language
//print_r( date_parse_from_format( 'M j, Y' , 'Sep 9, 2020') );
//$item['timestamp'] = $this->extractDate($a->plaintext);
$item['author'] = 'albiononline.com';
if($this->getInput('full')) {
$item['content'] = $this->getFullChangelog($item['uri']);
} else {
//$item['content'] = trim(preg_replace('/\s+/', ' ', $data->find('span', 0)->plaintext));
// Just use title, no info at all or use title and date, see above
$item['content'] = $item['title'];
}
$item['uid'] = hash('sha256', $item['title']);
$this->items[] = $item;
}
}
private function getFullChangelog($url) {
$html = getSimpleHTMLDOMCached($url)
or returnServerError('Unable to load changelog post from "' . $url . '"!');
$html = defaultLinkTo($html, self::URI);
return $html->find('div.small-12.columns', 1)->innertext;
}
}

View File

@@ -8,14 +8,25 @@ class AllocineFRBridge extends BridgeAbstract {
const DESCRIPTION = 'Bridge for allocine.fr'; const DESCRIPTION = 'Bridge for allocine.fr';
const PARAMETERS = array( array( const PARAMETERS = array( array(
'category' => array( 'category' => array(
'name' => 'category', 'name' => 'Emission',
'type' => 'list', 'type' => 'list',
'exampleValue' => 'Faux Raccord', 'title' => 'Sélectionner l\'emission',
'title' => 'Select your category',
'values' => array( 'values' => array(
'Faux Raccord' => 'faux-raccord', 'Faux Raccord' => 'faux-raccord',
'Top 5' => 'top-5', 'Fanzone' => 'fanzone',
'Tueurs en Séries' => 'tueurs-en-serie' 'Game In Ciné' => 'game-in-cine',
'Pour la faire courte' => 'pour-la-faire-courte',
'Home Cinéma' => 'home-cinema',
'PILS - Par Ici Les Sorties' => 'pils-par-ici-les-sorties',
'AlloCiné : l\'émission, sur LeStream' => 'allocine-lemission-sur-lestream',
'Give Me Five' => 'give-me-five',
'Aviez-vous remarqué ?' => 'aviez-vous-remarque',
'Et paf, il est mort' => 'et-paf-il-est-mort',
'The Big Fan Theory' => 'the-big-fan-theory',
'Clichés' => 'cliches',
'Complètement...' => 'completement',
'#Fun Facts' => 'fun-facts',
'Origin Story' => 'origin-story',
) )
) )
)); ));
@@ -23,19 +34,30 @@ class AllocineFRBridge extends BridgeAbstract {
public function getURI(){ public function getURI(){
if(!is_null($this->getInput('category'))) { if(!is_null($this->getInput('category'))) {
switch($this->getInput('category')) { $categories = array(
case 'faux-raccord': 'faux-raccord' => 'video/programme-12284/saison-37054/',
$uri = static::URI . 'video/programme-12284/saison-32180/'; 'fanzone' => 'video/programme-12298/saison-37059/',
break; 'game-in-cine' => 'video/programme-12288/saison-22971/',
case 'top-5': 'pour-la-faire-courte' => 'video/programme-20960/saison-29678/',
$uri = static::URI . 'video/programme-12299/saison-29561/'; 'home-cinema' => 'video/programme-12287/saison-34703/',
break; 'pils-par-ici-les-sorties' => 'video/programme-25789/saison-37253/',
case 'tueurs-en-serie': 'allocine-lemission-sur-lestream' => 'video/programme-25123/saison-36067/',
$uri = static::URI . 'video/programme-12286/saison-22938/'; 'give-me-five' => 'video/programme-21919/saison-34518/',
break; 'aviez-vous-remarque' => 'video/programme-19518/saison-37084/',
} 'et-paf-il-est-mort' => 'video/programme-25113/saison-36657/',
'the-big-fan-theory' => 'video/programme-20403/saison-37419/',
'cliches' => 'video/programme-24834/saison-35591/',
'completement' => 'video/programme-23859/saison-34102/',
'fun-facts' => 'video/programme-23040/saison-32686/',
'origin-story' => 'video/programme-25667/saison-37041/'
);
return $uri; $category = $this->getInput('category');
if(array_key_exists($category, $categories)) {
return static::URI . $categories[$category];
} else {
returnClientError('Emission inconnue');
}
} }
return parent::getURI(); return parent::getURI();
@@ -63,23 +85,23 @@ class AllocineFRBridge extends BridgeAbstract {
self::PARAMETERS[$this->queriedContext]['category']['values'] self::PARAMETERS[$this->queriedContext]['category']['values']
); );
foreach($html->find('.media-meta-list figure.media-meta-fig') as $element) { foreach($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) {
$item = array(); $item = array();
$title = $element->find('div.titlebar h3.title a', 0); $title = $element->find('a[class*=meta-title-link]', 0);
$content = trim($element->innertext); $content = trim($element->outertext);
$figCaption = strpos($content, $category);
if($figCaption !== false) { // Replace image 'src' with the one in 'data-src'
$content = str_replace('src="/', 'src="' . static::URI, $content); $content = preg_replace('@src="data:image/gif;base64,[A-Za-z0-9+\/]*"@', '', $content);
$content = str_replace('href="/', 'href="' . static::URI, $content); $content = preg_replace('@data-src=@', 'src=', $content);
$content = str_replace('src=\'/', 'src=\'' . static::URI, $content);
$content = str_replace('href=\'/', 'href=\'' . static::URI, $content); // Remove date in the content to prevent content update while the video is getting older
$item['content'] = $content; $content = preg_replace('@<div class="meta-sub light">.*<span>[^<]*</span>[^<]*</div>@', '', $content);
$item['title'] = trim($title->innertext);
$item['uri'] = static::URI . $title->href; $item['content'] = $content;
$this->items[] = $item; $item['title'] = trim($title->innertext);
} $item['uri'] = static::URI . substr($title->href, 1);
$this->items[] = $item;
} }
} }
} }

View File

@@ -134,11 +134,11 @@ EOT;
// data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0" // data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0"
// data-asin-currency-code="USD" data-substitute-count="-1" ... /> // data-asin-currency-code="USD" data-substitute-count="-1" ... />
if ($asinData) { if ($asinData) {
return [ return array(
'price' => $asinData->getAttribute('data-asin-price'), 'price' => $asinData->getAttribute('data-asin-price'),
'currency' => $asinData->getAttribute('data-asin-currency-code'), 'currency' => $asinData->getAttribute('data-asin-currency-code'),
'shipping' => $asinData->getAttribute('data-asin-shipping') 'shipping' => $asinData->getAttribute('data-asin-shipping')
]; );
} }
return false; return false;
@@ -150,11 +150,11 @@ EOT;
preg_match('/^\s*([A-Z]{3}|£|\$)\s?([\d.,]+)\s*$/', $priceDiv->plaintext, $matches); preg_match('/^\s*([A-Z]{3}|£|\$)\s?([\d.,]+)\s*$/', $priceDiv->plaintext, $matches);
if (count($matches) === 3) { if (count($matches) === 3) {
return [ return array(
'price' => $matches[2], 'price' => $matches[2],
'currency' => $matches[1], 'currency' => $matches[1],
'shipping' => '0' 'shipping' => '0'
]; );
} }
return false; return false;

View File

@@ -3,7 +3,9 @@ class AnidexBridge extends BridgeAbstract {
const MAINTAINER = 'ORelio'; const MAINTAINER = 'ORelio';
const NAME = 'Anidex'; const NAME = 'Anidex';
const URI = 'https://anidex.info/'; const URI = 'http://anidex.info/'; // anidex.info has ddos-guard so we need to use anidex.moe
const ALTERNATE_URI = 'https://anidex.moe/'; // anidex.moe returns 301 unless Host is set to anidex.info
const ALTERNATE_HOST = 'anidex.info'; // Correct host for requesting anidex.moe without 301 redirect
const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.'; const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.';
const PARAMETERS = array( const PARAMETERS = array(
array( array(
@@ -108,7 +110,7 @@ class AnidexBridge extends BridgeAbstract {
public function collectData() { public function collectData() {
// Build Search URL from user-provided parameters // Build Search URL from user-provided parameters
$search_url = self::URI . '?s=upload_timestamp&o=desc'; $search_url = self::ALTERNATE_URI . '?s=upload_timestamp&o=desc';
foreach (array('id', 'lang_id', 'group_id') as $param_name) { foreach (array('id', 'lang_id', 'group_id') as $param_name) {
$param = $this->getInput($param_name); $param = $this->getInput($param_name);
if (!empty($param) && intval($param) != 0 && ctype_digit(str_replace(',', '', $param))) { if (!empty($param) && intval($param) != 0 && ctype_digit(str_replace(',', '', $param))) {
@@ -131,8 +133,16 @@ class AnidexBridge extends BridgeAbstract {
$opt[CURLOPT_COOKIE] = 'anidex_h_toggle=' . $h; $opt[CURLOPT_COOKIE] = 'anidex_h_toggle=' . $h;
} }
// We need to use a different Host HTTP header to reach the correct page on ALTERNATE_URI
$headers = array('Host: ' . self::ALTERNATE_HOST);
// The HTTPS certificate presented by anidex.moe is for anidex.info. We need to ignore this.
// As a consequence, the bridge is intentionally marked as insecure by setting self::URI to http://
$opt[CURLOPT_SSL_VERIFYHOST] = 0;
$opt[CURLOPT_SSL_VERIFYPEER] = 0;
// Retrieve torrent listing from search results, which does not contain torrent description // Retrieve torrent listing from search results, which does not contain torrent description
$html = getSimpleHTMLDOM($search_url, array(), $opt) $html = getSimpleHTMLDOM($search_url, $headers, $opt)
or returnServerError('Could not request Anidex: ' . $search_url); or returnServerError('Could not request Anidex: ' . $search_url);
$links = $html->find('a'); $links = $html->find('a');
$results = array(); $results = array();
@@ -156,10 +166,11 @@ class AnidexBridge extends BridgeAbstract {
if ($torrent_id != 0 && ctype_digit($torrent_id)) { if ($torrent_id != 0 && ctype_digit($torrent_id)) {
//Retrieve data for this torrent ID //Retrieve data for this torrent ID
$item_uri = self::URI . 'torrent/' . $torrent_id; $item_browse_uri = self::URI . 'torrent/' . $torrent_id;
$item_fetch_uri = self::ALTERNATE_URI . 'torrent/' . $torrent_id;
//Retrieve full description from torrent page //Retrieve full description from torrent page (cached for 24 hours: 86400 seconds)
if ($item_html = getSimpleHTMLDOMCached($item_uri)) { if ($item_html = getSimpleHTMLDOMCached($item_fetch_uri, 86400, $headers, $opt)) {
//Retrieve data from page contents //Retrieve data from page contents
$item_title = str_replace(' (Torrent) - AniDex ', '', $item_html->find('title', 0)->plaintext); $item_title = str_replace(' (Torrent) - AniDex ', '', $item_html->find('title', 0)->plaintext);
@@ -191,7 +202,7 @@ class AnidexBridge extends BridgeAbstract {
//Build and add final item //Build and add final item
$item = array(); $item = array();
$item['uri'] = $item_uri; $item['uri'] = $item_browse_uri;
$item['title'] = $item_title; $item['title'] = $item_title;
$item['author'] = $item_author; $item['author'] = $item_author;
$item['timestamp'] = $item_date; $item['timestamp'] = $item_date;

View File

@@ -102,7 +102,6 @@ class AnimeUltimeBridge extends BridgeAbstract {
$item_description = defaultLinkTo($item_description, self::URI); $item_description = defaultLinkTo($item_description, self::URI);
$item_description = str_replace("\r", '', $item_description); $item_description = str_replace("\r", '', $item_description);
$item_description = str_replace("\n", '', $item_description); $item_description = str_replace("\n", '', $item_description);
$item_description = utf8_encode($item_description);
//Build and add final item //Build and add final item
$item = array(); $item = array();

View File

@@ -0,0 +1,149 @@
<?php
class AppleAppStoreBridge extends BridgeAbstract {
const MAINTAINER = 'captn3m0';
const NAME = 'Apple App Store';
const URI = 'https://apps.apple.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns version updates for a specific application';
const PARAMETERS = array(array(
'id' => array(
'name' => 'Application ID',
'required' => true,
'exampleValue' => '310633997'
),
'p' => array(
'name' => 'Platform',
'type' => 'list',
'values' => array(
'iPad' => 'ipad',
'iPhone' => 'iphone',
'Mac' => 'mac',
// The following 2 are present in responses
// but not yet tested
'Web' => 'web',
'Apple TV' => 'appletv',
),
'defaultValue' => 'iphone',
),
'country' => array(
'name' => 'Store Country',
'type' => 'list',
'values' => array(
'US' => 'US',
'India' => 'IN',
'Canada' => 'CA'
),
'defaultValue' => 'US',
),
));
const PLATFORM_MAPPING = array(
'iphone' => 'ios',
'ipad' => 'ios',
);
private function makeHtmlUrl($id, $country){
return 'https://apps.apple.com/' . $country . '/app/id' . $id;
}
private function makeJsonUrl($id, $platform, $country){
return "https://amp-api.apps.apple.com/v1/catalog/$country/apps/$id?platform=$platform&extend=versionHistory";
}
public function getName(){
if (isset($this->name)) {
return $this->name . ' - AppStore Updates';
}
return parent::getName();
}
/**
* In case of some platforms, the data is present in the initial response
*/
private function getDataFromShoebox($id, $platform, $country){
$uri = $this->makeHtmlUrl($id, $country);
$html = getSimpleHTMLDOMCached($uri, 3600);
$script = $html->find('script[id="shoebox-ember-data-store"]', 0);
$json = json_decode($script->innertext, true);
return $json['data'];
}
private function getJWTToken($id, $platform, $country){
$uri = $this->makeHtmlUrl($id, $country);
$html = getSimpleHTMLDOMCached($uri, 3600);
$meta = $html->find('meta[name="web-experience-app/config/environment"]', 0);
$json = urldecode($meta->content);
$json = json_decode($json);
return $json->MEDIA_API->token;
}
private function getAppData($id, $platform, $country, $token){
$uri = $this->makeJsonUrl($id, $platform, $country);
$headers = array(
"Authorization: Bearer $token",
);
$json = json_decode(getContents($uri, $headers), true);
return $json['data'][0];
}
/**
* Parses the version history from the data received
* @return array list of versions with details on each element
*/
private function getVersionHistory($data, $platform){
switch($platform) {
case 'mac':
return $data['relationships']['platforms']['data'][0]['attributes']['versionHistory'];
default:
$os = self::PLATFORM_MAPPING[$platform];
return $data['attributes']['platformAttributes'][$os]['versionHistory'];
}
}
public function collectData() {
$id = $this->getInput('id');
$country = $this->getInput('country');
$platform = $this->getInput('p');
switch ($platform) {
case 'mac':
$data = $this->getDataFromShoebox($id, $platform, $country);
break;
default:
$token = $this->getJWTToken($id, $platform, $country);
$data = $this->getAppData($id, $platform, $country, $token);
}
$versionHistory = $this->getVersionHistory($data, $platform);
$name = $this->name = $data['attributes']['name'];
$author = $data['attributes']['artistName'];
foreach ($versionHistory as $row) {
$item = array();
$item['content'] = nl2br($row['releaseNotes']);
$item['title'] = $name . ' - ' . $row['versionDisplay'];
$item['timestamp'] = $row['releaseDate'];
$item['author'] = $author;
$item['uri'] = $this->makeHtmlUrl($id, $country);
$this->items[] = $item;
}
}
}

View File

@@ -5,19 +5,19 @@ class AppleMusicBridge extends BridgeAbstract {
const URI = 'https://www.apple.com'; const URI = 'https://www.apple.com';
const DESCRIPTION = 'Fetches the latest releases from an artist'; const DESCRIPTION = 'Fetches the latest releases from an artist';
const MAINTAINER = 'Limero'; const MAINTAINER = 'Limero';
const PARAMETERS = [[ const PARAMETERS = array(array(
'url' => [ 'url' => array(
'name' => 'Artist URL', 'name' => 'Artist URL',
'exampleValue' => 'https://itunes.apple.com/us/artist/dunderpatrullen/329796274', 'exampleValue' => 'https://itunes.apple.com/us/artist/dunderpatrullen/329796274',
'required' => true, 'required' => true,
], ),
'imgSize' => [ 'imgSize' => array(
'name' => 'Image size for thumbnails (in px)', 'name' => 'Image size for thumbnails (in px)',
'type' => 'number', 'type' => 'number',
'defaultValue' => 512, 'defaultValue' => 512,
'required' => true, 'required' => true,
] )
]]; ));
const CACHE_TIMEOUT = 21600; // 6 hours const CACHE_TIMEOUT = 21600; // 6 hours
public function collectData() { public function collectData() {
@@ -36,12 +36,12 @@ class AppleMusicBridge extends BridgeAbstract {
// Loop through each object // Loop through each object
foreach ($json->included as $obj) { foreach ($json->included as $obj) {
if ($obj->type === 'lockup/album') { if ($obj->type === 'lockup/album') {
$this->items[] = [ $this->items[] = array(
'title' => $obj->attributes->artistName . ' - ' . $obj->attributes->name, 'title' => $obj->attributes->artistName . ' - ' . $obj->attributes->name,
'uri' => $obj->attributes->url, 'uri' => $obj->attributes->url,
'timestamp' => $obj->attributes->releaseDate, 'timestamp' => $obj->attributes->releaseDate,
'enclosures' => $obj->relationships->artwork->data->id, 'enclosures' => $obj->relationships->artwork->data->id,
]; );
} elseif ($obj->type === 'image') { } elseif ($obj->type === 'image') {
$images[$obj->id] = $obj->attributes->url; $images[$obj->id] = $obj->attributes->url;
} }
@@ -49,9 +49,9 @@ class AppleMusicBridge extends BridgeAbstract {
// Add the images to each item // Add the images to each item
foreach ($this->items as &$item) { foreach ($this->items as &$item) {
$item['enclosures'] = [ $item['enclosures'] = array(
str_replace('{w}x{h}bb.{f}', $imgSize . 'x0w.jpg', $images[$item['enclosures']]), str_replace('{w}x{h}bb.{f}', $imgSize . 'x0w.jpg', $images[$item['enclosures']]),
]; );
} }
// Sort the order to put the latest albums first // Sort the order to put the latest albums first

View File

@@ -2,8 +2,8 @@
class AtmoNouvelleAquitaineBridge extends BridgeAbstract { class AtmoNouvelleAquitaineBridge extends BridgeAbstract {
const NAME = 'Atmo Nouvelle Aquitaine'; const NAME = 'Atmo Nouvelle Aquitaine';
const URI = 'https://www.atmo-nouvelleaquitaine.org/monair/commune/'; const URI = 'https://www.atmo-nouvelleaquitaine.org';
const DESCRIPTION = 'Fetches the latest air polution of Bordeaux from Atmo Nouvelle Aquitaine'; const DESCRIPTION = 'Fetches the latest air polution of cities in Nouvelle Aquitaine from Atmo';
const MAINTAINER = 'floviolleau'; const MAINTAINER = 'floviolleau';
const PARAMETERS = array(array( const PARAMETERS = array(array(
'cities' => array( 'cities' => array(
@@ -27,7 +27,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract {
} }
public function collectData() { public function collectData() {
$uri = self::URI . $this->getInput('cities'); $uri = self::URI . '/monair/commune/' . $this->getInput('cities');
$html = getSimpleHTMLDOM($uri) $html = getSimpleHTMLDOM($uri)
or returnServerError('Could not request ' . $uri); or returnServerError('Could not request ' . $uri);
@@ -77,7 +77,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract {
private function getLegendIndexes() { private function getLegendIndexes() {
$rawIndexes = $this->dom->find('.prevision-legend .prevision-legend-label'); $rawIndexes = $this->dom->find('.prevision-legend .prevision-legend-label');
$indexes = []; $indexes = array();
for ($i = 0; $i < count($rawIndexes); $i++) { for ($i = 0; $i < count($rawIndexes); $i++) {
if ($rawIndexes[$i]->hasAttribute('data-color')) { if ($rawIndexes[$i]->hasAttribute('data-color')) {
$indexes[$rawIndexes[$i]->getAttribute('data-color')] = $rawIndexes[$i]->innertext; $indexes[$rawIndexes[$i]->getAttribute('data-color')] = $rawIndexes[$i]->innertext;

View File

@@ -0,0 +1,58 @@
<?php
class AtmoOccitanieBridge extends BridgeAbstract {
const NAME = 'Atmo Occitanie';
const URI = 'https://www.atmo-occitanie.org/';
const DESCRIPTION = 'Fetches the latest air polution of cities in Occitanie from Atmo';
const MAINTAINER = 'floviolleau';
const PARAMETERS = array(array(
'city' => array(
'name' => 'Ville',
'required' => true
)
));
const CACHE_TIMEOUT = 7200;
public function collectData() {
$uri = self::URI . $this->getInput('city');
$html = getSimpleHTMLDOM($uri)
or returnServerError('Could not request ' . $uri);
$generalMessage = $html->find('.landing-ville .city-banner .iqa-avertissement', 0)->innertext;
$recommendationsDom = $html->find('.landing-ville .recommandations', 0);
$recommendationsItemDom = $recommendationsDom->find('.recommandation-item .label');
$recommendationsMessage = '';
$i = 0;
$len = count($recommendationsItemDom);
foreach ($recommendationsItemDom as $key => $value) {
if ($i == 0) {
$recommendationsMessage .= trim($value->innertext) . '.';
} else {
$recommendationsMessage .= ' ' . trim($value->innertext) . '.';
}
$i++;
}
$lastRecommendationsDom = $recommendationsDom->find('.col-md-6', -1);
$informationHeaderMessage = $lastRecommendationsDom->find('.heading', 0)->innertext;
$indice = $lastRecommendationsDom->find('.current-indice .indice div', 0)->innertext;
$informationDescriptionMessage = $lastRecommendationsDom->find('.current-indice .description p', 0)->innertext;
$message = "$generalMessage L'indice est de $indice/10. $informationDescriptionMessage. $recommendationsMessage";
$city = $this->getInput('city');
$item['uri'] = $uri;
$today = date('d/m/Y');
$item['title'] = "Bulletin de l'air du $today pour la ville : $city.";
//$item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-occitanie.org #QualiteAir. ' . $message;
$item['title'] .= ' #QualiteAir. ' . $message;
$item['author'] = 'floviolleau';
$item['content'] = $message;
$item['uid'] = hash('sha256', $item['title']);
$this->items[] = $item;
}
}

View File

@@ -77,110 +77,69 @@ class AutoJMBridge extends BridgeAbstract {
$model_url = self::URI . $this->getInput('url'); $model_url = self::URI . $this->getInput('url');
// Get the session cookies and the form token // Build the GET data
$this->getInitialParameters($model_url); $get_data = 'form[energy]=' . $this->getInput('energy') .
'&form[transmission]=' . $this->getInput('transmission') .
'&form[priceMin]=' . $this->getInput('priceMin') .
'&form[priceMin]=' . $this->getInput('priceMin');
// Build the form // Set the header 'X-Requested-With' like the website does it
$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( $header = array(
'Content-Type: application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With: XMLHttpRequest'
);
// 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 // Get the JSON content of the form
$json = getContents($model_url, $header, $curl_opts) $json = getContents($model_url . '?' . $get_data, $header)
or returnServerError('Could not request AutoJM.'); or returnServerError('Could not request AutoJM.');
// Extract the HTML content from the JSON result // Extract the HTML content from the JSON result
$data = json_decode($json); $data = json_decode($json);
$html = str_get_html($data->content); $html = str_get_html($data->results);
// Go through every finisha of the model // Go through every car of the model
$list = $html->find('h3'); $list = $html->find('div[class=car-card]');
foreach ($list as $finish) { foreach ($list as $car) {
$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 ; // Get the Finish name if this car is the first of a new finish
// As the URL could be identical even if the price change, some RSS reader will not show those offers as new items $prev_tag = $car->prev_sibling();
$item['uri'] = $url . '#' . md5($item['content']); if($prev_tag->tag == 'div' && $prev_tag->class == 'results-title') {
$finish_name = $prev_tag->plaintext;
$this->items[] = $item;
} }
}
}
/** // Get the info about the car offer
* Gets the session cookie and the form token $image = $car->find('div[class=car-card__visual]', 0)->find('img', 0)->src;
* $serie = $car->find('div[class=car-card__title]', 0)->plaintext;
* @param string $pageURL The URL from which to get the values $url = $car->find('a', 0)->href;
*/ // Check if the car model is in stock or available only on order
private function getInitialParameters($pageURL) { if($car->find('span[class*=tag--dispo]', 0) != null) {
$ch = curl_init(); $availability = 'En Stock';
curl_setopt($ch, CURLOPT_URL, $pageURL); } else {
curl_setopt($ch, CURLOPT_HEADER, true); $availability = 'Sur commande';
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;
} }
} $discount_html = $car->find('span[class=promo]', 0);
$this->cookies = trim(substr($cookies, 1)); // Check if there is any discount dsiplayed
if ($discount_html != null) {
$discount = $discount_html->plaintext;
} else {
$discount = 'inconnue';
}
$price = $car->find('span[class=price]', 0)->plaintext;
// Get the token from the content // Construct the new item
$html = str_get_html($content); $item = array();
$token = $html->find('input[type=hidden][id=form__token]', 0); $item['title'] = $finish_name . ' ' . $serie;
$this->token = $token->value; $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;
}
} }
} }

View File

@@ -0,0 +1,55 @@
<?php
class AwwwardsBridge extends BridgeAbstract {
const NAME = 'Awwwards';
const URI = 'https://www.awwwards.com/';
const DESCRIPTION = 'Fetches the latest ten sites of the day from Awwwards';
const MAINTAINER = 'Paroleen';
const CACHE_TIMEOUT = 3600;
const SITESURI = 'https://www.awwwards.com/websites/sites_of_the_day/';
const SITEURI = 'https://www.awwwards.com/sites/';
const ASSETSURI = 'https://assets.awwwards.com/awards/media/cache/thumb_417_299/';
private $sites = array();
public function getIcon() {
return 'https://www.awwwards.com/favicon.ico';
}
private function fetchSites() {
Debug::log('Fetching all sites');
$sites = getSimpleHTMLDOM(self::SITESURI)
or returnServerError('Could not fetch JSON for sites.');
Debug::log('Parsing all JSON data');
foreach($sites->find('li[data-model]') as $site) {
$decode = html_entity_decode($site->attr['data-model'],
ENT_QUOTES, 'utf-8');
$decode = json_decode($decode, true);
$this->sites[] = $decode;
}
}
public function collectData() {
$this->fetchSites();
Debug::log('Building RSS feed');
foreach($this->sites as $site) {
$item = array();
$item['title'] = $site['title'];
$item['timestamp'] = $site['createdAt'];
$item['categories'] = $site['tags'];
$item['content'] = '<img src="'
. self::ASSETSURI
. $site['images']['thumbnail']
. '">';
$item['uri'] = self::SITEURI . $site['slug'];
$this->items[] = $item;
if(count($this->items) >= 10)
break;
}
}
}

View File

@@ -1,73 +1,262 @@
<?php <?php
class BandcampBridge extends BridgeAbstract { class BandcampBridge extends BridgeAbstract {
const MAINTAINER = 'sebsauvage'; const MAINTAINER = 'sebsauvage, Roliga';
const NAME = 'Bandcamp Tag'; const NAME = 'Bandcamp Bridge';
const URI = 'https://bandcamp.com/'; const URI = 'https://bandcamp.com/';
const CACHE_TIMEOUT = 600; // 10min const CACHE_TIMEOUT = 600; // 10min
const DESCRIPTION = 'New bandcamp release by tag'; const DESCRIPTION = 'New bandcamp releases by tag, band or album';
const PARAMETERS = array( array( const PARAMETERS = array(
'tag' => array( 'By tag' => array(
'name' => 'tag', 'tag' => array(
'type' => 'text', 'name' => 'tag',
'required' => true 'type' => 'text',
'required' => true
)
),
'By band' => array(
'band' => array(
'name' => 'band',
'type' => 'text',
'title' => 'Band name as seen in the band page URL',
'required' => true
),
'type' => array(
'name' => 'Articles are',
'type' => 'list',
'values' => array(
'Releases' => 'releases',
'Releases, new one when track list changes' => 'changes',
'Individual tracks' => 'tracks'
),
'defaultValue' => 'changes'
),
'limit' => array(
'name' => 'limit',
'type' => 'number',
'title' => 'Number of releases to return',
'defaultValue' => 5
)
),
'By album' => array(
'band' => array(
'name' => 'band',
'type' => 'text',
'title' => 'Band name as seen in the album page URL',
'required' => true
),
'album' => array(
'name' => 'album',
'type' => 'text',
'title' => 'Album name as seen in the album page URL',
'required' => true
),
'type' => array(
'name' => 'Articles are',
'type' => 'list',
'values' => array(
'Releases' => 'releases',
'Releases, new one when track list changes' => 'changes',
'Individual tracks' => 'tracks'
),
'defaultValue' => 'tracks'
)
) )
)); );
const IMGURI = 'https://f4.bcbits.com/'; const IMGURI = 'https://f4.bcbits.com/';
const IMGSIZE_300PX = 23; const IMGSIZE_300PX = 23;
const IMGSIZE_700PX = 16; const IMGSIZE_700PX = 16;
private $feedName;
public function getIcon() { public function getIcon() {
return 'https://s4.bcbits.com/img/bc_favicon.ico'; return 'https://s4.bcbits.com/img/bc_favicon.ico';
} }
public function collectData(){ public function collectData(){
$url = self::URI . 'api/hub/1/dig_deeper'; switch($this->queriedContext) {
$data = $this->buildRequestJson(); case 'By tag':
$header = array( $url = self::URI . 'api/hub/1/dig_deeper';
'Content-Type: application/json', $data = $this->buildRequestJson();
'Content-Length: ' . strlen($data) $header = array(
); 'Content-Type: application/json',
$opts = array( 'Content-Length: ' . strlen($data)
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $data
);
$content = getContents($url, $header, $opts)
or returnServerError('Could not complete request to: ' . $url);
$json = json_decode($content);
if ($json->ok !== true) {
returnServerError('Invalid response');
}
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;
$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 = array(
'uri' => $url,
'author' => $full_artist,
'title' => $full_title
); );
$item['content'] = "<img src='$small_img' /><br/>$full_title"; $opts = array(
$item['enclosures'] = array($img); CURLOPT_CUSTOMREQUEST => 'POST',
$this->items[] = $item; CURLOPT_POSTFIELDS => $data
);
$content = getContents($url, $header, $opts)
or returnServerError('Could not complete request to: ' . $url);
$json = json_decode($content);
if ($json->ok !== true) {
returnServerError('Invalid response');
}
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;
$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 = 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;
}
break;
case 'By band':
case 'By album':
$html = getSimpleHTMLDOMCached($this->getURI(), 86400);
$titleElement = $html->find('head meta[name=title]', 0)
or returnServerError('Unable to find title on: ' . $this->getURI());
$this->feedName = $titleElement->content;
$regex = '/band_id=(\d+)/';
if(preg_match($regex, $html, $matches) == false)
returnServerError('Unable to find band ID on: ' . $this->getURI());
$band_id = $matches[1];
$tralbums = array();
switch($this->queriedContext) {
case 'By band':
$query_data = array(
'band_id' => $band_id
);
$band_data = $this->apiGet('mobile/22/band_details', $query_data);
$num_albums = min(count($band_data->discography), $this->getInput('limit'));
for($i = 0; $i < $num_albums; $i++) {
$album_basic_data = $band_data->discography[$i];
// 'a' or 't' for albums and individual tracks respectively
$tralbum_type = substr($album_basic_data->item_type, 0, 1);
$query_data = array(
'band_id' => $band_id,
'tralbum_type' => $tralbum_type,
'tralbum_id' => $album_basic_data->item_id
);
$tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
}
break;
case 'By album':
$regex = '/album=(\d+)/';
if(preg_match($regex, $html, $matches) == false)
returnServerError('Unable to find album ID on: ' . $this->getURI());
$album_id = $matches[1];
$query_data = array(
'band_id' => $band_id,
'tralbum_type' => 'a',
'tralbum_id' => $album_id
);
$tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
break;
}
foreach ($tralbums as $tralbum_data) {
if ($tralbum_data->type === 'a' && $this->getInput('type') === 'tracks') {
foreach ($tralbum_data->tracks as $track) {
$query_data = array(
'band_id' => $band_id,
'tralbum_type' => 't',
'tralbum_id' => $track->track_id
);
$track_data = $this->apiGet('mobile/22/tralbum_details', $query_data);
$this->items[] = $this->buildTralbumItem($track_data);
}
} else {
$this->items[] = $this->buildTralbumItem($tralbum_data);
}
}
break;
} }
} }
private function buildTralbumItem($tralbum_data){
$band_data = $tralbum_data->band;
// Format title like: ARTIST - ALBUM/TRACK (OPTIONAL RELEASER)
// Format artist/author like: ARTIST (OPTIONAL RELEASER)
//
// If the album/track is released under a label/a band other than the artist
// themselves, append that releaser name to the title and artist/author.
//
// This sadly doesn't always work right for individual tracks as the artist
// of the track is always set to the releaser.
$artist = $tralbum_data->tralbum_artist;
$full_title = $artist . ' - ' . $tralbum_data->title;
$full_artist = $artist;
if (isset($tralbum_data->label)) {
$full_title .= ' (' . $tralbum_data->label . ')';
$full_artist .= ' (' . $tralbum_data->label . ')';
} elseif ($band_data->name !== $artist) {
$full_title .= ' (' . $band_data->name . ')';
$full_artist .= ' (' . $band_data->name . ')';
}
$small_img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_300PX);
$img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_700PX);
$item = array(
'uri' => $tralbum_data->bandcamp_url,
'author' => $full_artist,
'title' => $full_title,
'enclosures' => array($img),
'timestamp' => $tralbum_data->release_date
);
$item['categories'] = array();
foreach ($tralbum_data->tags as $tag) {
$item['categories'][] = $tag->norm_name;
}
// Give articles a unique UID depending on its track list
// Releases should then show up as new articles when tracks are added
if ($this->getInput('type') === 'changes') {
$item['uid'] = "bandcamp/$band_data->band_id/$tralbum_data->id/";
foreach ($tralbum_data->tracks as $track) {
$item['uid'] .= $track->track_id;
}
}
$item['content'] = "<img src='$small_img' /><br/>$full_title<br/>";
if ($tralbum_data->type === 'a') {
$item['content'] .= '<ol>';
foreach ($tralbum_data->tracks as $track) {
$item['content'] .= "<li>$track->title</li>";
}
$item['content'] .= '</ol>';
}
if (!empty($tralbum_data->about)) {
$item['content'] .= '<p>'
. nl2br($tralbum_data->about)
. '</p>';
}
return $item;
}
private function buildRequestJson(){ private function buildRequestJson(){
$requestJson = array( $requestJson = array(
'tag' => $this->getInput('tag'), 'tag' => $this->getInput('tag'),
@@ -81,11 +270,94 @@ class BandcampBridge extends BridgeAbstract {
return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg'; return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg';
} }
private function apiGet($endpoint, $query_data) {
$url = self::URI . 'api/' . $endpoint . '?' . http_build_query($query_data);
$data = json_decode(getContents($url))
or returnServerError('API request to "' . $url . '" failed.');
return $data;
}
public function getURI(){
switch($this->queriedContext) {
case 'By tag':
if(!is_null($this->getInput('tag'))) {
return self::URI
. 'tag/'
. urlencode($this->getInput('tag'))
. '?sort_field=date';
}
break;
case 'By band':
if(!is_null($this->getInput('band'))) {
return 'https://'
. $this->getInput('band')
. '.bandcamp.com/music';
}
break;
case 'By album':
if(!is_null($this->getInput('band')) && !is_null($this->getInput('album'))) {
return 'https://'
. $this->getInput('band')
. '.bandcamp.com/album/'
. $this->getInput('album');
}
break;
}
return parent::getURI();
}
public function getName(){ public function getName(){
if(!is_null($this->getInput('tag'))) { switch($this->queriedContext) {
return $this->getInput('tag') . ' - Bandcamp Tag'; case 'By tag':
if(!is_null($this->getInput('tag'))) {
return $this->getInput('tag') . ' - Bandcamp Tag';
}
break;
case 'By band':
if(isset($this->feedName)) {
return $this->feedName . ' - Bandcamp Band';
} elseif(!is_null($this->getInput('band'))) {
return $this->getInput('band') . ' - Bandcamp Band';
}
break;
case 'By album':
if(isset($this->feedName)) {
return $this->feedName . ' - Bandcamp Album';
} elseif(!is_null($this->getInput('album'))) {
return $this->getInput('album') . ' - Bandcamp Album';
}
break;
} }
return parent::getName(); return parent::getName();
} }
public function detectParameters($url) {
$params = array();
// By tag
$regex = '/^(https?:\/\/)?bandcamp\.com\/tag\/([^\/.&?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['tag'] = urldecode($matches[2]);
return $params;
}
// By band
$regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/';
if(preg_match($regex, $url, $matches) > 0) {
$params['band'] = urldecode($matches[2]);
return $params;
}
// By album
$regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['band'] = urldecode($matches[2]);
$params['album'] = urldecode($matches[3]);
return $params;
}
return null;
}
} }

View File

@@ -3,17 +3,11 @@ class BastaBridge extends BridgeAbstract {
const MAINTAINER = 'qwertygc'; const MAINTAINER = 'qwertygc';
const NAME = 'Bastamag Bridge'; const NAME = 'Bastamag Bridge';
const URI = 'http://www.bastamag.net/'; const URI = 'https://www.bastamag.net/';
const CACHE_TIMEOUT = 7200; // 2h const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Returns the newest articles.'; const DESCRIPTION = 'Returns the newest articles.';
public function collectData(){ public function collectData(){
// Replaces all relative image URLs by absolute URLs.
// Relative URLs always start with 'local/'!
function replaceImageUrl($content){
return preg_replace('/src=["\']{1}([^"\']+)/ims', 'src=\'' . self::URI . '$1\'', $content);
}
$html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend') $html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend')
or returnServerError('Could not request Bastamag.'); or returnServerError('Could not request Bastamag.');
@@ -25,7 +19,13 @@ class BastaBridge extends BridgeAbstract {
$item['title'] = $element->find('title', 0)->innertext; $item['title'] = $element->find('title', 0)->innertext;
$item['uri'] = $element->find('guid', 0)->plaintext; $item['uri'] = $element->find('guid', 0)->plaintext;
$item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext); $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);
$item['content'] = replaceImageUrl(getSimpleHTMLDOM($item['uri'])->find('div.texte', 0)->innertext); // Replaces all relative image URLs by absolute URLs.
// Relative URLs always start with 'local/'!
$item['content'] = preg_replace(
'/src=["\']{1}([^"\']+)/ims',
'src=\'' . self::URI . '$1\'',
getSimpleHTMLDOM($item['uri'])->find('div.texte', 0)->innertext
);
$this->items[] = $item; $this->items[] = $item;
$limit++; $limit++;
} }

View File

@@ -92,7 +92,7 @@ class BingSearchBridge extends BridgeAbstract
or returnServerError('Could not request ' . self::NAME); or returnServerError('Could not request ' . self::NAME);
$sizeKey = $this->getInput('image_size'); $sizeKey = $this->getInput('image_size');
$items = []; $items = array();
foreach ($html->find('a.iusc') as $element) { foreach ($html->find('a.iusc') as $element) {
$data = json_decode(htmlspecialchars_decode($element->getAttribute('m')), true); $data = json_decode(htmlspecialchars_decode($element->getAttribute('m')), true);

View File

@@ -0,0 +1,29 @@
<?php
class BleepingComputerBridge extends FeedExpander {
const MAINTAINER = 'csisoap';
const NAME = 'Bleeping Computer';
const URI = 'https://www.bleepingcomputer.com/';
const DESCRIPTION = 'Returns the newest articles.';
protected function parseItem($item){
$item = parent::parseItem($item);
$article_html = getSimpleHTMLDOMCached($item['uri']);
if(!$article_html) {
$item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>';
return $item;
}
$article_content = $article_html->find('div.articleBody', 0)->innertext;
$article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="cz-related-article-wrapp');
$item['content'] = trim($article_content);
return $item;
}
public function collectData(){
$feed = static::URI . 'feed/';
$this->collectExpandableDatas($feed);
}
}

View File

@@ -0,0 +1,60 @@
<?php
class BlizzardNewsBridge extends XPathAbstract {
const NAME = 'Blizzard News';
const URI = 'https://news.blizzard.com';
const DESCRIPTION = 'Blizzard (game company) newsfeed';
const MAINTAINER = 'Niehztog';
const PARAMETERS = array(
'' => array(
'locale' => array(
'name' => 'Language',
'type' => 'list',
'values' => array(
'Deutsch' => 'de-de',
'English (EU)' => 'en-gb',
'English (US)' => 'en-us',
'Español (EU)' => 'es-es',
'Español (AL)' => 'es-mx',
'Français' => 'fr-fr',
'Italiano' => 'it-it',
'日本語' => 'ja-jp',
'한국어' => 'ko-kr',
'Polski' => 'pl-pl',
'Português (AL)' => 'pt-br',
'Русский' => 'ru-ru',
'ภาษาไทย' => 'th-th',
'简体中文' => 'zh-cn',
'繁體中文' => 'zh-tw'
),
'defaultValue' => 'en-us',
'title' => 'Select your language'
)
)
);
const CACHE_TIMEOUT = 3600;
const XPATH_EXPRESSION_ITEM = '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article';
const XPATH_EXPRESSION_ITEM_TITLE = './/div/div[2]/h2';
const XPATH_EXPRESSION_ITEM_CONTENT = './/div[@class="ArticleListItem-description"]/div[@class="h6"]';
const XPATH_EXPRESSION_ITEM_URI = './/a[@class="ArticleLink ArticleLink"]/@href';
const XPATH_EXPRESSION_ITEM_AUTHOR = '';
const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp';
const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/div[@class="ArticleListItem-image"]/@style';
const XPATH_EXPRESSION_ITEM_CATEGORIES = './/div[@class="ArticleListItem-label"]';
const SETTING_FIX_ENCODING = true;
/**
* Source Web page URL (should provide either HTML or XML content)
* @return string
*/
protected function getSourceUrl(){
$locale = $this->getInput('locale');
if('zh-cn' === $locale) {
return 'https://cn.news.blizzard.com';
}
return 'https://news.blizzard.com/' . $locale;
}
}

View File

@@ -1,69 +0,0 @@
<?php
class BloombergBridge extends BridgeAbstract
{
const NAME = 'Bloomberg';
const URI = 'https://www.bloomberg.com/';
const DESCRIPTION = 'Trending stories from Bloomberg';
const MAINTAINER = 'mdemoss';
const PARAMETERS = array(
'Trending Stories' => array(),
'From Search' => array(
'q' => array(
'name' => 'Keyword',
'required' => true
)
)
);
public function getName()
{
switch($this->queriedContext) {
case 'Trending Stories':
return self::NAME . ' Trending Stories';
case 'From Search':
if (!is_null($this->getInput('q'))) {
return self::NAME . ' Search : ' . $this->getInput('q');
}
break;
}
return parent::getName();
}
public function getIcon() {
return 'https://assets.bwbx.io/s3/javelin/public/hub/images/favicon-black-63fe5249d3.png';
}
public function collectData()
{
switch($this->queriedContext) {
case 'Trending Stories': // Get list of top new <article>s from the front page.
$html = getSimpleHTMLDOMCached($this->getURI(), 300);
$stories = $html->find('ul.top-news-v3__stories article.top-news-v3-story');
break;
case 'From Search': // Get list of <article> elements from search.
$html = getSimpleHTMLDOMCached(
$this->getURI() .
'search?sort=time:desc&page=1&query=' .
urlencode($this->getInput('q')), 300
);
$stories = $html->find('div.search-result-items article.search-result-story');
break;
}
foreach ($stories as $element) {
$item['uri'] = $element->find('h1 a', 0)->href;
if (preg_match('#^https://#i', $item['uri']) !== 1) {
$item['uri'] = $this->getURI() . $item['uri'];
}
$articleHtml = getSimpleHTMLDOMCached($item['uri']);
if (!$articleHtml) {
continue;
}
$item['title'] = $element->find('h1 a', 0)->plaintext;
$item['timestamp'] = strtotime($articleHtml->find('meta[name=iso-8601-publish-date],meta[name=date]', 0)->content);
$item['content'] = $articleHtml->find('meta[name=description]', 0)->content;
$this->items[] = $item;
}
}
}

View File

@@ -16,6 +16,7 @@ class BrutBridge extends BridgeAbstract {
'Entertainment' => 'entertainment', 'Entertainment' => 'entertainment',
'Sports' => 'sport', 'Sports' => 'sport',
'Nature' => 'nature', 'Nature' => 'nature',
'Health' => 'health',
), ),
'defaultValue' => 'news', 'defaultValue' => 'news',
), ),
@@ -26,6 +27,7 @@ class BrutBridge extends BridgeAbstract {
'United States' => 'us', 'United States' => 'us',
'United Kingdom' => 'uk', 'United Kingdom' => 'uk',
'France' => 'fr', 'France' => 'fr',
'Spain' => 'es',
'India' => 'in', 'India' => 'in',
'Mexico' => 'mx', 'Mexico' => 'mx',
), ),

View File

@@ -23,8 +23,8 @@ class CNETFranceBridge extends FeedExpander
) )
); );
private $bannedTitle = []; private $bannedTitle = array();
private $bannedURL = []; private $bannedURL = array();
public function collectData() public function collectData()
{ {

View File

@@ -22,7 +22,7 @@ class CachetBridge extends BridgeAbstract {
); );
const CACHE_TIMEOUT = 300; const CACHE_TIMEOUT = 300;
private $componentCache = []; private $componentCache = array();
public function getURI() { public function getURI() {
return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host'); return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host');
@@ -114,13 +114,13 @@ class CachetBridge extends BridgeAbstract {
$uidOrig = $permalink . $incident->created_at; $uidOrig = $permalink . $incident->created_at;
$uid = hash('sha512', $uidOrig); $uid = hash('sha512', $uidOrig);
$timestamp = strtotime($incident->created_at); $timestamp = strtotime($incident->created_at);
$categories = []; $categories = array();
$categories[] = $incident->human_status; $categories[] = $incident->human_status;
if ($componentName !== '') { if ($componentName !== '') {
$categories[] = $componentName; $categories[] = $componentName;
} }
$item = []; $item = array();
$item['uri'] = $permalink; $item['uri'] = $permalink;
$item['title'] = $title; $item['title'] = $title;
$item['timestamp'] = $timestamp; $item['timestamp'] = $timestamp;

View File

@@ -2,7 +2,7 @@
class CastorusBridge extends BridgeAbstract { class CastorusBridge extends BridgeAbstract {
const MAINTAINER = 'logmanoriginal'; const MAINTAINER = 'logmanoriginal';
const NAME = 'Castorus Bridge'; const NAME = 'Castorus Bridge';
const URI = 'http://www.castorus.com'; const URI = 'https://www.castorus.com';
const CACHE_TIMEOUT = 600; // 10min const CACHE_TIMEOUT = 600; // 10min
const DESCRIPTION = 'Returns the latest changes'; const DESCRIPTION = 'Returns the latest changes';

84
bridges/CeskaTelevizeBridge.php Executable file
View File

@@ -0,0 +1,84 @@
<?php
class CeskaTelevizeBridge extends BridgeAbstract {
const NAME = 'Česká televize Bridge';
const URI = 'https://www.ceskatelevize.cz';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'Return newest videos';
const MAINTAINER = 'kolarcz';
const PARAMETERS = array(
array(
'url' => array(
'name' => 'url to the show',
'required' => true,
'exampleValue' => 'https://www.ceskatelevize.cz/porady/1097181328-udalosti/dily/'
)
)
);
private function fixChars($text) {
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
}
private function getUploadTimeFromString($string) {
if (strpos($string, 'dnes') !== false) {
return strtotime('today');
} elseif (strpos($string, 'včera') !== false) {
return strtotime('yesterday');
} elseif (!preg_match('/(\d+).\s(\d+).(\s(\d+))?/', $string, $match)) {
returnServerError('Could not get date from Česká televize string');
}
$date = sprintf('%04d-%02d-%02d', isset($match[3]) ? $match[3] : date('Y'), $match[2], $match[1]);
return strtotime($date);
}
public function collectData() {
$url = $this->getInput('url');
$validUrl = '/^(https:\/\/www\.ceskatelevize\.cz\/porady\/\d+-[a-z0-9-]+\/)(dily\/((nove|vysilani)\/)?)?$/';
if (!preg_match($validUrl, $url, $match)) {
returnServerError('Invalid url');
}
$category = isset($match[4]) ? $match[4] : 'nove';
$fixedUrl = "{$match[1]}dily/{$category}/";
$html = getSimpleHTMLDOM($fixedUrl)
or returnServerError('Could not request Česká televize');
$this->feedUri = $fixedUrl;
$this->feedName = str_replace('Přehled dílů — ', '', $this->fixChars($html->find('title', 0)->plaintext));
if ($category !== 'nove') {
$this->feedName .= " ({$category})";
}
foreach ($html->find('.episodes-broadcast-content a.episode_list_item') as $element) {
$itemTitle = $element->find('.episode_list_item-title', 0);
$itemContent = $element->find('.episode_list_item-desc', 0);
$itemDate = $element->find('.episode_list_item-date', 0);
$itemThumbnail = $element->find('img', 0);
$itemUri = self::URI . $element->getAttribute('href');
$item = array(
'title' => $this->fixChars($itemTitle->plaintext),
'uri' => $itemUri,
'content' => '<img src="https:' . $itemThumbnail->getAttribute('src') . '" /><br />'
. $this->fixChars($itemContent->plaintext),
'timestamp' => $this->getUploadTimeFromString($itemDate->plaintext)
);
$this->items[] = $item;
}
}
public function getURI() {
return isset($this->feedUri) ? $this->feedUri : parent::getURI();
}
public function getName() {
return isset($this->feedName) ? $this->feedName : parent::getName();
}
}

View File

@@ -3,7 +3,7 @@ class CollegeDeFranceBridge extends BridgeAbstract {
const MAINTAINER = 'pit-fgfjiudghdf'; const MAINTAINER = 'pit-fgfjiudghdf';
const NAME = 'CollegeDeFrance'; const NAME = 'CollegeDeFrance';
const URI = 'http://www.college-de-france.fr/'; const URI = 'https://www.college-de-france.fr/';
const CACHE_TIMEOUT = 10800; // 3h const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the latest audio and video from CollegeDeFrance'; const DESCRIPTION = 'Returns the latest audio and video from CollegeDeFrance';

View File

@@ -0,0 +1,65 @@
<?php
class ComicsKingdomBridge extends BridgeAbstract {
const MAINTAINER = 'stjohnjohnson';
const NAME = 'Comics Kingdom Unofficial RSS';
const URI = 'https://www.comicskingdom.com/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Comics Kingdom Unofficial RSS';
const PARAMETERS = array( array(
'comicname' => array(
'name' => 'comicname',
'type' => 'text',
'required' => true
)
));
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI(), array(), array(), true, false)
or returnServerError('Could not request Comics Kingdom: ' . $this->getURI());
// Get author from first page
$author = $html->find('div.author p', 0)->plaintext
or returnServerError('Comics Kingdom comic does not exist: ' . $this->getURI());;
// Get current date/link
$link = $html->find('meta[property=og:url]', 0)->content;
for($i = 0; $i < 5; $i++) {
$item = array();
$page = getSimpleHTMLDOM($link)
or returnServerError('Could not request Comics Kingdom: ' . $link);
$imagelink = $page->find('meta[property=og:image]', 0)->content;
$prevSlug = $page->find('slider-arrow[:is-left-arrow=true]', 0);
$link = $this->getURI() . '/' . $prevSlug->getAttribute('date-slug');
$date = explode('/', $link);
$item['id'] = $imagelink;
$item['uri'] = $link;
$item['author'] = $author;
$item['title'] = 'Comics Kingdom ' . $this->getInput('comicname');
$item['timestamp'] = DateTime::createFromFormat('Y-m-d', $date[count($date) - 1])->getTimestamp();
$item['content'] = '<img src="' . $imagelink . '" />';
$this->items[] = $item;
}
}
public function getURI(){
if(!is_null($this->getInput('comicname'))) {
return self::URI . urlencode($this->getInput('comicname'));
}
return parent::getURI();
}
public function getName(){
if(!is_null($this->getInput('comicname'))) {
return $this->getInput('comicname') . ' - Comics Kingdom';
}
return parent::getName();
}
}

View File

@@ -10,20 +10,20 @@ class ContainerLinuxReleasesBridge extends BridgeAbstract {
const BETA = 'beta'; const BETA = 'beta';
const ALPHA = 'alpha'; const ALPHA = 'alpha';
const PARAMETERS = [ const PARAMETERS = array(
[ array(
'channel' => [ 'channel' => array(
'name' => 'Release Channel', 'name' => 'Release Channel',
'type' => 'list', 'type' => 'list',
'defaultValue' => self::STABLE, 'defaultValue' => self::STABLE,
'values' => [ 'values' => array(
'Stable' => self::STABLE, 'Stable' => self::STABLE,
'Beta' => self::BETA, 'Beta' => self::BETA,
'Alpha' => self::ALPHA, 'Alpha' => self::ALPHA,
], ),
] )
] )
]; );
private function getReleaseFeed($jsonUrl) { private function getReleaseFeed($jsonUrl) {
$json = getContents($jsonUrl) $json = getContents($jsonUrl)
@@ -39,7 +39,7 @@ class ContainerLinuxReleasesBridge extends BridgeAbstract {
$data = $this->getReleaseFeed($this->getJsonUri()); $data = $this->getReleaseFeed($this->getJsonUri());
foreach ($data as $releaseVersion => $release) { foreach ($data as $releaseVersion => $release) {
$item = []; $item = array();
$item['uri'] = "https://coreos.com/releases/#$releaseVersion"; $item['uri'] = "https://coreos.com/releases/#$releaseVersion";
$item['title'] = $releaseVersion; $item['title'] = $releaseVersion;

View File

@@ -0,0 +1,81 @@
<?php
class DarkReadingBridge extends FeedExpander {
const MAINTAINER = 'ORelio';
const NAME = 'Dark Reading Bridge';
const URI = 'https://www.darkreading.com/';
const DESCRIPTION = 'Returns the newest articles from Dark Reading';
const PARAMETERS = array( array(
'feed' => array(
'name' => 'Feed',
'type' => 'list',
'values' => array(
'All Dark Reading Stories' => '000_AllArticles',
'Attacks/Breaches' => '644_Attacks/Breaches',
'Application Security' => '645_Application%20Security',
'Database Security' => '646_Database%20Security',
'Cloud' => '647_Cloud',
'Endpoint' => '648_Endpoint',
'Authentication' => '649_Authentication',
'Privacy' => '650_Privacy',
'Mobile' => '651_Mobile',
'Perimeter' => '652_Perimeter',
'Risk' => '653_Risk',
'Compliance' => '654_Compliance',
'Operations' => '655_Operations',
'Careers and People' => '656_Careers%20and%20People',
'Identity and Access Management' => '657_Identity%20and%20Access%20Management',
'Analytics' => '658_Analytics',
'Threat Intelligence' => '659_Threat%20Intelligence',
'Security Monitoring' => '660_Security%20Monitoring',
'Vulnerabilities / Threats' => '661_Vulnerabilities%20/%20Threats',
'Advanced Threats' => '662_Advanced%20Threats',
'Insider Threats' => '663_Insider%20Threats',
'Vulnerability Management' => '664_Vulnerability%20Management',
)
)
));
public function collectData(){
$feed = $this->getInput('feed');
$feed_splitted = explode('_', $feed);
$feed_id = $feed_splitted[0];
$feed_name = $feed_splitted[1];
if(empty($feed) || !ctype_digit($feed_id) || !preg_match('/[A-Za-z%20\/]/', $feed_name)) {
returnClientError('Invalid feed, please check the "feed" parameter.');
}
$feed_url = $this->getURI() . 'rss_simple.asp';
if ($feed_id != '000') {
$feed_url .= '?f_n=' . $feed_id . '&f_ln=' . $feed_name;
}
$this->collectExpandableDatas($feed_url);
}
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
if (empty($item['content']))
return null; //ignore dummy articles
$article = getSimpleHTMLDOMCached($item['uri'])
or returnServerError('Could not request Dark Reading: ' . $item['uri']);
$item['content'] = $this->extractArticleContent($article);
$item['enclosures'] = array(); //remove author profile picture
return $item;
}
private function extractArticleContent($article){
$content = $article->find('div#article-main', 0)->innertext;
foreach (array(
'<div class="divsplitter',
'<div style="float: left; margin-right: 2px;',
'<div class="more-insights',
'<div id="more-insights',
) as $div_start) {
$content = stripRecursiveHTMLSection($content, 'div', $div_start);
}
$content = stripWithDelimiters($content, '<h1 ', '</h1>');
return $content;
}
}

View File

@@ -0,0 +1,24 @@
<?php
class DaveRamseyBlogBridge extends BridgeAbstract {
const MAINTAINER = 'johnpc';
const NAME = 'Dave Ramsey Blog';
const URI = 'https://www.daveramsey.com/blog';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Returns blog posts from daveramsey.com';
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request daveramsey.com.');
foreach ($html->find('.Post') as $element) {
$this->items[] = array(
'uri' => 'https://www.daveramsey.com' . $element->find('header > a', 0)->href,
'title' => $element->find('header > h2 > a', 0)->plaintext,
'tags' => $element->find('.Post-topic', 0)->plaintext,
'content' => $element->find('.Post-body', 0)->plaintext,
);
}
}
}

View File

@@ -116,6 +116,12 @@ class DesoutterBridge extends BridgeAbstract {
'name' => 'Load full articles', 'name' => 'Load full articles',
'type' => 'checkbox', 'type' => 'checkbox',
'title' => 'Enable to load the full article for each item' 'title' => 'Enable to load the full article for each item'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'defaultValue' => 3,
'title' => "Maximum number of items to return in the feed.\n0 = unlimited"
) )
) )
); );
@@ -156,6 +162,8 @@ class DesoutterBridge extends BridgeAbstract {
$this->title = html_entity_decode($html->find('title', 0)->plaintext, ENT_QUOTES); $this->title = html_entity_decode($html->find('title', 0)->plaintext, ENT_QUOTES);
$limit = $this->getInput('limit') ?: 0;
foreach($html->find('article') as $article) { foreach($html->find('article') as $article) {
$item = array(); $item = array();
@@ -169,6 +177,8 @@ class DesoutterBridge extends BridgeAbstract {
} }
$this->items[] = $item; $this->items[] = $item;
if ($limit > 0 && count($this->items) >= $limit) break;
} }
} }

View File

@@ -45,29 +45,22 @@ apple-icon-5c6fa9f2bce280428589c6195b7f1924206a53b782b371cfe2d02da932c8c173.png'
} }
public function collectData() { public function collectData() {
$html = getSimpleHTMLDOMCached($this->getURI()) $html = getSimpleHTMLDOMCached($this->getURI())
or returnServerError('Could not request ' . $this->getURI()); or returnServerError('Could not request ' . $this->getURI());
$html = defaultLinkTo($html, static::URI); $html = defaultLinkTo($html, static::URI);
$articles = $html->find('div[class="single-article"]') $articles = $html->find('div.crayons-story')
or returnServerError('Could not find articles!'); or returnServerError('Could not find articles!');
foreach($articles as $article) { foreach($articles as $article) {
if($article->find('[class*="cta"]', 0)) { // Skip ads
continue;
}
$item = array(); $item = array();
$item['uri'] = $article->find('a[id*=article-link]', 0)->href; $item['uri'] = $article->find('a[id*=article-link]', 0)->href;
$item['title'] = $article->find('h3', 0)->plaintext; $item['title'] = $article->find('h2 > a', 0)->plaintext;
// i.e. "Charlie Harrington・Sep 21" $item['timestamp'] = $article->find('time', 0)->datetime;
$item['timestamp'] = strtotime(explode('・', $article->find('h4 a', 0)->plaintext, 2)[1]); $item['author'] = $article->find('a.crayons-story__secondary.fw-medium', 0)->plaintext;
$item['author'] = explode('・', $article->find('h4 a', 0)->plaintext, 2)[0];
// Profile image // Profile image
$item['enclosures'] = array($article->find('img', 0)->src); $item['enclosures'] = array($article->find('img', 0)->src);
@@ -75,7 +68,6 @@ apple-icon-5c6fa9f2bce280428589c6195b7f1924206a53b782b371cfe2d02da932c8c173.png'
if($this->getInput('full')) { if($this->getInput('full')) {
$fullArticle = $this->getFullArticle($item['uri']); $fullArticle = $this->getFullArticle($item['uri']);
$item['content'] = <<<EOD $item['content'] = <<<EOD
<img src="{$item['enclosures'][0]}" alt="{$item['author']}">
<p>{$fullArticle}</p> <p>{$fullArticle}</p>
EOD; EOD;
} else { } else {
@@ -85,11 +77,21 @@ EOD;
EOD; EOD;
} }
$item['categories'] = array_map(function($e){ return $e->plaintext; }, $article->find('div.tags span.tag')); // categories
foreach ($article->find('a.crayons-tag') as $tag) {
$item['categories'][] = str_replace('#', '', $tag->plaintext);
}
$this->items[] = $item; $this->items[] = $item;
} }
}
public function getName() {
if (!is_null($this->getInput('tag'))) {
return ucfirst($this->getInput('tag')) . ' - dev.to';
}
return parent::getName();
} }
private function getFullArticle($url) { private function getFullArticle($url) {
@@ -98,6 +100,10 @@ EOD;
$html = defaultLinkTo($html, static::URI); $html = defaultLinkTo($html, static::URI);
if ($html->find('div.crayons-article__cover', 0)) {
return $html->find('div.crayons-article__cover', 0) . $html->find('[id="article-body"]', 0);
}
return $html->find('[id="article-body"]', 0); return $html->find('[id="article-body"]', 0);
} }
} }

View File

@@ -0,0 +1,84 @@
<?php
class DiarioDeNoticiasBridge extends BridgeAbstract {
const NAME = 'Diário de Notícias (PT)';
const URI = 'https://dn.pt';
const DESCRIPTION = 'Diário de Notícias (DN.PT)';
const MAINTAINER = 'somini';
const PARAMETERS = array(
'Tag' => array(
'n' => array(
'name' => 'Tag Name',
'exampleValue' => 'rogerio-casanova',
)
)
);
const MONPT = array(
'jan',
'fev',
'mar',
'abr',
'mai',
'jun',
'jul',
'ago',
'set',
'out',
'nov',
'dez',
);
public function getIcon() {
return 'https://static.globalnoticias.pt/dn/common/images/favicons/favicon-128.png';
}
public function getName() {
switch($this->queriedContext) {
case 'Tag':
$name = self::NAME . ' | Tag | ' . $this->getInput('n');
break;
default:
$name = self::NAME;
}
return $name;
}
public function getURI() {
switch($this->queriedContext) {
case 'Tag':
$url = self::URI . '/tag/' . $this->getInput('n') . '.html';
break;
default:
$url = self::URI;
}
return $url;
}
public function collectData() {
$archives = self::getURI();
$html = getSimpleHTMLDOMCached($archives)
or returnServerError('Could not load content');
foreach($html->find('article') as $element) {
$item = array();
$title = $element->find('.t-am-title', 0);
$link = $element->find('a.t-am-text', 0);
$item['title'] = $title->plaintext;
$item['uri'] = self::URI . $link->href;
$snippet = $element->find('.t-am-lead', 0);
if ($snippet) {
$item['content'] = $snippet->plaintext;
}
preg_match('|edicao-do-dia\\/(?P<day>\d\d)-(?P<monpt>\w\w\w)-(?P<year>\d\d\d\d)|', $link->href, $d);
if ($d) {
$item['timestamp'] = sprintf('%s-%s-%s', $d['year'], array_search($d['monpt'], self::MONPT) + 1, $d['day']);
}
$this->items[] = $item;
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
class DiarioDoAlentejoBridge extends BridgeAbstract {
const MAINTAINER = 'somini';
const NAME = 'Diário do Alentejo';
const URI = 'https://www.diariodoalentejo.pt';
const DESCRIPTION = 'Semanário Regionalista Independente';
const CACHE_TIMEOUT = 28800; // 8h
/* This is used to hack around obtaining a timestamp. It's just a list of Month names in Portuguese ... */
const PT_MONTH_NAMES = array(
'janeiro',
'fevereiro',
'março',
'abril',
'maio',
'junho',
'julho',
'agosto',
'setembro',
'outubro',
'novembro',
'dezembro');
public function getIcon() {
return 'https://www.diariodoalentejo.pt/images/favicon/apple-touch-icon.png';
}
public function collectData(){
/* This is slow as molasses (>30s!), keep the cache timeout high to avoid killing the host */
$html = getSimpleHTMLDOMCached($this->getURI() . '/pt/noticias-listagem.aspx')
or returnServerError('Could not load content');
foreach($html->find('.list_news .item') as $element) {
$item = array();
$item_link = $element->find('.body h2.title a', 0);
/* Another broken URL, see also `bridges/ComboiosDePortugalBridge.php` */
$item['uri'] = self::URI . implode('/', array_map('urlencode', explode('/', $item_link->href)));
$item['title'] = $item_link->innertext;
$item['timestamp'] = str_ireplace(
array_map(function($name) { return ' ' . $name . ' '; }, self::PT_MONTH_NAMES),
array_map(function($num) { return sprintf('-%02d-', $num); }, range(1, sizeof(self::PT_MONTH_NAMES))),
$element->find('span.date', 0)->innertext);
/* Fix the Image URL */
$item_image = $element->find('img.thumb', 0);
$item_image->src = preg_replace('/.*&img=([^&]+).*/', '\1', $item_image->getAttribute('data-src'));
/* Content: */
/* - Image */
/* - Category */
$content = $item_image .
'<center>' . $element->find('a.category', 0) . '</center>';
$item['content'] = defaultLinkTo($content, self::URI);
$this->items[] = $item;
}
}
}

123
bridges/DonnonsBridge.php Normal file
View File

@@ -0,0 +1,123 @@
<?php
/**
* Retourne les dons d'une recherche filtrée sur le site Donnons.org
* Example: https://donnons.org/Sport/Ile-de-France
*/
class DonnonsBridge extends BridgeAbstract {
const MAINTAINER = 'Binnette';
const NAME = 'Donnons.org';
const URI = 'https://donnons.org';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Retourne les dons depuis le site Donnons.org.';
const PARAMETERS = array(
array(
'q' => array(
'name' => 'Url de recherche',
'required' => true,
'exampleValue' => '/Sport/Ile-de-France',
'pattern' => '\/.*',
'title' => 'Faites une recherche sur le site. Puis copiez ici la fin de lurl. Doit commencer par /',
),
'p' => array(
'name' => 'Nombre de pages à scanner',
'type' => 'number',
'defaultValue' => 5,
'title' => 'Indique le nombre de pages de donnons.org qui seront scannées'
)
)
);
public function collectData() {
$pages = $this->getInput('p');
for($i = 1; $i <= $pages; $i++) {
$this->collectDataByPage($i);
}
}
private function collectDataByPage($page) {
$uri = $this->getPageURI($page);
$html = getSimpleHTMLDOM($uri)
or returnServerError('No results for this query.');
$searchDiv = $html->find('div[id=search]', 0);
if(!is_null($searchDiv)) {
$elements = $searchDiv->find('a.lst-annonce');
foreach($elements as $element) {
$item = array();
// Lien vers le don
$item['uri'] = self::URI . $element->href;
// Id de l'objet
$item['uid'] = $element->getAttribute('data-id');
// Grab info from json
$jsonString = $element->find('script', 0)->innertext;
$json = json_decode($jsonString, true);
$name = $json['name'];
$category = $json['category'];
$date = $json['availabilityStarts'];
$description = $json['description'];
$city = $json['availableAtOrFrom']['address']['addressLocality'];
$region = $json['availableAtOrFrom']['address']['addressRegion'];
// Grab info from HTML
$imageSrc = $element->find('img.ima-center', 0)->getAttribute('data-src');
$image = self::URI . $imageSrc;
$author = $element->find('div.avatar-holder', 0)->plaintext;
$content = '
<img style="margin-right:1em;" src="' . $image . '">
<div>
<h1>' . $name . '</h1>
<p>' . $description . '</p>
<p>Lieu : <b>' . $city . '</b> - ' . $region . '</p>
<p>Par : ' . $author . '</p>
<p>Date : ' . $date . '</p>
</div>
';
// Titre du don
$item['title'] = '[' . $category . '] ' . $name;
$item['timestamp'] = $date;
$item['author'] = $author;
$item['content'] = $content;
$item['enclosures'] = array($image);
$this->items[] = $item;
}
}
}
private function getPageURI($page) {
$uri = $this->getURI();
$haveQueryParams = strpos($uri, '?') !== false;
if($haveQueryParams) {
return $uri . '&page=' . $page;
} else {
return $uri . '?page=' . $page;
}
}
public function getURI() {
if(!is_null($this->getInput('q'))) {
return self::URI . $this->getInput('q');
}
return parent::getURI();
}
public function getName() {
if(!is_null($this->getInput('q'))) {
return 'Donnons.org - ' . $this->getInput('q');
}
return parent::getName();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,48 +13,51 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
} }
public function collectData(){ public function collectData(){
$html = getSimpleHTMLDOM(self::URI . '/shots') $html = getSimpleHTMLDOM(self::URI)
or returnServerError('Error while downloading the website content'); or returnServerError('Error while downloading the website content');
$json = $this->loadEmbeddedJsonData($html); $json = $this->loadEmbeddedJsonData($html);
foreach($html->find('li[id^="screenshot-"]') as $shot) { foreach($html->find('li[id^="screenshot-"]') as $shot) {
$item = []; $item = array();
$additional_data = $this->findJsonForShot($shot, $json); $additional_data = $this->findJsonForShot($shot, $json);
if ($additional_data === null) { if ($additional_data === null) {
$item['uri'] = self::URI . $shot->find('a', 0)->href; $item['uri'] = self::URI . $shot->find('a', 0)->href;
$item['title'] = $shot->find('.dribbble-over strong', 0)->plaintext; $item['title'] = $shot->find('.shot-title', 0)->plaintext;
} else { } else {
$item['timestamp'] = strtotime($additional_data['published_at']); $item['timestamp'] = strtotime($additional_data['published_at']);
$item['uri'] = self::URI . $additional_data['path']; $item['uri'] = self::URI . $additional_data['path'];
$item['title'] = $additional_data['title']; $item['title'] = $additional_data['title'];
} }
$item['author'] = trim($shot->find('.attribution-user a', 0)->plaintext); $item['author'] = trim($shot->find('.user-information .display-name', 0)->plaintext);
$description = $shot->find('.comment', 0); $description = $shot->find('.comment', 0);
$item['content'] = $description === null ? '' : $description->plaintext; $item['content'] = $description === null ? '' : $description->plaintext;
$preview_path = $shot->find('picture source', 0)->attr['srcset']; $preview_path = $shot->find('figure img', 1)->attr['data-srcset'];
$item['content'] .= $this->getImageTag($preview_path, $item['title']); $item['content'] .= $this->getImageTag($preview_path, $item['title']);
$item['enclosures'] = [$this->getFullSizeImagePath($preview_path)]; $item['enclosures'] = array($this->getFullSizeImagePath($preview_path));
$this->items[] = $item; $this->items[] = $item;
} }
} }
private function loadEmbeddedJsonData($html){ private function loadEmbeddedJsonData($html){
$json = []; $json = array();
$scripts = $html->find('script'); $scripts = $html->find('script');
foreach($scripts as $script) { foreach($scripts as $script) {
if(strpos($script->innertext, 'newestShots') !== false) { if(strpos($script->innertext, 'newestShots') !== false) {
// fix single quotes // fix single quotes
$script->innertext = str_replace('\'', '"', $script->innertext); $script->innertext = preg_replace('/\'(.*)\'(,?)$/im', '"\1"\2', $script->innertext);
// fix JavaScript JSON (why do they not adhere to the standard?) // fix JavaScript JSON (why do they not adhere to the standard?)
$script->innertext = preg_replace('/(\w+):/i', '"\1":', $script->innertext); $script->innertext = preg_replace('/^(\s*)(\w+):/im', '\1"\2":', $script->innertext);
// fix relative dates, so they are recognized by strtotime
$script->innertext = preg_replace('/"about ([0-9]+ hours? ago)"(,?)$/im', '"\1"\2', $script->innertext);
// find beginning of JSON array // find beginning of JSON array
$start = strpos($script->innertext, '['); $start = strpos($script->innertext, '[');
@@ -91,6 +94,6 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
} }
private function getFullSizeImagePath($preview_path){ private function getFullSizeImagePath($preview_path){
return str_replace('_1x', '', $preview_path); return explode('?compress=1', $preview_path)[0];
} }
} }

View File

@@ -40,7 +40,7 @@ class EconomistBridge extends BridgeAbstract {
if ($nextprev) if ($nextprev)
$nextprev->outertext = ''; $nextprev->outertext = '';
$section = [ $article->find('h3[itemprop="articleSection"]', 0)->plaintext ]; $section = array( $article->find('h3[itemprop="articleSection"]', 0)->plaintext );
$item = array(); $item = array();
$item['title'] = $header->find('span', 0)->innertext . ': ' $item['title'] = $header->find('span', 0)->innertext . ': '

View File

@@ -95,7 +95,7 @@ class ElloBridge extends BridgeAbstract {
private function getEnclosures($post, $postData) { private function getEnclosures($post, $postData) {
$assets = []; $assets = array();
foreach($post->links->assets as $asset) { foreach($post->links->assets as $asset) {
foreach($postData->linked->assets as $assetLink) { foreach($postData->linked->assets as $assetLink) {
if($asset == $assetLink->id) { if($asset == $assetLink->id) {
@@ -124,7 +124,7 @@ class ElloBridge extends BridgeAbstract {
$cacheFac->setWorkingDir(PATH_LIB_CACHES); $cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope(get_called_class()); $cache->setScope(get_called_class());
$cache->setKey(['key']); $cache->setKey(array('key'));
$key = $cache->loadData(); $key = $cache->loadData();
if($key == null) { if($key == null) {

View File

@@ -3,7 +3,7 @@ class ElsevierBridge extends BridgeAbstract {
const MAINTAINER = 'Pierre Mazière'; const MAINTAINER = 'Pierre Mazière';
const NAME = 'Elsevier journals recent articles'; const NAME = 'Elsevier journals recent articles';
const URI = 'http://www.journals.elsevier.com/'; const URI = 'https://www.journals.elsevier.com/';
const CACHE_TIMEOUT = 43200; //12h const CACHE_TIMEOUT = 43200; //12h
const DESCRIPTION = 'Returns the recent articles published in Elsevier journals'; const DESCRIPTION = 'Returns the recent articles published in Elsevier journals';

View File

@@ -0,0 +1,93 @@
<?php
class EpicgamesBridge extends BridgeAbstract {
const NAME = 'Epic Games Store News';
const MAINTAINER = 'otakuf';
const URI = 'https://www.epicgames.com';
const DESCRIPTION = 'Returns the latest posts from epicgames.com';
const CACHE_TIMEOUT = 3600; // 60min
const PARAMETERS = array( array(
'postcount' => array(
'name' => 'Limit',
'type' => 'number',
'title' => 'Maximum number of items to return',
'defaultValue' => 10,
),
'language' => array(
'name' => 'Language',
'type' => 'list',
'values' => array(
'English' => 'en',
'العربية' => 'ar',
'Deutsch' => 'de',
'Español (Spain)' => 'es-ES',
'Español (LA)' => 'es-MX',
'Français' => 'fr',
'Italiano' => 'it',
'日本語' => 'ja',
'한국어' => 'ko',
'Polski' => 'pl',
'Português (Brasil)' => 'pt-BR',
'Русский' => 'ru',
'ไทย' => 'th',
'Türkçe' => 'tr',
'简体中文' => 'zh-CN',
'繁體中文' => 'zh-Hant',
),
'title' => 'Language of blog posts',
'defaultValue' => 'en',
),
));
public function collectData() {
$api = 'https://store-content.ak.epicgames.com/api/';
// Get sticky posts first
// Example: https://store-content.ak.epicgames.com/api/ru/content/blog/sticky?locale=ru
$urlSticky = $api . $this->getInput('language') . '/content/blog/sticky';
// Then get posts
// Example: https://store-content.ak.epicgames.com/api/ru/content/blog?limit=25
$urlBlog = $api . $this->getInput('language') . '/content/blog?limit=' . $this->getInput('postcount');
$dataSticky = getContents($urlSticky)
or returnServerError('Unable to get the sticky posts from epicgames.com!');
$dataBlog = getContents($urlBlog)
or returnServerError('Unable to get the news posts from epicgames.com!');
// Merge data
$decodedData = array_merge(json_decode($dataSticky), json_decode($dataBlog));
foreach($decodedData as $key => $value) {
$item = array();
$item['uri'] = self::URI . $value->url;
$item['title'] = $value->title;
$item['timestamp'] = $value->date;
$item['author'] = 'Epic Games Store';
if(!empty($value->author)) {
$item['author'] = $value->author;
}
if(!empty($value->content)) {
$item['content'] = defaultLinkTo($value->content, self::URI);
}
if(!empty($value->image)) {
$item['enclosures'][] = $value->image;
}
$item['uid'] = $value->_id;
$item['id'] = $value->_id;
$this->items[] = $item;
}
// Sort data
usort($this->items, function ($item1, $item2) {
if ($item2['timestamp'] == $item1['timestamp']) {
return 0;
}
return ($item2['timestamp'] < $item1['timestamp']) ? -1 : 1;
});
// Limit data
$this->items = array_slice($this->items, 0, $this->getInput('postcount'));
}
}

View File

@@ -0,0 +1,70 @@
<?php
class EsquerdaNetBridge extends FeedExpander {
const MAINTAINER = 'somini';
const NAME = 'Esquerda.net';
const URI = 'https://www.esquerda.net';
const DESCRIPTION = 'Esquerda.net';
const PARAMETERS = array(
array(
'feed' => array(
'name' => 'Feed',
'type' => 'list',
'defaultValue' => 'Geral',
'values' => array(
'Geral' => 'geral',
'Dossier' => 'artigos-dossier',
'Vídeo' => 'video',
'Opinião' => 'opinioes',
'Rádio' => 'radio',
)
)
)
);
public function getURI() {
$type = $this->getInput('feed');
return self::URI . '/rss/' . $type;
}
public function getIcon() {
return 'https://www.esquerda.net/sites/default/files/favicon_0.ico';
}
public function collectData(){
parent::collectExpandableDatas($this->getURI());
}
protected function parseItem($newsItem){
# Fix Publish date
$badDate = $newsItem->pubDate;
preg_match('|(?P<day>\d\d)/(?P<month>\d\d)/(?P<year>\d\d\d\d) - (?P<hour>\d\d):(?P<minute>\d\d)|', $badDate, $d);
$newsItem->pubDate = sprintf('%s-%s-%sT%s:%s', $d['year'], $d['month'], $d['day'], $d['hour'], $d['minute']);
$item = parent::parseItem($newsItem);
# Include all the content
$uri = $item['uri'];
$html = getSimpleHTMLDOMCached($uri)
or returnServerError('Could not load content for ' . $uri);
$content = $html->find('div#content div.content', 0);
## Fix author
$authorHTML = $html->find('.field-name-field-op-author a', 0);
if ($authorHTML) {
$item['author'] = $authorHTML->innertext;
$authorHTML->remove();
}
## Remove crap
$content->find('.field-name-addtoany', 0)->remove();
## Fix links
$content = defaultLinkTo($content, self::URI);
## Fix Images
foreach($content->find('img') as $img) {
$altSrc = $img->getAttribute('data-src');
if ($altSrc) {
$img->setAttribute('src', $altSrc);
}
$img->width = null;
$img->height = null;
}
$item['content'] = $content;
return $item;
}
}

View File

@@ -1,7 +1,7 @@
<?php <?php
class ExtremeDownloadBridge extends BridgeAbstract { class ExtremeDownloadBridge extends BridgeAbstract {
const NAME = 'Extreme Download'; const NAME = 'Extreme Download';
const URI = 'https://ww1.extreme-d0wn.com/'; const URI = 'https://www.extreme-down.ninja/';
const DESCRIPTION = 'Suivi de série sur Extreme Download'; const DESCRIPTION = 'Suivi de série sur Extreme Download';
const MAINTAINER = 'sysadminstory'; const MAINTAINER = 'sysadminstory';
const PARAMETERS = array( const PARAMETERS = array(

View File

@@ -2,7 +2,7 @@
class FB2Bridge extends BridgeAbstract { class FB2Bridge extends BridgeAbstract {
const MAINTAINER = 'teromene'; const MAINTAINER = 'teromene';
const NAME = 'Facebook Alternate'; const NAME = 'Facebook Bridge | Touch Site';
const URI = 'https://www.facebook.com/'; const URI = 'https://www.facebook.com/';
const CACHE_TIMEOUT = 1000; const CACHE_TIMEOUT = 1000;
const DESCRIPTION = 'Input a page title or a profile log. For a profile log, const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
@@ -12,7 +12,12 @@ class FB2Bridge extends BridgeAbstract {
'u' => array( 'u' => array(
'name' => 'Username', 'name' => 'Username',
'required' => true 'required' => true
) ),
'abbrev_name' => array(
'name' => 'Abbreviate author name in title',
'type' => 'checkbox',
'defaultValue' => true,
),
)); ));
public function getIcon() { public function getIcon() {
@@ -102,12 +107,12 @@ EOD
else else
$timestamp = 0; $timestamp = 0;
$item['uri'] = html_entity_decode('http://touch.facebook.com' $item['uri'] = html_entity_decode('https://touch.facebook.com'
. $content->find("div[class='_52jc _5qc4 _78cz _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href'), ENT_QUOTES); . $content->find("div[class='_52jc _5qc4 _78cz _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href'), ENT_QUOTES);
//Decode images //Decode images
$imagecleaned = preg_replace_callback('/<i [^>]* style="[^"]*url\(\'(.*?)\'\).*?><\/i>/m', function ($matches) { $imagecleaned = preg_replace_callback('/<i [^>]* style="[^"]*url\(\'(.*?)\'\).*?><\/i>/m', function ($matches) {
return "<img src='" . str_replace(['\\3a ', '\\3d ', '\\26 '], [':', '=', '&'], $matches[1]) . "' />"; return "<img src='" . str_replace(array('\\3a ', '\\3d ', '\\26 '), array(':', '=', '&'), $matches[1]) . "' />";
}, $content); }, $content);
$content = str_get_html($imagecleaned); $content = str_get_html($imagecleaned);
@@ -159,7 +164,11 @@ EOD
$content = preg_replace('/<img src=\'.*?safe_image\.php.*?\' \/>/m', '', $content); $content = preg_replace('/<img src=\'.*?safe_image\.php.*?\' \/>/m', '', $content);
//Remove the double section tags //Remove the double section tags
$content = str_replace(['<section><section>', '</section></section>'], ['<section>', '</section>'], $content); $content = str_replace(
array('<section><section>', '</section></section>'),
array('<section>', '</section>'),
$content
);
//Move the section tag link upper, if it is down //Move the section tag link upper, if it is down
$content = str_get_html($content); $content = str_get_html($content);
@@ -182,8 +191,10 @@ EOD
$item['content'] = html_entity_decode($content, ENT_QUOTES); $item['content'] = html_entity_decode($content, ENT_QUOTES);
$title = $author; $title = $author;
if (strlen($title) > 24) if ($this->getInput('abbrev_name') === true) {
$title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...'; if (strlen($title) > 24)
$title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...';
}
$title = $title . ' | ' . strip_tags($content); $title = $title . ' | ' . strip_tags($content);
if (strlen($title) > 64) if (strlen($title) > 64)
$title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...'; $title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
@@ -281,10 +292,20 @@ EOD
} }
public function getName(){ public function getName(){
return (isset($this->name) ? $this->name . ' - ' : '') . 'Facebook Bridge'; $username = $this->getInput('u');
if (isset($username)) {
return $this->getInput('u') . ' | Facebook';
} else {
return self::NAME;
}
} }
public function getURI(){ public function getURI(){
return 'http://facebook.com'; $username = $this->getInput('u');
if (isset($username)) {
return 'https://facebook.com/' . $this->getInput('u') . '/posts';
} else {
return self::URI;
}
} }
} }

67
bridges/FM4Bridge.php Normal file
View File

@@ -0,0 +1,67 @@
<?php
class FM4Bridge extends BridgeAbstract
{
const MAINTAINER = 'joni1993';
const NAME = 'FM4 Bridge';
const URI = 'https://fm4.orf.at';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Feed for FM4 articles by tags (authors)';
const PARAMETERS = array(
array(
'tag' => array(
'name' => 'Tag (author, category, ...)',
'title' => 'Tag to retrieve',
'exampleValue' => 'musik'
),
'loadcontent' => array(
'name' => 'Load Full Article Content',
'title' => 'Retrieve full content of articles (may take longer)',
'type' => 'checkbox'
),
'pages' => array(
'name' => 'Pages',
'title' => 'Amount of pages to load',
'type' => 'number',
'defaultValue' => 1
)
)
);
private function getPageData($tag, $page) {
if($tag)
$uri = self::URI . '/tags/' . $tag;
else
$uri = self::URI;
$uri = $uri . '?page=' . $page;
$html = getSimpleHTMLDOM($uri)
or returnServerError('Error while downloading the website content');
$page_items = array();
foreach ($html->find('div[class*=listItem]') as $article) {
$item = array();
$item['uri'] = $article->find('a', 0)->href;
$item['title'] = $article->find('h2', 0)->plaintext;
$item['author'] = $article->find('p[class*=keyword]', 0)->plaintext;
$item['timestamp'] = strtotime($article->find('p[class*=time]', 0)->plaintext);
if ($this->getInput('loadcontent')) {
$item['content'] = getSimpleHTMLDOM($item['uri'])->find('div[class=storyText]', 0)->innertext
or returnServerError('Error while downloading the full article');
}
$page_items[] = $item;
}
return $page_items;
}
public function collectData() {
for ($cur_page = 1; $cur_page <= $this->getInput('pages'); $cur_page++) {
$this->items = array_merge($this->items, $this->getPageData($this->getInput('tag'), $cur_page));
}
}
}

View File

@@ -2,7 +2,7 @@
class FacebookBridge extends BridgeAbstract { class FacebookBridge extends BridgeAbstract {
const MAINTAINER = 'teromene, logmanoriginal'; const MAINTAINER = 'teromene, logmanoriginal';
const NAME = 'Facebook Bridge'; const NAME = 'Facebook Bridge | Main Site';
const URI = 'https://www.facebook.com/'; const URI = 'https://www.facebook.com/';
const CACHE_TIMEOUT = 300; // 5min const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Input a page title or a profile log. For a profile log, const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
@@ -30,7 +30,7 @@ class FacebookBridge extends BridgeAbstract {
'type' => 'checkbox', 'type' => 'checkbox',
'required' => false, 'required' => false,
'defaultValue' => false, 'defaultValue' => false,
'title' => 'Feed includes reviews when checked' 'title' => 'Feed includes reviews when unchecked'
) )
), ),
'Group' => array( 'Group' => array(
@@ -66,14 +66,13 @@ class FacebookBridge extends BridgeAbstract {
case 'User': case 'User':
if(!empty($this->authorName)) { if(!empty($this->authorName)) {
return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName;
. ' - ' . static::NAME;
} }
break; break;
case 'Group': case 'Group':
if(!empty($this->groupName)) { if(!empty($this->groupName)) {
return $this->groupName . ' - ' . static::NAME; return $this->groupName;
} }
break; break;
@@ -82,6 +81,34 @@ class FacebookBridge extends BridgeAbstract {
return parent::getName(); return parent::getName();
} }
public function detectParameters($url){
$params = array();
// By profile
$regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/profile\.php\?id\=([^\/?&\n]+)?(.*)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['u'] = urldecode($matches[3]);
return $params;
}
// By group
$regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/groups\/([^\/?\n]+)?(.*)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['g'] = urldecode($matches[3]);
return $params;
}
// By username
$regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/([^\/?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['u'] = urldecode($matches[3]);
return $params;
}
return null;
}
public function getURI() { public function getURI() {
$uri = self::URI; $uri = self::URI;
@@ -148,7 +175,13 @@ class FacebookBridge extends BridgeAbstract {
$header = array(); $header = array();
} }
$html = getSimpleHTMLDOM($this->getURI(), $header) $touchURI = str_replace(
'https://www.facebook',
'https://touch.facebook',
$this->getURI()
);
$html = getSimpleHTMLDOM($touchURI, $header)
or returnServerError('Failed loading facebook page: ' . $this->getURI()); or returnServerError('Failed loading facebook page: ' . $this->getURI());
if(!$this->isPublicGroup($html)) { if(!$this->isPublicGroup($html)) {
@@ -159,19 +192,18 @@ class FacebookBridge extends BridgeAbstract {
$this->groupName = $this->extractGroupName($html); $this->groupName = $this->extractGroupName($html);
$posts = $html->find('div.userContentWrapper') $posts = $html->find('div.story_body_container')
or returnServerError('Failed finding posts!'); or returnServerError('Failed finding posts!');
foreach($posts as $post) { foreach($posts as $post) {
$item = array(); $item = array();
$item['uri'] = $this->extractGroupURI($post); $item['uri'] = $this->extractGroupPostURI($post);
$item['title'] = $this->extractGroupTitle($post); $item['title'] = $this->extractGroupPostTitle($post);
$item['author'] = $this->extractGroupAuthor($post); $item['author'] = $this->extractGroupPostAuthor($post);
$item['content'] = $this->extractGroupContent($post); $item['content'] = $this->extractGroupPostContent($post);
$item['timestamp'] = $this->extractGroupTimestamp($post); $item['enclosures'] = $this->extractGroupPostEnclosures($post);
$item['enclosures'] = $this->extractGroupEnclosures($post);
$this->items[] = $item; $this->items[] = $item;
@@ -188,16 +220,7 @@ class FacebookBridge extends BridgeAbstract {
$urlparts = parse_url($group); $urlparts = parse_url($group);
if($urlparts['host'] !== parse_url(self::URI)['host'] $this->validateHost($urlparts['host']);
&& 'www.' . $urlparts['host'] !== parse_url(self::URI)['host']) {
returnClientError('The host you provided is invalid! Received "'
. $urlparts['host']
. '", expected "'
. parse_url(self::URI)['host']
. '"!');
}
return explode('/', $urlparts['path'])[2]; return explode('/', $urlparts['path'])[2];
@@ -209,24 +232,47 @@ class FacebookBridge extends BridgeAbstract {
} }
private function validateHost($provided_host) {
// Handle mobile links
if (strpos($provided_host, 'm.') === 0) {
$provided_host = substr($provided_host, strlen('m.'));
}
if (strpos($provided_host, 'touch.') === 0) {
$provided_host = substr($provided_host, strlen('touch.'));
}
$facebook_host = parse_url(self::URI)['host'];
if ($provided_host !== $facebook_host
&& 'www.' . $provided_host !== $facebook_host) {
returnClientError('The host you provided is invalid! Received "'
. $provided_host
. '", expected "'
. $facebook_host
. '"!');
}
}
/**
* @param $html simple_html_dom
* @return bool
*/
private function isPublicGroup($html) { private function isPublicGroup($html) {
// Facebook redirects to the groups about page for non-public groups // Facebook touch just presents a login page for non-public groups
$about = $html->find('#pagelet_group_about', 0); $title = $html->find('title', 0);
return $title->plaintext !== 'Log in to Facebook | Facebook';
return !($about);
} }
private function extractGroupName($html) { private function extractGroupName($html) {
$ogtitle = $html->find('meta[property="og:title"]', 0) $ogtitle = $html->find('._de1', 0)
or returnServerError('Unable to find group title!'); or returnServerError('Unable to find group title!');
return html_entity_decode($ogtitle->content, ENT_QUOTES); return html_entity_decode($ogtitle->plaintext, ENT_QUOTES);
} }
private function extractGroupURI($post) { private function extractGroupPostURI($post) {
$elements = $post->find('a') $elements = $post->find('a')
or returnServerError('Unable to find URI!'); or returnServerError('Unable to find URI!');
@@ -235,7 +281,8 @@ class FacebookBridge extends BridgeAbstract {
// Find the one that is a permalink // Find the one that is a permalink
if(strpos($anchor->href, 'permalink') !== false) { if(strpos($anchor->href, 'permalink') !== false) {
return $anchor->href; $arr = explode('?', $anchor->href, 2);
return $arr[0];
} }
} }
@@ -244,57 +291,61 @@ class FacebookBridge extends BridgeAbstract {
} }
private function extractGroupContent($post) { private function extractGroupPostContent($post) {
$content = $post->find('div.userContent', 0) $content = $post->find('div._5rgt', 0)
or returnServerError('Unable to find user content!'); or returnServerError('Unable to find user content!');
return $content->innertext . $content->next_sibling()->innertext; $context_text = $content->innertext;
if ($content->next_sibling() !== null) {
$context_text .= $content->next_sibling()->innertext;
}
return $context_text;
} }
private function extractGroupTimestamp($post) { private function extractGroupPostAuthor($post) {
$element = $post->find('abbr[data-utime]', 0) $element = $post->find('h3 a', 0)
or returnServerError('Unable to find timestamp!');
return $element->getAttribute('data-utime');
}
private function extractGroupAuthor($post) {
$element = $post->find('img', 0)
or returnServerError('Unable to find author information!'); or returnServerError('Unable to find author information!');
return $element->{'aria-label'}; return $element->plaintext;
} }
private function extractGroupEnclosures($post) { private function extractGroupPostEnclosures($post) {
$elements = $post->find('div.userContent', 0)->next_sibling()->find('img'); $elements = $post->find('span._6qdm');
if ($post->find('div._5rgt', 0)->next_sibling() !== null) {
array_push($elements, ...$post->find('div._5rgt', 0)->next_sibling()->find('i.img'));
}
$enclosures = array(); $enclosures = array();
$background_img_regex = '/background-image: ?url\\((.+?)\\);/';
foreach($elements as $enclosure) { foreach($elements as $enclosure) {
$enclosures[] = $enclosure->src; if(preg_match($background_img_regex, $enclosure, $matches) > 0) {
$bg_img_value = trim(html_entity_decode($matches[1], ENT_QUOTES), "'\"");
$bg_img_url = urldecode(preg_replace('/\\\([0-9a-z]{2}) /', '%$1', $bg_img_value));
$enclosures[] = urldecode($bg_img_url);
}
} }
return empty($enclosures) ? null : $enclosures; return empty($enclosures) ? null : $enclosures;
} }
private function extractGroupTitle($post) { private function extractGroupPostTitle($post) {
$element = $post->find('h5', 0) $element = $post->find('h3', 0)
or returnServerError('Unable to find title!'); or returnServerError('Unable to find title!');
if(strpos($element->plaintext, 'shared') === false) { if(strpos($element->plaintext, 'shared') === false) {
$content = strip_tags($this->extractGroupContent($post)); $content = strip_tags($this->extractGroupPostContent($post));
return $this->extractGroupAuthor($post) return $this->extractGroupPostAuthor($post)
. ' posted: ' . ' posted: '
. substr( . substr(
$content, $content,
@@ -321,13 +372,7 @@ class FacebookBridge extends BridgeAbstract {
$urlparts = parse_url($user); $urlparts = parse_url($user);
if($urlparts['host'] !== parse_url(self::URI)['host']) { $this->validateHost($urlparts['host']);
returnClientError('The host you provided is invalid! Received "'
. $urlparts['host']
. '", expected "'
. parse_url(self::URI)['host']
. '"!');
}
if(!array_key_exists('path', $urlparts) if(!array_key_exists('path', $urlparts)
|| $urlparts['path'] === '/') { || $urlparts['path'] === '/') {
@@ -528,7 +573,7 @@ EOD;
} }
// No captcha? We can carry on retrieving page contents :) // No captcha? We can carry on retrieving page contents :)
// First, we check wether the page is public or not // First, we check whether the page is public or not
$loginForm = $html->find('._585r', 0); $loginForm = $html->find('._585r', 0);
if($loginForm != null) { if($loginForm != null) {
@@ -674,8 +719,15 @@ EOD;
$uri = $post->find('abbr')[0]->parent()->getAttribute('href'); $uri = $post->find('abbr')[0]->parent()->getAttribute('href');
if (false !== strpos($uri, '?')) { // Extract fbid and patch link
$uri = substr($uri, 0, strpos($uri, '?')); if (strpos($uri, '?') !== false) {
$query = substr($uri, strpos($uri, '?') + 1);
parse_str($query, $query_params);
if (isset($query_params['story_fbid'])) {
$uri = self::URI . $query_params['story_fbid'];
} else {
$uri = substr($uri, 0, strpos($uri, '?'));
}
} }
//Build and add final item //Build and add final item
@@ -695,6 +747,7 @@ EOD;
} }
} }
} }
#endregion (User) #endregion (User)
} }

View File

@@ -35,6 +35,8 @@ class FicbookBridge extends BridgeAbstract {
), ),
); );
protected $titleName;
public function getURI() { public function getURI() {
switch($this->queriedContext) { switch($this->queriedContext) {
case 'Site News': { case 'Site News': {
@@ -56,6 +58,21 @@ class FicbookBridge extends BridgeAbstract {
} }
} }
public function getName() {
switch($this->queriedContext) {
case 'Site News': {
return $this->queriedContext . ' | ' . self::NAME;
}
case 'Fiction Updates': {
return $this->titleName . ' | ' . self::NAME;
}
case 'Fiction Comments': {
return $this->titleName . ' | Comments | ' . self::NAME;
}
default: return self::NAME;
}
}
public function collectData() { public function collectData() {
$header = array('Accept-Language: en-US'); $header = array('Accept-Language: en-US');
@@ -65,6 +82,10 @@ class FicbookBridge extends BridgeAbstract {
$html = defaultLinkTo($html, self::URI); $html = defaultLinkTo($html, self::URI);
if ($this->queriedContext == 'Fiction Updates' or $this->queriedContext == 'Fiction Comments') {
$this->titleName = $html->find('.fanfic-main-info > h1', 0)->innertext;
}
switch($this->queriedContext) { switch($this->queriedContext) {
case 'Site News': return $this->collectSiteNews($html); case 'Site News': return $this->collectSiteNews($html);
case 'Fiction Updates': return $this->collectUpdatesData($html); case 'Fiction Updates': return $this->collectUpdatesData($html);
@@ -84,7 +105,7 @@ class FicbookBridge extends BridgeAbstract {
} }
private function collectCommentsData($html) { private function collectCommentsData($html) {
foreach($html->find('article.post') as $article) { foreach($html->find('article.comment-container') as $article) {
$this->items[] = array( $this->items[] = array(
'uri' => $article->find('.comment_link_to_fic > a', 0)->href, 'uri' => $article->find('.comment_link_to_fic > a', 0)->href,
'title' => $article->find('.comment_author', 0)->plaintext, 'title' => $article->find('.comment_author', 0)->plaintext,
@@ -97,7 +118,7 @@ class FicbookBridge extends BridgeAbstract {
} }
private function collectUpdatesData($html) { private function collectUpdatesData($html) {
foreach($html->find('ul.table-of-contents > li') as $chapter) { foreach($html->find('ul.list-of-fanfic-parts > li') as $chapter) {
$item = array( $item = array(
'uri' => $chapter->find('a', 0)->href, 'uri' => $chapter->find('a', 0)->href,
'title' => $chapter->find('a', 0)->plaintext, 'title' => $chapter->find('a', 0)->plaintext,
@@ -130,10 +151,10 @@ class FicbookBridge extends BridgeAbstract {
'июня', 'июня',
'июля', 'июля',
'августа', 'августа',
'Сентября', 'сентября',
'октября', 'октября',
'Ноября', 'ноября',
'Декабря', 'декабря',
); );
$en_month = array( $en_month = array(

View File

@@ -0,0 +1,50 @@
<?php
class FirstLookMediaTechBridge extends BridgeAbstract {
const NAME = 'First Look Media - Technology';
const URI = 'https://tech.firstlook.media';
const DESCRIPTION = 'First Look Media Technology page';
const MAINTAINER = 'somini';
const PARAMETERS = array(
array(
'projects' => array(
'type' => 'checkbox',
'name' => 'Include Projects?',
)
)
);
public function collectData() {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not load content');
if ($this->getInput('projects')) {
$top_projects = $html->find('.PromoList-ul', 0);
foreach($top_projects->find('li.PromoList-item') as $element) {
$item = array();
$item_uri = $element->find('a', 0);
$item['uri'] = $item_uri->href;
$item['title'] = strip_tags($item_uri->innertext);
$item['content'] = $element->find('div > div', 0);
$this->items[] = $item;
}
}
$top_articles = $html->find('.PromoList-ul', 1);
foreach($top_articles->find('li.PromoList-item') as $element) {
$item = array();
$item_left = $element->find('div > div', 0);
$item_date = $element->find('.PromoList-date', 0);
$item['timestamp'] = strtotime($item_date->innertext);
$item_date->outertext = ''; /* Remove */
$item['author'] = $item_left->innertext;
$item_uri = $element->find('a', 0);
$item['uri'] = self::URI . $item_uri->href;
$item['title'] = strip_tags($item_uri);
$this->items[] = $item;
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
class FolhaDeSaoPauloBridge extends FeedExpander {
const MAINTAINER = 'somini';
const NAME = 'Folha de São Paulo';
const URI = 'https://www1.folha.uol.com.br';
const DESCRIPTION = 'Returns the newest posts from Folha de São Paulo (full text)';
const PARAMETERS = array(
array(
'feed' => array(
'name' => 'Feed sub-URL',
'type' => 'text',
'title' => 'Select the sub-feed (see https://www1.folha.uol.com.br/feed/)',
'exampleValue' => 'emcimadahora/rss091.xml',
)
)
);
protected function parseItem($item){
$item = parent::parseItem($item);
$articleHTMLContent = getSimpleHTMLDOMCached($item['uri']);
if($articleHTMLContent) {
foreach ($articleHTMLContent->find('div.c-news__body .is-hidden') as $toRemove) {
$toRemove->innertext = '';
}
$item_content = $articleHTMLContent->find('div.c-news__body', 0);
if ($item_content) {
$text = $item_content->innertext;
$text = strip_tags($text, '<p><b><a><blockquote><figure><figcaption><img><strong><em>');
$item['content'] = $text;
$item['uri'] = explode('*', $item['uri'])[1];
}
} else {
Debug::log('???: ' . $item['uri']);
}
return $item;
}
public function collectData(){
$feed_input = $this->getInput('feed');
if (substr($feed_input, 0, strlen(self::URI)) === self::URI) {
Debug::log('Input:: ' . $feed_input);
$feed_url = $feed_input;
} else {
/* TODO: prepend `/` if missing */
$feed_url = self::URI . '/' . $this->getInput('feed');
}
Debug::log('URL: ' . $feed_url);
$this->collectExpandableDatas($feed_url);
}
}

View File

@@ -0,0 +1,27 @@
<?php
class FreeCodeCampBridge extends FeedExpander {
const MAINTAINER = 'IceWreck';
const NAME = 'FreeCodecamp Bridge';
const URI = 'https://www.freecodecamp.org';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'RSS feed for FreeCodeCamp';
// Freecodecamp removed their old full content rss feed and replaced it with one liner content.
public function collectData(){
$this->collectExpandableDatas('https://www.freecodecamp.org/news/rss/', 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);
// the actual article
foreach($articlePage->find('.post-full-content') as $element)
$article = $article . $element;
$item['content'] = $article;
return $item;
}
}

View File

@@ -0,0 +1,110 @@
<?php
class FurAffinityUserBridge extends BridgeAbstract {
const NAME = 'FurAffinity User Gallery';
const URI = 'https://www.furaffinity.net';
const MAINTAINER = 'CyberJacob';
const PARAMETERS = array(
array(
'searchUsername' => array(
'name' => 'Search Username',
'type' => 'text',
'required' => true,
'title' => 'Username to fetch the gallery for'
),
'loginUsername' => array(
'name' => 'Login Username',
'type' => 'text',
'required' => true
),
'loginPassword' => array(
'name' => 'Login Password',
'type' => 'text',
'required' => true
)
)
);
public function collectData() {
$cookies = self::login();
$url = self::URI . '/gallery/' . $this->getInput('searchUsername');
$html = getSimpleHTMLDOM($url, $cookies)
or returnServerError('Could not load the user\'s galary page.');
$submissions = $html->find('section[id=gallery-gallery]', 0)->find('figure');
foreach($submissions as $submission) {
$item = array();
$item['title'] = $submission->find('figcaption', 0)->find('a', 0)->plaintext;
$thumbnail = $submission->find('a', 0);
$thumbnail->href = self::URI . $thumbnail->href;
$item['content'] = $submission->find('a', 0);
$this->items[] = $item;
}
}
public function getName() {
return self::NAME . ' for ' . $this->getInput('searchUsername');
}
public function getURI() {
return self::URI . '/user/' . $this->getInput('searchUsername');
}
private function login() {
$ch = curl_init(self::URI . '/login/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, ini_get('user_agent'));
curl_setopt($ch, CURLOPT_ENCODING, '');
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
$fields = implode('&', array(
'action=login',
'retard_protection=1',
'name=' . urlencode($this->getInput('loginUsername')),
'pass=' . urlencode($this->getInput('loginPassword')),
'login=Login to Faraffinity'
));
curl_setopt($ch, CURLOPT_POST, 5);
curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
if(defined('PROXY_URL') && !defined('NOPROXY')) {
curl_setopt($ch, CURLOPT_PROXY, PROXY_URL);
}
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
$data = curl_exec($ch);
$errorCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
$curlErrno = curl_errno($ch);
$curlInfo = curl_getinfo($ch);
if($data === false)
fDebug::log("Cant't download {$url} cUrl error: {$curlError} ({$curlErrno})");
curl_close($ch);
if($errorCode != 200) {
returnServerError(error_get_last());
} else {
preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $data, $matches);
$cookies = array();
foreach($matches[1] as $item) {
parse_str($item, $cookie);
$cookies = array_merge($cookies, $cookie);
}
return $cookies;
}
}
}

View File

@@ -96,7 +96,7 @@ class FuturaSciencesBridge extends FeedExpander {
} }
private function extractArticleContent($article){ private function extractArticleContent($article){
$contents = $article->find('section.article-text-classic', 0)->innertext; $contents = $article->find('section.article-text', 1)->innertext;
$headline = trim($article->find('p.description', 0)->plaintext); $headline = trim($article->find('p.description', 0)->plaintext);
if(!empty($headline)) if(!empty($headline))
$headline = '<p><b>' . $headline . '</b></p>'; $headline = '<p><b>' . $headline . '</b></p>';
@@ -129,6 +129,7 @@ class FuturaSciencesBridge extends FeedExpander {
$contents = stripWithDelimiters($contents, 'fs:xt:clickname="', '"'); $contents = stripWithDelimiters($contents, 'fs:xt:clickname="', '"');
$contents = StripWithDelimiters($contents, '<section class="module-toretain module-propal-nl', '</section>'); $contents = StripWithDelimiters($contents, '<section class="module-toretain module-propal-nl', '</section>');
$contents = stripWithDelimiters($contents, '<script ', '</script>'); $contents = stripWithDelimiters($contents, '<script ', '</script>');
$contents = stripWithDelimiters($contents, '<script>', '</script>');
return $headline . trim($contents); return $headline . trim($contents);
} }

View File

@@ -113,8 +113,8 @@ class GBAtempBridge extends BridgeAbstract {
break; break;
case 'T': case 'T':
foreach($html->find('li.portal-tutorial') as $tutorialItem) { foreach($html->find('li.portal-tutorial') as $tutorialItem) {
$url = self::URI . $tutorialItem->find('a', 0)->href; $url = self::URI . $tutorialItem->find('a', 1)->href;
$title = $tutorialItem->find('a', 0)->plaintext; $title = $tutorialItem->find('a', 1)->plaintext;
$time = $this->findItemDate($tutorialItem); $time = $this->findItemDate($tutorialItem);
$author = $tutorialItem->find('a.username', 0)->plaintext; $author = $tutorialItem->find('a.username', 0)->plaintext;
$content = $this->fetchPostContent($url, self::URI); $content = $this->fetchPostContent($url, self::URI);

View File

@@ -117,7 +117,7 @@ class GQMagazineBridge extends BridgeAbstract
*/ */
private function loadFullArticle($uri){ private function loadFullArticle($uri){
$html = getSimpleHTMLDOMCached($uri); $html = getSimpleHTMLDOMCached($uri);
return $html->find('section[data-test-id=ArticleBodyContent]', 0); return $html->find('section[data-test-id=MainContentWrapper]', 0);
} }
/** /**

View File

@@ -5,7 +5,7 @@ class GiphyBridge extends BridgeAbstract {
const MAINTAINER = 'kraoc'; const MAINTAINER = 'kraoc';
const NAME = 'Giphy Bridge'; const NAME = 'Giphy Bridge';
const URI = 'http://giphy.com/'; const URI = 'https://giphy.com/';
const CACHE_TIMEOUT = 300; //5min const CACHE_TIMEOUT = 300; //5min
const DESCRIPTION = 'Bridge for giphy.com'; const DESCRIPTION = 'Bridge for giphy.com';

View File

@@ -82,18 +82,21 @@ class GithubIssueBridge extends BridgeAbstract {
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id); $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
$author = $comment->find('.author', 0)->plaintext; $author = $comment->find('.author', 0);
if ($author) {
$title .= ' / ' . trim($comment->plaintext); $author = $author->plaintext;
} else {
$content = $title; $author = '';
if (null !== $comment->nextSibling()) {
$content = $comment->nextSibling()->innertext;
if ($comment->nextSibling()->nodeName() === 'span') {
$content = $comment->nextSibling()->nextSibling()->innertext;
}
} }
$title .= ' / '
. trim(str_replace(
array('octicon','-'), array(''),
$comment->find('.octicon', 0)->getAttribute('class')
));
$content = $comment->plaintext;
$item = array(); $item = array();
$item['author'] = $author; $item['author'] = $author;
$item['uri'] = $uri; $item['uri'] = $uri;
@@ -135,32 +138,20 @@ class GithubIssueBridge extends BridgeAbstract {
substr($issue->find('.gh-header-number', 0)->plaintext, 1) substr($issue->find('.gh-header-number', 0)->plaintext, 1)
); );
$comments = $issue->find(' $comments = $issue->find(
[id^="issue-"] > .comment, '.comment, .TimelineItem-badge'
[id^="issuecomment-"] > .comment, );
[id^="event-"],
[id^="ref-"]
');
foreach($comments as $comment) { foreach($comments as $comment) {
if ($comment->hasClass('comment')) {
if (!$comment->hasChildNodes()) { $comment = $comment->parent;
continue;
}
if (!$comment->hasClass('discussion-item-header')) {
$item = $this->extractIssueComment($issueNbr, $title, $comment); $item = $this->extractIssueComment($issueNbr, $title, $comment);
$items[] = $item; $items[] = $item;
continue; continue;
} } else {
$comment = $comment->parent;
while ($comment->hasClass('discussion-item-header')) {
$item = $this->extractIssueEvent($issueNbr, $title, $comment); $item = $this->extractIssueEvent($issueNbr, $title, $comment);
$items[] = $item; $items[] = $item;
$comment = $comment->nextSibling();
if (null == $comment) {
break;
}
$classes = explode(' ', $comment->getAttribute('class'));
} }
} }

View File

@@ -27,16 +27,16 @@ class GithubSearchBridge extends BridgeAbstract {
foreach($html->find('li.repo-list-item') as $element) { foreach($html->find('li.repo-list-item') as $element) {
$item = array(); $item = array();
$uri = $element->find('h3 a', 0)->href; $uri = $element->find('.f4 a', 0)->href;
$uri = substr(self::URI, 0, -1) . $uri; $uri = substr(self::URI, 0, -1) . $uri;
$item['uri'] = $uri; $item['uri'] = $uri;
$title = $element->find('h3', 0)->plaintext; $title = $element->find('.f4', 0)->plaintext;
$item['title'] = $title; $item['title'] = $title;
// Description // Description
if (count($element->find('p.d-inline-block')) != 0) { if (count($element->find('p.mb-1')) != 0) {
$content = $element->find('p.d-inline-block', 0)->innertext; $content = $element->find('p.mb-1', 0)->innertext;
} else{ } else{
$content = 'No description'; $content = 'No description';
} }

View File

@@ -0,0 +1,636 @@
<?php
class GithubTrendingBridge extends BridgeAbstract {
const MAINTAINER = 'liamka';
const NAME = 'Github Trending';
const URI = 'https://github.com/trending';
const URI_ITEM = 'https://github.com';
const CACHE_TIMEOUT = 43200; // 12hr
const DESCRIPTION = 'See what the GitHub community is most excited repos.';
const PARAMETERS = array(
'By language' => array(
'language' => array(
'name' => 'Select language',
'type' => 'list',
'values' => array(
'All languages' => '',
'C++' => 'c++',
'HTML' => 'html',
'Java' => 'java',
'JavaScript' => 'javascript',
'PHP' => 'php',
'Python' => 'python',
'Ruby' => 'ruby',
'Unknown languages' => 'unknown languages',
'1C Enterprise' => '1c enterprise',
'4D' => '4d',
'ABAP' => 'abap',
'ABNF' => 'abnf',
'ActionScript' => 'actionscript',
'Ada' => 'ada',
'Adobe Font Metrics' => 'adobe font metrics',
'Agda' => 'agda',
'AGS Script' => 'ags script',
'Alloy' => 'alloy',
'Alpine Abuild' => 'alpine abuild',
'Altium Designer' => 'altium designer',
'AMPL' => 'ampl',
'AngelScript' => 'angelscript',
'Ant Build System' => 'ant build system',
'ANTLR' => 'antlr',
'ApacheConf' => 'apacheconf',
'Apex' => 'apex',
'API Blueprint' => 'api blueprint',
'APL' => 'apl',
'Apollo Guidance Computer' => 'apollo guidance computer',
'AppleScript' => 'applescript',
'Arc' => 'arc',
'AsciiDoc' => 'asciidoc',
'ASN.1' => 'asn.1',
'ASP' => 'asp',
'AspectJ' => 'aspectj',
'Assembly' => 'assembly',
'Asymptote' => 'asymptote',
'ATS' => 'ats',
'Augeas' => 'augeas',
'AutoHotkey' => 'autohotkey',
'AutoIt' => 'autoit',
'Awk' => 'awk',
'Ballerina' => 'ballerina',
'Batchfile' => 'batchfile',
'Befunge' => 'befunge',
'BibTeX' => 'bibtex',
'Bison' => 'bison',
'BitBake' => 'bitbake',
'Blade' => 'blade',
'BlitzBasic' => 'blitzbasic',
'BlitzMax' => 'blitzmax',
'Bluespec' => 'bluespec',
'Boo' => 'boo',
'Brainfuck' => 'brainfuck',
'Brightscript' => 'brightscript',
'Zeek' => 'zeek',
'C' => 'c',
'C#' => 'c#',
'C++' => 'c++',
'C-ObjDump' => 'c-objdump',
'C2hs Haskell' => 'c2hs haskell',
'Cabal Config' => 'cabal config',
'CartoCSS' => 'cartocss',
'Ceylon' => 'ceylon',
'Chapel' => 'chapel',
'Charity' => 'charity',
'ChucK' => 'chuck',
'Cirru' => 'cirru',
'Clarion' => 'clarion',
'Clean' => 'clean',
'Click' => 'click',
'CLIPS' => 'clips',
'Clojure' => 'clojure',
'Closure Templates' => 'closure templates',
'Cloud Firestore Security Rules' => 'cloud firestore security rules',
'CMake' => 'cmake',
'COBOL' => 'cobol',
'CodeQL' => 'codeql',
'CoffeeScript' => 'coffeescript',
'ColdFusion' => 'coldfusion',
'ColdFusion CFC' => 'coldfusion cfc',
'COLLADA' => 'collada',
'Common Lisp' => 'common lisp',
'Common Workflow Language' => 'common workflow language',
'Component Pascal' => 'component pascal',
'CoNLL-U' => 'conll-u',
'Cool' => 'cool',
'Coq' => 'coq',
'Cpp-ObjDump' => 'cpp-objdump',
'Creole' => 'creole',
'Crystal' => 'crystal',
'CSON' => 'cson',
'Csound' => 'csound',
'Csound Document' => 'csound document',
'Csound Score' => 'csound score',
'CSS' => 'css',
'CSV' => 'csv',
'Cuda' => 'cuda',
'cURL Config' => 'curl config',
'CWeb' => 'cweb',
'Cycript' => 'cycript',
'Cython' => 'cython',
'D' => 'd',
'D-ObjDump' => 'd-objdump',
'Darcs Patch' => 'darcs patch',
'Dart' => 'dart',
'DataWeave' => 'dataweave',
'desktop' => 'desktop',
'Dhall' => 'dhall',
'Diff' => 'diff',
'DIGITAL Command Language' => 'digital command language',
'dircolors' => 'dircolors',
'DirectX 3D File' => 'directx 3d file',
'DM' => 'dm',
'DNS Zone' => 'dns zone',
'Dockerfile' => 'dockerfile',
'Dogescript' => 'dogescript',
'DTrace' => 'dtrace',
'Dylan' => 'dylan',
'E' => 'e',
'Eagle' => 'eagle',
'Easybuild' => 'easybuild',
'EBNF' => 'ebnf',
'eC' => 'ec',
'Ecere Projects' => 'ecere projects',
'ECL' => 'ecl',
'ECLiPSe' => 'eclipse',
'EditorConfig' => 'editorconfig',
'Edje Data Collection' => 'edje data collection',
'edn' => 'edn',
'Eiffel' => 'eiffel',
'EJS' => 'ejs',
'Elixir' => 'elixir',
'Elm' => 'elm',
'Emacs Lisp' => 'emacs lisp',
'EmberScript' => 'emberscript',
'EML' => 'eml',
'EQ' => 'eq',
'Erlang' => 'erlang',
'F#' => 'f#',
'F*' => 'f*',
'Factor' => 'factor',
'Fancy' => 'fancy',
'Fantom' => 'fantom',
'Faust' => 'faust',
'FIGlet Font' => 'figlet font',
'Filebench WML' => 'filebench wml',
'Filterscript' => 'filterscript',
'fish' => 'fish',
'FLUX' => 'flux',
'Formatted' => 'formatted',
'Forth' => 'forth',
'Fortran' => 'fortran',
'FreeMarker' => 'freemarker',
'Frege' => 'frege',
'G-code' => 'g-code',
'Game Maker Language' => 'game maker language',
'GAML' => 'gaml',
'GAMS' => 'gams',
'GAP' => 'gap',
'GCC Machine Description' => 'gcc machine description',
'GDB' => 'gdb',
'GDScript' => 'gdscript',
'Genie' => 'genie',
'Genshi' => 'genshi',
'Gentoo Ebuild' => 'gentoo ebuild',
'Gentoo Eclass' => 'gentoo eclass',
'Gerber Image' => 'gerber image',
'Gettext Catalog' => 'gettext catalog',
'Gherkin' => 'gherkin',
'Git Attributes' => 'git attributes',
'Git Config' => 'git config',
'GLSL' => 'glsl',
'Glyph' => 'glyph',
'Glyph Bitmap Distribution Format' => 'glyph bitmap distribution format',
'GN' => 'gn',
'Gnuplot' => 'gnuplot',
'Go' => 'go',
'Golo' => 'golo',
'Gosu' => 'gosu',
'Grace' => 'grace',
'Gradle' => 'gradle',
'Grammatical Framework' => 'grammatical framework',
'Graph Modeling Language' => 'graph modeling language',
'GraphQL' => 'graphql',
'Graphviz (DOT)' => 'graphviz (dot)',
'Groovy' => 'groovy',
'Groovy Server Pages' => 'groovy server pages',
'Hack' => 'hack',
'Haml' => 'haml',
'Handlebars' => 'handlebars',
'HAProxy' => 'haproxy',
'Harbour' => 'harbour',
'Haskell' => 'haskell',
'Haxe' => 'haxe',
'HCL' => 'hcl',
'HiveQL' => 'hiveql',
'HLSL' => 'hlsl',
'HolyC' => 'holyc',
'HTML' => 'html',
'HTML+Django' => 'html+django',
'HTML+ECR' => 'html+ecr',
'HTML+EEX' => 'html+eex',
'HTML+ERB' => 'html+erb',
'HTML+PHP' => 'html+php',
'HTML+Razor' => 'html+razor',
'HTTP' => 'http',
'HXML' => 'hxml',
'Hy' => 'hy',
'HyPhy' => 'hyphy',
'IDL' => 'idl',
'Idris' => 'idris',
'Ignore List' => 'ignore list',
'IGOR Pro' => 'igor pro',
'Inform 7' => 'inform 7',
'INI' => 'ini',
'Inno Setup' => 'inno setup',
'Io' => 'io',
'Ioke' => 'ioke',
'IRC log' => 'irc log',
'Isabelle' => 'isabelle',
'Isabelle ROOT' => 'isabelle root',
'J' => 'j',
'Jasmin' => 'jasmin',
'Java' => 'java',
'Java Properties' => 'java properties',
'Java Server Pages' => 'java server pages',
'JavaScript' => 'javascript',
'JavaScript+ERB' => 'javascript+erb',
'JFlex' => 'jflex',
'Jison' => 'jison',
'Jison Lex' => 'jison lex',
'Jolie' => 'jolie',
'JSON' => 'json',
'JSON with Comments' => 'json with comments',
'JSON5' => 'json5',
'JSONiq' => 'jsoniq',
'JSONLD' => 'jsonld',
'Jsonnet' => 'jsonnet',
'JSX' => 'jsx',
'Julia' => 'julia',
'Jupyter Notebook' => 'jupyter notebook',
'KiCad Layout' => 'kicad layout',
'KiCad Legacy Layout' => 'kicad legacy layout',
'KiCad Schematic' => 'kicad schematic',
'Kit' => 'kit',
'Kotlin' => 'kotlin',
'KRL' => 'krl',
'LabVIEW' => 'labview',
'Lasso' => 'lasso',
'Latte' => 'latte',
'Lean' => 'lean',
'Less' => 'less',
'Lex' => 'lex',
'LFE' => 'lfe',
'LilyPond' => 'lilypond',
'Limbo' => 'limbo',
'Linker Script' => 'linker script',
'Linux Kernel Module' => 'linux kernel module',
'Liquid' => 'liquid',
'Literate Agda' => 'literate agda',
'Literate CoffeeScript' => 'literate coffeescript',
'Literate Haskell' => 'literate haskell',
'LiveScript' => 'livescript',
'LLVM' => 'llvm',
'Logos' => 'logos',
'Logtalk' => 'logtalk',
'LOLCODE' => 'lolcode',
'LookML' => 'lookml',
'LoomScript' => 'loomscript',
'LSL' => 'lsl',
'LTspice Symbol' => 'ltspice symbol',
'Lua' => 'lua',
'M' => 'm',
'M4' => 'm4',
'M4Sugar' => 'm4sugar',
'Makefile' => 'makefile',
'Mako' => 'mako',
'Markdown' => 'markdown',
'Marko' => 'marko',
'Mask' => 'mask',
'Mathematica' => 'mathematica',
'MATLAB' => 'matlab',
'Maven POM' => 'maven pom',
'Max' => 'max',
'MAXScript' => 'maxscript',
'mcfunction' => 'mcfunction',
'MediaWiki' => 'mediawiki',
'Mercury' => 'mercury',
'Meson' => 'meson',
'Metal' => 'metal',
'Microsoft Developer Studio Project' => 'microsoft developer studio project',
'MiniD' => 'minid',
'Mirah' => 'mirah',
'mIRC Script' => 'mirc script',
'MLIR' => 'mlir',
'Modelica' => 'modelica',
'Modula-2' => 'modula-2',
'Modula-3' => 'modula-3',
'Module Management System' => 'module management system',
'Monkey' => 'monkey',
'Moocode' => 'moocode',
'MoonScript' => 'moonscript',
'Motorola 68K Assembly' => 'motorola 68k assembly',
'MQL4' => 'mql4',
'MQL5' => 'mql5',
'MTML' => 'mtml',
'MUF' => 'muf',
'mupad' => 'mupad',
'Muse' => 'muse',
'Myghty' => 'myghty',
'nanorc' => 'nanorc',
'NASL' => 'nasl',
'NCL' => 'ncl',
'Nearley' => 'nearley',
'Nemerle' => 'nemerle',
'nesC' => 'nesc',
'NetLinx' => 'netlinx',
'NetLinx+ERB' => 'netlinx+erb',
'NetLogo' => 'netlogo',
'NewLisp' => 'newlisp',
'Nextflow' => 'nextflow',
'Nginx' => 'nginx',
'Nim' => 'nim',
'Ninja' => 'ninja',
'Nit' => 'nit',
'Nix' => 'nix',
'NL' => 'nl',
'NPM Config' => 'npm config',
'NSIS' => 'nsis',
'Nu' => 'nu',
'NumPy' => 'numpy',
'ObjDump' => 'objdump',
'Object Data Instance Notation' => 'object data instance notation',
'Objective-C' => 'objective-c',
'Objective-C++' => 'objective-c++',
'Objective-J' => 'objective-j',
'ObjectScript' => 'objectscript',
'OCaml' => 'ocaml',
'Odin' => 'odin',
'Omgrofl' => 'omgrofl',
'ooc' => 'ooc',
'Opa' => 'opa',
'Opal' => 'opal',
'Open Policy Agent' => 'open policy agent',
'OpenCL' => 'opencl',
'OpenEdge ABL' => 'openedge abl',
'OpenQASM' => 'openqasm',
'OpenRC runscript' => 'openrc runscript',
'OpenSCAD' => 'openscad',
'OpenStep Property List' => 'openstep property list',
'OpenType Feature File' => 'opentype feature file',
'Org' => 'org',
'Ox' => 'ox',
'Oxygene' => 'oxygene',
'Oz' => 'oz',
'P4' => 'p4',
'Pan' => 'pan',
'Papyrus' => 'papyrus',
'Parrot' => 'parrot',
'Parrot Assembly' => 'parrot assembly',
'Parrot Internal Representation' => 'parrot internal representation',
'Pascal' => 'pascal',
'Pawn' => 'pawn',
'Pep8' => 'pep8',
'Perl' => 'perl',
'PHP' => 'php',
'Pic' => 'pic',
'Pickle' => 'pickle',
'PicoLisp' => 'picolisp',
'PigLatin' => 'piglatin',
'Pike' => 'pike',
'PLpgSQL' => 'plpgsql',
'PLSQL' => 'plsql',
'Pod' => 'pod',
'Pod 6' => 'pod 6',
'PogoScript' => 'pogoscript',
'Pony' => 'pony',
'PostCSS' => 'postcss',
'PostScript' => 'postscript',
'POV-Ray SDL' => 'pov-ray sdl',
'PowerBuilder' => 'powerbuilder',
'PowerShell' => 'powershell',
'Prisma' => 'prisma',
'Processing' => 'processing',
'Proguard' => 'proguard',
'Prolog' => 'prolog',
'Propeller Spin' => 'propeller spin',
'Protocol Buffer' => 'protocol buffer',
'Public Key' => 'public key',
'Pug' => 'pug',
'Puppet' => 'puppet',
'Pure Data' => 'pure data',
'PureBasic' => 'purebasic',
'PureScript' => 'purescript',
'Python' => 'python',
'Python console' => 'python console',
'Python traceback' => 'python traceback',
'q' => 'q',
'QMake' => 'qmake',
'QML' => 'qml',
'Quake' => 'quake',
'R' => 'r',
'Racket' => 'racket',
'Ragel' => 'ragel',
'Raku' => 'raku',
'RAML' => 'raml',
'Rascal' => 'rascal',
'Raw token data' => 'raw token data',
'RDoc' => 'rdoc',
'Readline Config' => 'readline config',
'REALbasic' => 'realbasic',
'Reason' => 'reason',
'Rebol' => 'rebol',
'Red' => 'red',
'Redcode' => 'redcode',
'Regular Expression' => 'regular expression',
// 'Ren'Py' => 'ren'py',
'RenderScript' => 'renderscript',
'reStructuredText' => 'restructuredtext',
'REXX' => 'rexx',
'RHTML' => 'rhtml',
'Rich Text Format' => 'rich text format',
'Ring' => 'ring',
'Riot' => 'riot',
'RMarkdown' => 'rmarkdown',
'RobotFramework' => 'robotframework',
'Roff' => 'roff',
'Roff Manpage' => 'roff manpage',
'Rouge' => 'rouge',
'RPC' => 'rpc',
'RPM Spec' => 'rpm spec',
'Ruby' => 'ruby',
'RUNOFF' => 'runoff',
'Rust' => 'rust',
'Sage' => 'sage',
'SaltStack' => 'saltstack',
'SAS' => 'sas',
'Sass' => 'sass',
'Scala' => 'scala',
'Scaml' => 'scaml',
'Scheme' => 'scheme',
'Scilab' => 'scilab',
'SCSS' => 'scss',
'sed' => 'sed',
'Self' => 'self',
'ShaderLab' => 'shaderlab',
'Shell' => 'shell',
'ShellSession' => 'shellsession',
'Shen' => 'shen',
'Slash' => 'slash',
'Slice' => 'slice',
'Slim' => 'slim',
'Smali' => 'smali',
'Smalltalk' => 'smalltalk',
'Smarty' => 'smarty',
'SmPL' => 'smpl',
'SMT' => 'smt',
'Solidity' => 'solidity',
'SourcePawn' => 'sourcepawn',
'SPARQL' => 'sparql',
'Spline Font Database' => 'spline font database',
'SQF' => 'sqf',
'SQL' => 'sql',
'SQLPL' => 'sqlpl',
'Squirrel' => 'squirrel',
'SRecode Template' => 'srecode template',
'SSH Config' => 'ssh config',
'Stan' => 'stan',
'Standard ML' => 'standard ml',
'Starlark' => 'starlark',
'Stata' => 'stata',
'STON' => 'ston',
'Stylus' => 'stylus',
'SubRip Text' => 'subrip text',
'SugarSS' => 'sugarss',
'SuperCollider' => 'supercollider',
'Svelte' => 'svelte',
'SVG' => 'svg',
'Swift' => 'swift',
'SWIG' => 'swig',
'SystemVerilog' => 'systemverilog',
'Tcl' => 'tcl',
'Tcsh' => 'tcsh',
'Tea' => 'tea',
'Terra' => 'terra',
'TeX' => 'tex',
'Texinfo' => 'texinfo',
'Text' => 'text',
'Textile' => 'textile',
'Thrift' => 'thrift',
'TI Program' => 'ti program',
'TLA' => 'tla',
'TOML' => 'toml',
'TSQL' => 'tsql',
'TSX' => 'tsx',
'Turing' => 'turing',
'Turtle' => 'turtle',
'Twig' => 'twig',
'TXL' => 'txl',
'Type Language' => 'type language',
'TypeScript' => 'typescript',
'Unified Parallel C' => 'unified parallel c',
'Unity3D Asset' => 'unity3d asset',
'Unix Assembly' => 'unix assembly',
'Uno' => 'uno',
'UnrealScript' => 'unrealscript',
'UrWeb' => 'urweb',
'V' => 'v',
'Vala' => 'vala',
'VBA' => 'vba',
'VBScript' => 'vbscript',
'VCL' => 'vcl',
'Verilog' => 'verilog',
'VHDL' => 'vhdl',
'Vim script' => 'vim script',
'Vim Snippet' => 'vim snippet',
'Visual Basic .NET' => 'visual basic .net',
'Visual Basic .NET' => 'visual basic .net',
'Volt' => 'volt',
'Vue' => 'vue',
'Wavefront Material' => 'wavefront material',
'Wavefront Object' => 'wavefront object',
'wdl' => 'wdl',
'Web Ontology Language' => 'web ontology language',
'WebAssembly' => 'webassembly',
'WebIDL' => 'webidl',
'WebVTT' => 'webvtt',
'Wget Config' => 'wget config',
'Windows Registry Entries' => 'windows registry entries',
'wisp' => 'wisp',
'Wollok' => 'wollok',
'World of Warcraft Addon Data' => 'world of warcraft addon data',
'X BitMap' => 'x bitmap',
'X Font Directory Index' => 'x font directory index',
'X PixMap' => 'x pixmap',
'X10' => 'x10',
'xBase' => 'xbase',
'XC' => 'xc',
'XCompose' => 'xcompose',
'XML' => 'xml',
'XML Property List' => 'xml property list',
'Xojo' => 'xojo',
'XPages' => 'xpages',
'XProc' => 'xproc',
'XQuery' => 'xquery',
'XS' => 'xs',
'XSLT' => 'xslt',
'Xtend' => 'xtend',
'Yacc' => 'yacc',
'YAML' => 'yaml',
'YANG' => 'yang',
'YARA' => 'yara',
'YASnippet' => 'yasnippet',
'ZAP' => 'zap',
'Zeek' => 'zeek',
'ZenScript' => 'zenscript',
'Zephir' => 'zephir',
'Zig' => 'zig',
'ZIL' => 'zil',
'Zimpl' => 'zimpl',
),
'defaultValue' => 'All languages'
)
),
'global' => array(
'date_range' => array(
'name' => 'Date range',
'type' => 'list',
'required' => false,
'values' => array(
'Today' => 'today',
'Weekly' => 'weekly',
'Monthly' => 'monthly',
),
'defaultValue' => 'today'
)
)
);
public function collectData(){
$params = array('since' => urlencode($this->getInput('date_range')));
$url = self::URI . '/' . $this->getInput('language') . '?' . http_build_query($params);
$html = getSimpleHTMLDOM($url)
or returnServerError('Error while downloading the website content');
$this->items = array();
foreach($html->find('.Box-row') as $element) {
$item = array();
// URI
$item['uri'] = self::URI_ITEM . $element->find('h1 a', 0)->href;
// Title
$item['title'] = str_replace(' ', '', trim(strip_tags($element->find('h1 a', 0)->plaintext)));
// Description
$item['content'] = trim(strip_tags($element->find('p.text-gray', 0)->innertext));
// Time
$item['timestamp'] = time();
// TODO: Proxy?
$this->items[] = $item;
}
}
public function getName(){
if($this->getInput('language') == '') {
return self::NAME . ': all';
} elseif (!is_null($this->getInput('language'))) {
return self::NAME . ': ' . $this->getInput('language');
}
return parent::getName();
}
}

View File

@@ -3,34 +3,78 @@ class GizmodoBridge extends FeedExpander {
const MAINTAINER = 'polopollo'; const MAINTAINER = 'polopollo';
const NAME = 'Gizmodo'; const NAME = 'Gizmodo';
const URI = 'http://gizmodo.com/'; const URI = 'https://gizmodo.com';
const CACHE_TIMEOUT = 1800; // 30min const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns the newest posts from Gizmodo (full text).'; const DESCRIPTION = 'Returns the newest posts from Gizmodo.';
protected function parseItem($item){ protected function parseItem($item) {
$item = parent::parseItem($item); $item = parent::parseItem($item);
$articleHTMLContent = getSimpleHTMLDOMCached($item['uri']); $html = getSimpleHTMLDOMCached($item['uri'])
if(!$articleHTMLContent) { or returnServerError('Could not request: ' . $item['uri']);
$text = 'Could not load ' . $item['uri'];
} else {
$text = $articleHTMLContent->find('div.entry-content', 0)->innertext;
foreach($articleHTMLContent->find('pagespeed_iframe') as $element) {
$text .= '<p>link to a iframe (could be a video): <a href="'
. $element->src
. '">'
. $element->src
. '</a></p><br>';
}
$text = strip_tags($text, '<p><b><a><blockquote><img><em>'); $html = defaultLinkTo($html, $this->getURI());
} $this->stripTags($html);
$this->handleFigureTags($html);
$this->handleIframeTags($html);
// Get header image
$image = $html->find('meta[property="og:image"]', 0)->content;
$item['content'] = $html->find('div.js_post-content', 0)->innertext;
// Get categories
$categories = explode(',', $html->find('meta[name="keywords"]', 0)->content);
$item['categories'] = array_map('trim', $categories);
$item['enclosures'][] = $html->find('meta[property="og:image"]', 0)->content;
$item['content'] = $text;
return $item; return $item;
} }
public function collectData(){ public function collectData() {
$this->collectExpandableDatas('http://feeds.gawker.com/gizmodo/full'); $this->collectExpandableDatas(self::URI . '/rss', 20);
}
private function stripTags($html) {
foreach ($html->find('aside') as $aside) {
$aside->outertext = '';
}
foreach ($html->find('div.ad-unit') as $div) {
$div->outertext = '';
}
foreach ($html->find('script') as $script) {
$script->outertext = '';
}
}
private function handleFigureTags($html) {
foreach ($html->find('figure') as $index => $figure) {
if (isset($figure->attr['data-id'])) {
$id = $figure->attr['data-id'];
$format = $figure->attr['data-format'];
} else {
$img = $figure->find('img', 0);
$id = $img->attr['data-chomp-id'];
$format = $img->attr['data-format'];
$figure->find('div.img-permalink-sub-wrapper', 0)->style = '';
}
$imageUrl = 'https://i.kinja-img.com/gawker-media/image/upload/' . $id . '.' . $format;
$figure->find('span', 0)->outertext = <<<EOD
<img src="{$imageUrl}">
EOD;
}
}
private function handleIframeTags($html) {
foreach($html->find('iframe') as $iframe) {
$iframe->src = urljoin($this->getURI(), $iframe->src);
}
} }
} }

View File

@@ -28,7 +28,7 @@ class GoComicsBridge extends BridgeAbstract {
$page = getSimpleHTMLDOM($link) $page = getSimpleHTMLDOM($link)
or returnServerError('Could not request GoComics: ' . $link); or returnServerError('Could not request GoComics: ' . $link);
$imagelink = $page->find('.img-fluid', 1)->src; $imagelink = $page->find('.comic.container', 0)->getAttribute('data-image');
$date = explode('/', $link); $date = explode('/', $link);
$item['id'] = $imagelink; $item['id'] = $imagelink;

View File

@@ -25,35 +25,37 @@ class GoogleSearchBridge extends BridgeAbstract {
public function collectData(){ public function collectData(){
$html = ''; $html = '';
$html = getSimpleHTMLDOM(self::URI $html = getSimpleHTMLDOM($this->getURI())
. 'search?q='
. urlencode($this->getInput('q'))
. '&num=100&complete=0&tbs=qdr:y,sbd:1')
or returnServerError('No results for this query.'); or returnServerError('No results for this query.');
$emIsRes = $html->find('div[id=ires]', 0); $emIsRes = $html->find('div[id=res]', 0);
if(!is_null($emIsRes)) { if(!is_null($emIsRes)) {
foreach($emIsRes->find('div[class=g]') as $element) { foreach($emIsRes->find('div[class=g]') as $element) {
$item = array(); $item = array();
// Extract direct URL from google href (eg. /url?q=...)
$t = $element->find('a[href]', 0)->href; $t = $element->find('a[href]', 0)->href;
$item['uri'] = '' . $t; $item['uri'] = htmlspecialchars_decode($t);
parse_str(parse_url($t, PHP_URL_QUERY), $parameters);
if(isset($parameters['q'])) {
$item['uri'] = $parameters['q'];
}
$item['title'] = $element->find('h3', 0)->plaintext; $item['title'] = $element->find('h3', 0)->plaintext;
$item['content'] = $element->find('span[class=st]', 0)->plaintext; $item['content'] = $element->find('span[class=aCOpRe]', 0)->plaintext;
$this->items[] = $item; $this->items[] = $item;
} }
} }
} }
public function getURI() {
if (!is_null($this->getInput('q'))) {
return self::URI
. 'search?q='
. urlencode($this->getInput('q'))
. '&num=100&complete=0&tbs=qdr:y,sbd:1';
}
return parent::getURI();
}
public function getName(){ public function getName(){
if(!is_null($this->getInput('q'))) { if(!is_null($this->getInput('q'))) {
return $this->getInput('q') . ' - Google search'; return $this->getInput('q') . ' - Google search';

View File

@@ -2,7 +2,7 @@
class HDWallpapersBridge extends BridgeAbstract { class HDWallpapersBridge extends BridgeAbstract {
const MAINTAINER = 'nel50n'; const MAINTAINER = 'nel50n';
const NAME = 'HD Wallpapers Bridge'; const NAME = 'HD Wallpapers Bridge';
const URI = 'http://www.hdwallpapers.in/'; const URI = 'https://www.hdwallpapers.in/';
const CACHE_TIMEOUT = 43200; //12h const CACHE_TIMEOUT = 43200; //12h
const DESCRIPTION = 'Returns the latests wallpapers from HDWallpapers'; const DESCRIPTION = 'Returns the latests wallpapers from HDWallpapers';
@@ -72,7 +72,7 @@ class HDWallpapersBridge extends BridgeAbstract {
public function getName(){ public function getName(){
if(!is_null($this->getInput('c')) && !is_null($this->getInput('r'))) { if(!is_null($this->getInput('c')) && !is_null($this->getInput('r'))) {
return 'HDWallpapers - ' return 'HDWallpapers - '
. str_replace(['__', '_'], [' & ', ' '], $this->getInput('c')) . str_replace(array('__', '_'), array(' & ', ' '), $this->getInput('c'))
. ' [' . ' ['
. $this->getInput('r') . $this->getInput('r')
. ']'; . ']';

View File

@@ -40,18 +40,15 @@ class HeiseBridge extends FeedExpander {
protected function parseItem($feedItem) { protected function parseItem($feedItem) {
$item = parent::parseItem($feedItem); $item = parent::parseItem($feedItem);
$uri = $item['uri']; $uri = $item['uri'] . '&seite=all';
do { $article = getSimpleHTMLDOMCached($uri)
$article = getSimpleHTMLDOMCached($uri) or returnServerError('Could not open article: ' . $uri);
or returnServerError('Could not open article: ' . $uri);
if ($article) {
$article = defaultLinkTo($article, $uri); $article = defaultLinkTo($article, $uri);
$item = $this->addArticleToItem($item, $article); $item = $this->addArticleToItem($item, $article);
}
if($next = $article->find('.pagination a[rel="next"]', 0))
$uri = $next->href;
} while ($next);
return $item; return $item;
} }
@@ -62,6 +59,9 @@ class HeiseBridge extends FeedExpander {
$content = $article->find('div[class*="article-content"]', 0); $content = $article->find('div[class*="article-content"]', 0);
if ($content == null)
$content = $article->find('#article_content', 0);
foreach($content->find('p, h3, ul, table, pre, img') as $element) { foreach($content->find('p, h3, ul, table, pre, img') as $element) {
$item['content'] .= $element; $item['content'] .= $element;
} }

View File

@@ -19,6 +19,27 @@ class IGNBridge extends FeedExpander {
// $articlePage gets the entire page's contents // $articlePage gets the entire page's contents
$articlePage = getSimpleHTMLDOM($newsItem->link); $articlePage = getSimpleHTMLDOM($newsItem->link);
// List of BS elements
$uselessElements = array(
'.wiki-page-tools',
'.feedback-container',
'.paging-container',
'.dropdown-wrapper',
'.mw-editsection',
'.jsx-4115608983',
'.jsx-4213937408',
'.commerce-container',
'.widget-container',
'.newsletter-signup-button'
);
// Remove useless elements
foreach($uselessElements as $uslElement) {
foreach($articlePage->find($uslElement) as $jsWidget) {
$jsWidget->remove();
}
}
/* /*
* NOTE: Though articles and wiki/howtos have seperate styles of pages, there is no mechanism * 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. * for handling them seperately as it just ignores the DOM querys which it does not find.
@@ -33,19 +54,8 @@ class IGNBridge extends FeedExpander {
} }
// For Wikis and HowTos // For Wikis and HowTos
$uselessWikiElements = array(
'.wiki-page-tools',
'.feedback-container',
'.paging-container'
);
foreach($articlePage->find('.wiki-page') as $wikiContents) { foreach($articlePage->find('.wiki-page') as $wikiContents) {
$copy = clone $wikiContents; $article = $article . $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 // Add content to feed

View File

@@ -32,19 +32,23 @@ class InstagramBridge extends BridgeAbstract {
'required' => false, 'required' => false,
'values' => array( 'values' => array(
'All' => 'all', 'All' => 'all',
'Story' => 'story',
'Video' => 'video', 'Video' => 'video',
'Picture' => 'picture', 'Picture' => 'picture',
'Multiple' => 'multiple',
), ),
'defaultValue' => 'all' 'defaultValue' => 'all'
),
'direct_links' => array(
'name' => 'Use direct media links',
'type' => 'checkbox',
) )
) )
); );
const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951'; const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951';
const TAG_QUERY_HASH = '174a5243287c5f3a7de741089750ab3b'; const TAG_QUERY_HASH = '9b498c08113f1e09617a1703c22b2f32';
const STORY_QUERY_HASH = '865589822932d1b43dfe312121dd353a'; const SHORTCODE_QUERY_HASH = '865589822932d1b43dfe312121dd353a';
protected function getInstagramUserId($username) { protected function getInstagramUserId($username) {
@@ -54,14 +58,14 @@ class InstagramBridge extends BridgeAbstract {
$cacheFac->setWorkingDir(PATH_LIB_CACHES); $cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope(get_called_class()); $cache->setScope(get_called_class());
$cache->setKey([$username]); $cache->setKey(array($username));
$key = $cache->loadData(); $key = $cache->loadData();
if($key == null) { if($key == null) {
$data = getContents(self::URI . 'web/search/topsearch/?query=' . $username); $data = getContents(self::URI . 'web/search/topsearch/?query=' . $username);
foreach(json_decode($data)->users as $user) { foreach(json_decode($data)->users as $user) {
if($user->user->username === $username) { if(strtolower($user->user->username) === strtolower($username)) {
$key = $user->user->pk; $key = $user->user->pk;
} }
} }
@@ -75,10 +79,7 @@ class InstagramBridge extends BridgeAbstract {
} }
public function collectData(){ public function collectData(){
$directLink = !is_null($this->getInput('direct_links')) && $this->getInput('direct_links');
if(is_null($this->getInput('u')) && $this->getInput('media_type') == 'story') {
returnClientError('Stories are not supported for hashtags nor locations!');
}
$data = $this->getInstagramJSON($this->getURI()); $data = $this->getInstagramJSON($this->getURI());
@@ -93,22 +94,18 @@ class InstagramBridge extends BridgeAbstract {
foreach($userMedia as $media) { foreach($userMedia as $media) {
$media = $media->node; $media = $media->node;
if(!is_null($this->getInput('u'))) { switch($this->getInput('media_type')) {
switch($this->getInput('media_type')) { case 'all': break;
case 'all': break; case 'video':
case 'video': if($media->__typename != 'GraphVideo' || !$media->is_video) continue 2;
if($media->__typename != 'GraphVideo') continue 2; break;
break; case 'picture':
case 'picture': if($media->__typename != 'GraphImage') continue 2;
if($media->__typename != 'GraphImage') continue 2; break;
break; case 'multiple':
case 'story': if($media->__typename != 'GraphSidecar') continue 2;
if($media->__typename != 'GraphSidecar') continue 2; break;
break; default: break;
default: break;
}
} else {
if($this->getInput('media_type') == 'video' && !$media->is_video) continue;
} }
$item = array(); $item = array();
@@ -118,69 +115,110 @@ class InstagramBridge extends BridgeAbstract {
$item['author'] = $media->owner->username; $item['author'] = $media->owner->username;
} }
if (isset($media->edge_media_to_caption->edges[0]->node->text)) { $textContent = $this->getTextContent($media);
$textContent = $media->edge_media_to_caption->edges[0]->node->text;
} else {
$textContent = '(no text)';
}
$item['title'] = ($media->is_video ? '▶ ' : '') . trim($textContent); $item['title'] = ($media->is_video ? '▶ ' : '') . $textContent;
$titleLinePos = strpos(wordwrap($item['title'], 120), "\n"); $titleLinePos = strpos(wordwrap($item['title'], 120), "\n");
if ($titleLinePos != false) { if ($titleLinePos != false) {
$item['title'] = substr($item['title'], 0, $titleLinePos) . '...'; $item['title'] = substr($item['title'], 0, $titleLinePos) . '...';
} }
if(!is_null($this->getInput('u')) && $media->__typename == 'GraphSidecar') { if($directLink) {
$mediaURI = $media->display_url;
$data = $this->getInstagramStory($item['uri']);
$item['content'] = $data[0];
$item['enclosures'] = $data[1];
} else { } else {
$mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l'; $mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l';
$item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
$item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />';
$item['content'] .= '</a><br><br>' . nl2br(htmlentities($textContent));
$item['enclosures'] = array($mediaURI);
} }
switch($media->__typename) {
case 'GraphSidecar':
$data = $this->getInstagramSidecarData($item['uri'], $item['title']);
$item['content'] = $data[0];
$item['enclosures'] = $data[1];
break;
case 'GraphImage':
$item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
$item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />';
$item['content'] .= '</a><br><br>' . nl2br(htmlentities($textContent));
$item['enclosures'] = array($mediaURI);
break;
case 'GraphVideo':
$data = $this->getInstagramVideoData($item['uri'], $mediaURI);
$item['content'] = $data[0];
if($directLink) {
$item['enclosures'] = $data[1];
} else {
$item['enclosures'] = array($mediaURI);
}
$item['thumbnail'] = $mediaURI;
break;
default: break;
}
$item['timestamp'] = $media->taken_at_timestamp; $item['timestamp'] = $media->taken_at_timestamp;
$this->items[] = $item; $this->items[] = $item;
} }
} }
protected function getInstagramStory($uri) { // returns Sidecar(a post which has multiple media)'s contents and enclosures
protected function getInstagramSidecarData($uri, $postTitle) {
$mediaInfo = $this->getSinglePostData($uri);
$textContent = $this->getTextContent($mediaInfo);
$enclosures = array();
$content = '';
foreach($mediaInfo->edge_sidecar_to_children->edges as $singleMedia) {
$singleMedia = $singleMedia->node;
if($singleMedia->is_video) {
if(in_array($singleMedia->video_url, $enclosures)) continue; // check if not added yet
$content .= '<video controls><source src="' . $singleMedia->video_url . '" type="video/mp4"></video><br>';
array_push($enclosures, $singleMedia->video_url);
} else {
if(in_array($singleMedia->display_url, $enclosures)) continue; // check if not added yet
$content .= '<a href="' . $singleMedia->display_url . '" target="_blank">';
$content .= '<img src="' . $singleMedia->display_url . '" alt="' . $postTitle . '" />';
$content .= '</a><br>';
array_push($enclosures, $singleMedia->display_url);
}
}
$content .= '<br>' . nl2br(htmlentities($textContent));
return array($content, $enclosures);
}
// returns Video post's contents and enclosures
protected function getInstagramVideoData($uri, $mediaURI) {
$mediaInfo = $this->getSinglePostData($uri);
$textContent = $this->getTextContent($mediaInfo);
$content = '<video controls>';
$content .= '<source src="' . $mediaInfo->video_url . '" poster="' . $mediaURI . '" type="video/mp4">';
$content .= '<img src="' . $mediaURI . '" alt="">';
$content .= '</video><br>';
$content .= '<br>' . nl2br(htmlentities($textContent));
return array($content, array($mediaInfo->video_url));
}
protected function getTextContent($media) {
$textContent = '(no text)';
//Process the first element, that isn't in the node graph
if (count($media->edge_media_to_caption->edges) > 0) {
$textContent = trim($media->edge_media_to_caption->edges[0]->node->text);
}
return $textContent;
}
protected function getSinglePostData($uri) {
$shortcode = explode('/', $uri)[4]; $shortcode = explode('/', $uri)[4];
$data = getContents(self::URI . $data = getContents(self::URI .
'graphql/query/?query_hash=' . 'graphql/query/?query_hash=' .
self::STORY_QUERY_HASH . self::SHORTCODE_QUERY_HASH .
'&variables={"shortcode"%3A"' . '&variables={"shortcode"%3A"' .
$shortcode . $shortcode .
'"}'); '"}');
$mediaInfo = json_decode($data)->data->shortcode_media; return 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) {
$caption = $mediaInfo->edge_media_to_caption->edges[0]->node->text;
} else {
$caption = '';
}
$enclosures = [$mediaInfo->display_url];
$content = '<img src="' . htmlentities($mediaInfo->display_url) . '" alt="' . $caption . '" />';
foreach($mediaInfo->edge_sidecar_to_children->edges as $media) {
$display_url = $media->node->display_url;
if(!in_array($display_url, $enclosures)) { // add only if not added yet
$content .= '<img src="' . htmlentities($display_url) . '" alt="' . $caption . '" />';
$enclosures[] = $display_url;
}
}
return [$content, $enclosures];
} }
protected function getInstagramJSON($uri) { protected function getInstagramJSON($uri) {

View File

@@ -19,28 +19,6 @@ class JapanExpoBridge extends BridgeAbstract {
public function collectData(){ public function collectData(){
function frenchPubDateToTimestamp($date_to_parse) {
return strtotime(
strtr(
strtolower(str_replace('Publié le ', '', $date_to_parse)),
array(
'janvier' => 'jan',
'février' => 'feb',
'mars' => 'march',
'avril' => 'apr',
'mai' => 'may',
'juin' => 'jun',
'juillet' => 'jul',
'août' => 'aug',
'septembre' => 'sep',
'octobre' => 'oct',
'novembre' => 'nov',
'décembre' => 'dec'
)
)
);
}
$convert_article_images = function($matches){ $convert_article_images = function($matches){
if(is_array($matches) && count($matches) > 1) { if(is_array($matches) && count($matches) > 1) {
return '<img src="' . $matches[1] . '" />'; return '<img src="' . $matches[1] . '" />';
@@ -82,7 +60,7 @@ class JapanExpoBridge extends BridgeAbstract {
$content = $headings . $article; $content = $headings . $article;
} else { } else {
$date_text = $element->find('span.date', 0)->plaintext; $date_text = $element->find('span.date', 0)->plaintext;
$timestamp = frenchPubDateToTimestamp($date_text); $timestamp = $this->frenchPubDateToTimestamp($date_text);
$title = trim($element->find('span._title', 0)->plaintext); $title = trim($element->find('span._title', 0)->plaintext);
$content = '<img src="' $content = '<img src="'
. $thumbnail . $thumbnail
@@ -103,4 +81,26 @@ class JapanExpoBridge extends BridgeAbstract {
$count++; $count++;
} }
} }
private function frenchPubDateToTimestamp($date_to_parse) {
return strtotime(
strtr(
strtolower(str_replace('Publié le ', '', $date_to_parse)),
array(
'janvier' => 'jan',
'février' => 'feb',
'mars' => 'march',
'avril' => 'apr',
'mai' => 'may',
'juin' => 'jun',
'juillet' => 'jul',
'août' => 'aug',
'septembre' => 'sep',
'octobre' => 'oct',
'novembre' => 'nov',
'décembre' => 'dec'
)
)
);
}
} }

View File

@@ -347,5 +347,6 @@ class JustETFBridge extends BridgeAbstract {
return $element->plaintext; return $element->plaintext;
} }
#endregion #endregion
} }

View File

@@ -5,7 +5,7 @@ class KonachanBridge extends MoebooruBridge {
const MAINTAINER = 'mitsukarenai'; const MAINTAINER = 'mitsukarenai';
const NAME = 'Konachan'; const NAME = 'Konachan';
const URI = 'http://konachan.com/'; const URI = 'https://konachan.com/';
const DESCRIPTION = 'Returns images from given page'; const DESCRIPTION = 'Returns images from given page';
} }

View File

@@ -3,7 +3,7 @@ class KoreusBridge extends FeedExpander {
const MAINTAINER = 'pit-fgfjiudghdf'; const MAINTAINER = 'pit-fgfjiudghdf';
const NAME = 'Koreus'; const NAME = 'Koreus';
const URI = 'http://www.koreus.com/'; const URI = 'https://www.koreus.com/';
const DESCRIPTION = 'Returns the newest posts from Koreus (full text)'; const DESCRIPTION = 'Returns the newest posts from Koreus (full text)';
protected function parseItem($item){ protected function parseItem($item){
@@ -17,6 +17,6 @@ class KoreusBridge extends FeedExpander {
} }
public function collectData(){ public function collectData(){
$this->collectExpandableDatas('http://feeds.feedburner.com/Koreus-articles'); $this->collectExpandableDatas('https://feeds.feedburner.com/Koreus-articles');
} }
} }

View File

@@ -34,6 +34,12 @@ class KununuBridge extends BridgeAbstract {
'name' => 'Include benefits', 'name' => 'Include benefits',
'type' => 'checkbox', 'type' => 'checkbox',
'title' => 'Activate to include benefits in the feed' 'title' => 'Activate to include benefits in the feed'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'defaultValue' => 3,
'title' => "Maximum number of items to return in the feed.\n0 = unlimited"
) )
), ),
array( array(
@@ -108,6 +114,8 @@ class KununuBridge extends BridgeAbstract {
$articles = $section->find('article') $articles = $section->find('article')
or returnServerError('Unable to find articles!'); or returnServerError('Unable to find articles!');
$limit = $this->getInput('limit') ?: 0;
// Go through all articles // Go through all articles
foreach($articles as $article) { foreach($articles as $article) {
@@ -141,6 +149,8 @@ class KununuBridge extends BridgeAbstract {
$this->items[] = $item; $this->items[] = $item;
if ($limit > 0 && count($this->items) >= $limit) break;
} }
} }

View File

@@ -431,11 +431,11 @@ class LeBonCoinBridge extends BridgeAbstract {
); );
if($this->getInput('region') != '') { if($this->getInput('region') != '') {
$requestJson->filters->location['regions'] = [$this->getInput('region')]; $requestJson->filters->location['regions'] = array($this->getInput('region'));
} }
if($this->getInput('department') != '') { if($this->getInput('department') != '') {
$requestJson->filters->location['departments'] = [$this->getInput('department')]; $requestJson->filters->location['departments'] = array($this->getInput('department'));
} }
if($this->getInput('cities') != '') { if($this->getInput('cities') != '') {
@@ -467,7 +467,7 @@ class LeBonCoinBridge extends BridgeAbstract {
} }
if($this->getInput('estate') != '') { if($this->getInput('estate') != '') {
$requestJson->filters->enums['real_estate_type'] = [$this->getInput('estate')]; $requestJson->filters->enums['real_estate_type'] = array($this->getInput('estate'));
} }
if($this->getInput('roomsmin') != '' if($this->getInput('roomsmin') != ''
@@ -526,7 +526,7 @@ class LeBonCoinBridge extends BridgeAbstract {
} }
if($this->getInput('fuel') != '') { if($this->getInput('fuel') != '') {
$requestJson->filters->enums['fuel'] = [$this->getInput('fuel')]; $requestJson->filters->enums['fuel'] = array($this->getInput('fuel'));
} }
$requestJson->limit = 30; $requestJson->limit = 30;

View File

@@ -26,8 +26,8 @@ class LeMondeInformatiqueBridge extends FeedExpander {
//No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail //No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail
$content_node = $article_html->find('div.col-primary, div.col-sm-9', 0); $content_node = $article_html->find('div.col-primary, div.col-sm-9', 0);
$item['content'] = utf8_encode($this->cleanArticle($content_node->innertext)); $item['content'] = $this->cleanArticle($content_node->innertext);
$item['author'] = utf8_encode($article_html->find('div.author-infos', 0)->find('b', 0)->plaintext); $item['author'] = $article_html->find('div.author-infos', 0)->find('b', 0)->plaintext;
return $item; return $item;
} }

View File

@@ -11,7 +11,7 @@ class LesJoiesDuCodeBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI) $html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request LesJoiesDuCode.'); or returnServerError('Could not request LesJoiesDuCode.');
foreach($html->find('div.blog-post') as $element) { foreach($html->find('article.blog-post') as $element) {
$item = array(); $item = array();
$temp = $element->find('h1 a', 0); $temp = $element->find('h1 a', 0);
$titre = html_entity_decode($temp->innertext); $titre = html_entity_decode($temp->innertext);

View File

@@ -0,0 +1,22 @@
<?php
class ListverseBridge extends FeedExpander {
const MAINTAINER = 'IceWreck';
const NAME = 'Listverse Bridge';
const URI = 'https://listverse.com/';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'RSS feed for Listverse';
public function collectData(){
$this->collectExpandableDatas('https://listverse.com/feed/', 15);
}
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
// $articlePage gets the entire page's contents
$articlePage = getSimpleHTMLDOM($newsItem->link);
$article = $articlePage->find('#articlecontentonly', 0);
$item['content'] = $article;
return $item;
}
}

73
bridges/MallTvBridge.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
class MallTvBridge extends BridgeAbstract {
const NAME = 'MALL.TV Bridge';
const URI = 'https://www.mall.tv';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'Return newest videos';
const MAINTAINER = 'kolarcz';
const PARAMETERS = array(
array(
'url' => array(
'name' => 'url to the show',
'required' => true,
'exampleValue' => 'https://www.mall.tv/zivot-je-hra'
)
)
);
private function fixChars($text) {
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
}
private function getUploadTimeFromUrl($url) {
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request MALL.TV detail page');
$scriptLdJson = $html->find('script[type="application/ld+json"]', 0)->innertext;
if (!preg_match('/[\'"]uploadDate[\'"]\s*:\s*[\'"](\d{4}-\d{2}-\d{2})[\'"]/', $scriptLdJson, $match)) {
returnServerError('Could not get date from MALL.TV detail page');
}
return strtotime($match[1]);
}
public function collectData() {
$url = $this->getInput('url');
if (!preg_match('/^https:\/\/www\.mall\.tv\/[a-z0-9-]+(\/[a-z0-9-]+)?\/?$/', $url)) {
returnServerError('Invalid url');
}
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request MALL.TV');
$this->feedUri = $url;
$this->feedName = $this->fixChars($html->find('title', 0)->plaintext);
foreach ($html->find('section.isVideo .video-card') as $element) {
$itemTitle = $element->find('.video-card__details-link', 0);
$itemThumbnail = $element->find('.video-card__thumbnail', 0);
$itemUri = self::URI . $itemTitle->getAttribute('href');
$item = array(
'title' => $this->fixChars($itemTitle->plaintext),
'uri' => $itemUri,
'content' => '<img src="' . $itemThumbnail->getAttribute('data-src') . '" />',
'timestamp' => $this->getUploadTimeFromUrl($itemUri)
);
$this->items[] = $item;
}
}
public function getURI() {
return isset($this->feedUri) ? $this->feedUri : parent::getURI();
}
public function getName() {
return isset($this->feedName) ? $this->feedName : parent::getName();
}
}

View File

@@ -3,7 +3,7 @@ class MangareaderBridge extends BridgeAbstract {
const MAINTAINER = 'logmanoriginal'; const MAINTAINER = 'logmanoriginal';
const NAME = 'Mangareader Bridge'; const NAME = 'Mangareader Bridge';
const URI = 'http://www.mangareader.net'; const URI = 'https://www.mangareader.net';
const CACHE_TIMEOUT = 10800; // 3h const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the latest updates, popular mangas or manga updates (new chapters)'; const DESCRIPTION = 'Returns the latest updates, popular mangas or manga updates (new chapters)';

View File

@@ -0,0 +1,127 @@
<?php
class MarktplaatsBridge extends BridgeAbstract {
const NAME = 'Marktplaats';
const URI = 'https://marktplaats.nl';
const DESCRIPTION = 'Read search queries from marktplaats.nl';
const PARAMETERS = array(
'Search' => array(
'q' => array(
'name' => 'query',
'type' => 'text',
'required' => true,
'title' => 'The search string for marktplaats',
),
'z' => array(
'name' => 'zipcode',
'type' => 'text',
'required' => false,
'title' => 'Zip code for location limited searches',
),
'd' => array(
'name' => 'distance',
'type' => 'number',
'required' => false,
'title' => 'The distance in meters from the zipcode',
),
'f' => array(
'name' => 'priceFrom',
'type' => 'number',
'required' => false,
'title' => 'The minimal price in cents',
),
't' => array(
'name' => 'priceTo',
'type' => 'number',
'required' => false,
'title' => 'The maximal price in cents',
),
's' => array(
'name' => 'showGlobal',
'type' => 'checkbox',
'required' => false,
'title' => 'Include result with negative distance',
),
'i' => array(
'name' => 'includeImage',
'type' => 'checkbox',
'required' => false,
'title' => 'Include the image at the end of the content',
),
'r' => array(
'name' => 'includeRaw',
'type' => 'checkbox',
'required' => false,
'title' => 'Include the raw data behind the content',
)
)
);
const CACHE_TIMEOUT = 900;
public function collectData() {
$query = '';
$excludeGlobal = false;
if(!is_null($this->getInput('z')) && !is_null($this->getInput('d'))) {
$query = '&postcode=' . $this->getInput('z') . '&distanceMeters=' . $this->getInput('d');
}
if(!is_null($this->getInput('f'))) {
$query .= '&PriceCentsFrom=' . $this->getInput('f');
}
if(!is_null($this->getInput('t'))) {
$query .= '&PriceCentsTo=' . $this->getInput('t');
}
if(!is_null($this->getInput('s'))) {
if(!$this->getInput('s')) {
$excludeGlobal = true;
}
}
$url = 'https://www.marktplaats.nl/lrp/api/search?query=' . urlencode($this->getInput('q')) . $query;
$jsonString = getSimpleHTMLDOM($url, 900) or returnServerError('No contents received!');
$jsonObj = json_decode($jsonString);
foreach($jsonObj->listings as $listing) {
if(!$excludeGlobal || $listing->location->distanceMeters >= 0) {
$item = array();
$item['uri'] = 'https://marktplaats.nl' . $listing->vipUrl;
$item['title'] = $listing->title;
$item['timestamp'] = $listing->date;
$item['author'] = $listing->sellerInformation->sellerName;
$item['content'] = $listing->description;
$item['categories'] = $listing->verticals;
$item['uid'] = $listing->itemId;
if(!is_null($this->getInput('i')) && !empty($listing->imageUrls)) {
$item['enclosures'] = $listing->imageUrls;
if(is_array($listing->imageUrls)) {
foreach($listing->imageUrls as $imgurl) {
$item['content'] .= "<br />\n<img src='https:" . $imgurl . "' />";
}
} else {
$item['content'] .= "<br>\n<img src='https:" . $listing->imageUrls . "' />";
}
}
if(!is_null($this->getInput('r'))) {
if($this->getInput('r')) {
$item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing);
}
}
$item['content'] .= "<br>\n<br>\nPrice: " . $listing->priceInfo->priceCents / 100;
$item['content'] .= '&nbsp;&nbsp;(' . $listing->priceInfo->priceType . ')';
if(!empty($listing->location->cityName)) {
$item['content'] .= "<br><br>\n" . $listing->location->cityName;
}
if(!is_null($this->getInput('r'))) {
if($this->getInput('r')) {
$item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing);
}
}
$this->items[] = $item;
}
}
}
public function getName(){
if(!is_null($this->getInput('q'))) {
return $this->getInput('q') . ' - Marktplaats';
}
return parent::getName();
}
}

View File

@@ -78,7 +78,7 @@ class MastodonBridge extends FeedExpander {
public function getURI(){ public function getURI(){
if($this->getInput('canusername')) if($this->getInput('canusername'))
return 'https://' . $this->getInstance() . '/users/' . $this->getUsername() . '.atom'; return 'https://' . $this->getInstance() . '/@' . $this->getUsername() . '.rss';
return parent::getURI(); return parent::getURI();
} }

View File

@@ -0,0 +1,48 @@
<?php
class MediapartBlogsBridge extends BridgeAbstract {
const NAME = 'Mediapart Blogs';
const BASE_URI = 'https://blogs.mediapart.fr';
const URI = self::BASE_URI . '/blogs';
const MAINTAINER = 'somini';
const PARAMETERS = array(
array(
'slug' => array(
'name' => 'Blog Slug',
'type' => 'text',
'title' => 'Blog user name',
'exampleValue' => 'jean-vincot',
)
)
);
public function getIcon() {
return 'https://static.mediapart.fr/favicon/favicon-club.ico?v=2';
}
public function collectData() {
$html = getSimpleHTMLDOM(self::BASE_URI . '/' . $this->getInput('slug') . '/blog')
or returnServerError('Could not load content');
foreach($html->find('ul.post-list li') as $element) {
$item = array();
$item_title = $element->find('h3.title a', 0);
$item_divs = $element->find('div');
$item['title'] = $item_title->innertext;
$item['uri'] = self::BASE_URI . trim($item_title->href);
$item['author'] = $element->find('.author .subscriber', 0)->innertext;
$item['content'] = $item_divs[count($item_divs) - 2] . $item_divs[count($item_divs) - 1];
$item['timestamp'] = strtotime($element->find('.author time', 0)->datetime);
$this->items[] = $item;
}
}
public function getName() {
if ($this->getInput('slug')) {
return self::NAME . ' | ' . $this->getInput('slug');
}
return parent::getName();
}
}

View File

@@ -30,29 +30,34 @@ class MediapartBridge extends FeedExpander {
protected function parseItem($newsItem) { protected function parseItem($newsItem) {
$item = parent::parseItem($newsItem); $item = parent::parseItem($newsItem);
// Enable single page mode? // Mediapart provide multiple type of contents.
if ($this->getInput('single_page_mode') === true) { // We only process items relative to the newspaper
$item['uri'] .= '?onglet=full'; // See issue #1292 - https://github.com/RSS-Bridge/rss-bridge/issues/1292
} if (strpos($item['uri'], self::URI . 'journal/') === 0) {
// 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 // If a session cookie is defined, get the full article
$mpsessid = $this->getInput('mpsessid'); $mpsessid = $this->getInput('mpsessid');
if (!empty($mpsessid)) { if (!empty($mpsessid)) {
// Set the session cookie // Set the session cookie
$opt = array(); $opt = array();
$opt[CURLOPT_COOKIE] = 'MPSESSID=' . $mpsessid; $opt[CURLOPT_COOKIE] = 'MPSESSID=' . $mpsessid;
// Get the page // Get the page
$articlePage = getSimpleHTMLDOM( $articlePage = getSimpleHTMLDOM(
$newsItem->link . '?onglet=full', $newsItem->link . '?onglet=full',
array(), array(),
$opt); $opt);
// Extract the article content // Extract the article content
$content = $articlePage->find('div.content-article', 0)->innertext; $content = $articlePage->find('div.content-article', 0)->innertext;
$content = sanitize($content); $content = sanitize($content);
$content = defaultLinkTo($content, static::URI); $content = defaultLinkTo($content, static::URI);
$item['content'] .= $content; $item['content'] .= $content;
}
} }
return $item; return $item;

View File

@@ -3,22 +3,26 @@ class MondeDiploBridge extends BridgeAbstract {
const MAINTAINER = 'Pitchoule'; const MAINTAINER = 'Pitchoule';
const NAME = 'Monde Diplomatique'; const NAME = 'Monde Diplomatique';
const URI = 'http://www.monde-diplomatique.fr/'; const URI = 'https://www.monde-diplomatique.fr';
const CACHE_TIMEOUT = 21600; //6h const CACHE_TIMEOUT = 21600; //6h
const DESCRIPTION = 'Returns most recent results from MondeDiplo.'; const DESCRIPTION = 'Returns most recent results from MondeDiplo.';
private function cleanText($text) {
return trim(str_replace(array('&nbsp;', '&nbsp'), ' ', $text));
}
public function collectData(){ public function collectData(){
$html = getSimpleHTMLDOM(self::URI) $html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request MondeDiplo. for : ' . self::URI); or returnServerError('Could not request MondeDiplo. for : ' . self::URI);
foreach($html->find('div.unarticle') as $article) { foreach($html->find('div.unarticle') as $article) {
$element = $article->parent(); $element = $article->parent();
$title = $element->find('h3', 0)->plaintext;
$datesAuteurs = $element->find('div.dates_auteurs', 0)->plaintext;
$item = array(); $item = array();
$item['uri'] = self::URI . $element->href; $item['uri'] = self::URI . $element->href;
$item['title'] = $element->find('h3', 0)->plaintext; $item['title'] = $this->cleanText($title) . ' - ' . $this->cleanText($datesAuteurs);
$item['content'] = $element->find('div.dates_auteurs', 0)->plaintext $item['content'] = $this->cleanText(str_replace(array($title, $datesAuteurs), '', $element->plaintext));
. '<br>'
. strstr($element->find('div', 0)->plaintext, $element->find('div.dates_auteurs', 0)->plaintext, true);
$this->items[] = $item; $this->items[] = $item;
} }

View File

@@ -61,43 +61,44 @@ class MozillaBugTrackerBridge extends BridgeAbstract {
if($html === false) if($html === false)
returnServerError('Failed to load page!'); returnServerError('Failed to load page!');
// Fix relative URLs
defaultLinkTo($html, self::URI);
// Store header information into private members // Store header information into private members
$this->bugid = $html->find('#bugzilla-body', 0)->find('a', 0)->innertext; $this->bugid = $html->find('#field-value-bug_id', 0)->plaintext;
$this->bugdesc = $html->find('table.bugfields', 0)->find('tr', 0)->find('td', 0)->innertext; $this->bugdesc = $html->find('h1#field-value-short_desc', 0)->plaintext;
// Get and limit comments // Get and limit comments
$comments = $html->find('.bz_comment_table div.bz_comment'); $comments = $html->find('div.change-set');
if($limit > 0 && count($comments) > $limit) { if($limit > 0 && count($comments) > $limit) {
$comments = array_slice($comments, count($comments) - $limit, $limit); $comments = array_slice($comments, count($comments) - $limit, $limit);
} }
// Order comments if ($sorting === 'lf') {
switch($sorting) { $comments = array_reverse($comments, true);
case 'lf': $comments = array_reverse($comments, true);
case 'of':
default: // Nothing to do, keep original order
} }
foreach($comments as $comment) { foreach($comments as $comment) {
$comment = $this->inlineStyles($comment); $comment = $this->inlineStyles($comment);
$item = array(); $item = array();
$item['uri'] = $this->getURI() . '#' . $comment->id; $item['uri'] = $comment->find('h3.change-name', 0)->find('a', 0)->href;
$item['author'] = $comment->find('span.bz_comment_user', 0)->innertext; $item['author'] = $comment->find('td.change-author', 0)->plaintext;
$item['title'] = $comment->find('span.bz_comment_number', 0)->find('a', 0)->innertext; $item['title'] = $comment->find('h3.change-name', 0)->plaintext;
$item['timestamp'] = strtotime($comment->find('span.bz_comment_time', 0)->innertext); $item['timestamp'] = strtotime($comment->find('span.rel-time', 0)->title);
$item['content'] = $comment->find('pre.bz_comment_text', 0)->innertext; $item['content'] = '';
// Fix line breaks (they use LF) if ($comment->find('.comment-text', 0)) {
$item['content'] = str_replace("\n", '<br>', $item['content']); $item['content'] = $comment->find('.comment-text', 0)->outertext;
}
// Fix relative URIs if ($comment->find('div.activity', 0)) {
$item['content'] = $this->replaceRelativeURI($item['content']); $item['content'] .= $comment->find('div.activity', 0)->innertext;
}
$this->items[] = $item; $this->items[] = $item;
} }
} }
public function getURI(){ public function getURI(){
@@ -114,9 +115,8 @@ class MozillaBugTrackerBridge extends BridgeAbstract {
public function getName(){ public function getName(){
switch($this->queriedContext) { switch($this->queriedContext) {
case 'Bug comments': case 'Bug comments':
return 'Bug ' return $this->bugid
. $this->bugid . ' - '
. ' tracker for '
. $this->bugdesc . $this->bugdesc
. ' - ' . ' - '
. parent::getName(); . parent::getName();
@@ -125,17 +125,6 @@ class MozillaBugTrackerBridge extends BridgeAbstract {
} }
} }
/**
* 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 * Adds styles as attributes to tags with known classes
* *
@@ -144,10 +133,14 @@ class MozillaBugTrackerBridge extends BridgeAbstract {
* attributes. * attributes.
*/ */
private function inlineStyles($html){ private function inlineStyles($html){
foreach($html->find('.bz_obsolete') as $element) { foreach($html->find('.bz_closed') as $element) {
$element->style = 'text-decoration:line-through;'; $element->style = 'text-decoration:line-through;';
} }
foreach($html->find('pre') as $element) {
$element->style = 'white-space: pre-wrap;';
}
return $html; return $html;
} }
} }

View File

@@ -15,7 +15,7 @@ class MozillaSecurityBridge extends BridgeAbstract {
$html = defaultLinkTo($html, self::WEBROOT); $html = defaultLinkTo($html, self::WEBROOT);
$item = array(); $item = array();
$articles = $html->find('div[itemprop="articleBody"] h2'); $articles = $html->find('div[id="main-content"] h2');
foreach ($articles as $element) { foreach ($articles as $element) {
$item['title'] = $element->innertext; $item['title'] = $element->innertext;

View File

@@ -15,11 +15,11 @@ class N26Bridge extends BridgeAbstract
public function collectData() public function collectData()
{ {
$html = getSimpleHTMLDOM(self::URI . '/en-fr/blog-archive') $html = getSimpleHTMLDOM(self::URI . '/en-eu/blog-archive')
or returnServerError('Error while downloading the website content'); or returnServerError('Error while downloading the website content');
foreach($html->find('div.ga') as $article) { foreach($html->find('div[class="ag ah ai aj bs bt dx ea fo gx ie if ih ii ij ik s"]') as $article) {
$item = []; $item = array();
$item['uri'] = self::URI . $article->find('h2 a', 0)->href; $item['uri'] = self::URI . $article->find('h2 a', 0)->href;
$item['title'] = $article->find('h2 a', 0)->plaintext; $item['title'] = $article->find('h2 a', 0)->plaintext;
@@ -27,9 +27,9 @@ class N26Bridge extends BridgeAbstract
$fullArticle = getSimpleHTMLDOM($item['uri']) $fullArticle = getSimpleHTMLDOM($item['uri'])
or returnServerError('Error while downloading the full article'); or returnServerError('Error while downloading the full article');
$dateElement = $fullArticle->find('span[class="fk fl de ch fm by"]', 0); $dateElement = $fullArticle->find('time', 0);
$item['timestamp'] = strtotime($dateElement->plaintext); $item['timestamp'] = strtotime($dateElement->plaintext);
$item['content'] = $fullArticle->find('main article', 0)->innertext; $item['content'] = $fullArticle->find('div[class="af ag ah ai an"]', 1)->innertext;
$this->items[] = $item; $this->items[] = $item;
} }

60
bridges/NFLRUSBridge.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
class NFLRUSBridge extends BridgeAbstract {
const NAME = 'NFLRUS';
const URI = 'http://nflrus.ru/';
const DESCRIPTION = 'Returns the recent articles published on nflrus.ru';
const MAINTAINER = 'Maxim Shpak';
private function getEnglishMonth($month) {
$months = array(
'Января' => 'January',
'Февраля' => 'February',
'Марта' => 'March',
'Апреля' => 'April',
'Мая' => 'May',
'Июня' => 'June',
'Июля' => 'July',
'Августа' => 'August',
'Сентября' => 'September',
'Октября' => 'October',
'Ноября' => 'November',
'Декабря' => 'December',
);
if (isset($months[$month])) {
return $months[$month];
}
return false;
}
private function extractArticleTimestamp($article) {
$time = $article->find('time', 0);
if($time) {
$timestring = trim($time->plaintext);
$parts = explode(' ', $timestring);
$month = $this->getEnglishMonth($parts[1]);
if ($month) {
$timestring = $parts[0] . ' ' . $month . ' ' . $parts[2];
return strtotime($timestring);
}
}
return 0;
}
public function collectData() {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Unable to get any articles from NFLRUS');
$html = defaultLinkTo($html, self::URI);
foreach($html->find('article') as $article) {
$item = array();
$item['uri'] = $article->find('.b-article__title a', 0)->href;
$item['title'] = $article->find('.b-article__title a', 0)->plaintext;
$item['author'] = $article->find('.link-author', 0)->plaintext;
$item['timestamp'] = $this->extractArticleTimestamp($article);
$item['content'] = $article->find('div', 0)->innertext;
$this->items[] = $item;
}
}
}

View File

@@ -12,10 +12,8 @@ class NasaApodBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI . 'archivepix.html') $html = getSimpleHTMLDOM(self::URI . 'archivepix.html')
or returnServerError('Error while downloading the website content'); or returnServerError('Error while downloading the website content');
$list = explode('<br>', $html->find('b', 0)->innertext); // Start at 1 to skip the "APOD Full Archive" on top of the page
for($i = 1; $i < 4; $i++) {
for($i = 0; $i < 3; $i++) {
$line = $list[$i];
$item = array(); $item = array();
$uri_page = $html->find('a', $i + 3)->href; $uri_page = $html->find('a', $i + 3)->href;
@@ -26,9 +24,14 @@ class NasaApodBridge extends BridgeAbstract {
$picture_html_string = $picture_html->innertext; $picture_html_string = $picture_html->innertext;
//Extract image and explanation //Extract image and explanation
$media = $picture_html->find('p', 1)->innertext; $image_wrapper = $picture_html->find('a', 1);
$media = strstr($media, '<br>'); $image_path = $image_wrapper->href;
$media = preg_replace('/<br>/', '', $media, 1); $img_placeholder = $image_wrapper->find('img', 0);
$img_alt = $img_placeholder->alt;
$img_style = $img_placeholder->style;
$image_uri = self::URI . $image_path;
$new_img_placeholder = "<img src=\"$image_uri\" alt=\"$img_alt\" style=\"$img_style\">";
$media = "<a href=\"$image_uri\">$new_img_placeholder</a>";
$explanation = $picture_html->find('p', 2)->innertext; $explanation = $picture_html->find('p', 2)->innertext;
//Extract date from the picture page //Extract date from the picture page

View File

@@ -0,0 +1,59 @@
<?php
class NewOnNetflixBridge extends BridgeAbstract {
const NAME = 'NewOnNetflix removals bridge';
const URI = 'https://www.newonnetflix.info';
const DESCRIPTION = 'Upcoming removals from Netflix (NewOnNetflix already provides additions as RSS)';
const MAINTAINER = 'jdesgats';
const PARAMETERS = array(array(
'country' => array(
'name' => 'Country',
'type' => 'list',
'values' => array(
'Australia/New Zealand' => 'anz',
'Canada' => 'can',
'United Kingdom' => 'uk',
'United States' => 'usa',
),
'defaultValue' => 'uk',
)
));
const CACHE_TIMEOUT = 3600 * 24;
public function collectData() {
$baseURI = 'https://' . $this->getInput('country') . '.newonnetflix.info';
$html = getSimpleHTMLDOMCached($baseURI . '/lastchance', self::CACHE_TIMEOUT)
or returnServerError('Could not request NewOnNetflix (U FAILED LOL).');
foreach($html->find('article.oldpost') as $element) {
$title = $element->find('a.infopop[title]', 0);
$img = $element->find('img[lazy_src]', 0);
$date = $element->find('span[title]', 0);
// format sholud be 'dd/mm/yy - dd/mm/yy'
// (the added date might be "unknown")
$fromTo = array();
if (preg_match('/^\s*(.*?)\s*-\s*(.*?)\s*$/', $date->title, $fromTo)) {
$from = $fromTo[1];
$to = $fromTo[2];
} else {
$from = 'unknown';
$to = 'unknown';
}
$summary = <<<EOD
<img src="{$img->lazy_src}" loading="lazy">
<div>{$title->title}</div>
<div><strong>Added on:</strong>$from</div>
<div><strong>Removed on:</strong>$to</div>
EOD;
$item = array();
$item['uri'] = $baseURI . $title->href;
$item['title'] = $to . ' - ' . $title->plaintext;
$item['content'] = $summary;
// some movies are added and removed multiple times
$item['uid'] = $title->href . '-' . $to;
$this->items[] = $item;
}
}
}

View File

@@ -1,9 +1,10 @@
<?php <?php
class NextInpactBridge extends FeedExpander { class NextInpactBridge extends FeedExpander {
const MAINTAINER = 'qwertygc'; const MAINTAINER = 'qwertygc and ORelio';
const NAME = 'NextInpact Bridge'; const NAME = 'NextInpact Bridge';
const URI = 'https://www.nextinpact.com/'; const URI = 'https://www.nextinpact.com/';
const URI_HARDWARE = 'https://www.inpact-hardware.com/';
const DESCRIPTION = 'Returns the newest articles.'; const DESCRIPTION = 'Returns the newest articles.';
const PARAMETERS = array( array( const PARAMETERS = array( array(
@@ -11,10 +12,30 @@ class NextInpactBridge extends FeedExpander {
'name' => 'Feed', 'name' => 'Feed',
'type' => 'list', 'type' => 'list',
'values' => array( 'values' => array(
'Tous nos articles' => 'news', 'Nos actualités' => array(
'Nos contenus en accès libre' => 'acces-libre', 'Toutes nos publications' => 'news',
'Blog' => 'blog', 'Toutes nos publications sauf #LeBrief' => 'nobrief',
'Bons plans' => 'bonsplans' 'Toutes nos publications sauf INpact Hardware' => 'noih',
'Seulement les publications INpact Hardware' => 'hardware:news',
'Seulement les publications Next INpact' => 'nobrief-noih',
'Seulement les publications #LeBrief' => 'lebrief',
),
'Flux spécifiques' => array(
'Le blog' => 'blog',
'Les bons plans' => 'bonsplans',
'Publications INpact Hardware en accès libre' => 'hardware:acces-libre',
'Publications Next INpact en accès libre' => 'acces-libre',
),
'Flux thématiques' => array(
'Tech' => 'category:1',
'Logiciel' => 'category:2',
'Internet' => 'category:3',
'Mobilité' => 'category:4',
'Droit' => 'category:5',
'Économie' => 'category:6',
'Culture numérique' => 'category:7',
'Next INpact' => 'category:8',
)
) )
), ),
'filter_premium' => array( 'filter_premium' => array(
@@ -39,9 +60,27 @@ class NextInpactBridge extends FeedExpander {
public function collectData(){ public function collectData(){
$feed = $this->getInput('feed'); $feed = $this->getInput('feed');
if (empty($feed)) $base_uri = self::URI;
$args = '';
if (empty($feed)) {
// Default to All articles
$feed = 'news'; $feed = 'news';
$this->collectExpandableDatas(self::URI . 'rss/' . $feed . '.xml'); }
if (strpos($feed, 'hardware:') === 0) {
// Feed hosted on Hardware domain
$base_uri = self::URI_HARDWARE;
$feed = str_replace('hardware:', '', $feed);
}
if (strpos($feed, 'category:') === 0) {
// Feed with specific category parameter
$args = '?CategoryIds=' . str_replace('category:', '', $feed);
$feed = 'params';
}
$this->collectExpandableDatas($base_uri . 'rss/' . $feed . '.xml' . $args);
} }
protected function parseItem($newsItem){ protected function parseItem($newsItem){
@@ -57,9 +96,11 @@ class NextInpactBridge extends FeedExpander {
if (!is_object($html)) if (!is_object($html))
return 'Failed to request NextInpact: ' . $url; return 'Failed to request NextInpact: ' . $url;
// Filter premium and brief articles?
$brief_selector = 'div.brief-container';
foreach(array( foreach(array(
'filter_premium' => 'h2.title_reserve_article', 'filter_premium' => 'p.red-msg',
'filter_brief' => 'div.brief-inner-content' 'filter_brief' => $brief_selector
) as $param_name => $selector) { ) as $param_name => $selector) {
$param_val = intval($this->getInput($param_name)); $param_val = intval($this->getInput($param_name));
if ($param_val != 0) { if ($param_val != 0) {
@@ -71,38 +112,71 @@ class NextInpactBridge extends FeedExpander {
} }
} }
if (is_object($html->find('div[itemprop=articleBody], div.brief-inner-content', 0))) { $article_content = $html->find('div.article-content', 0);
if (!is_object($article_content)) {
$article_content = $html->find('div.content', 0);
}
if (is_object($article_content)) {
$subtitle = trim($html->find('span.sub_title, div.brief-head', 0)); // Subtitle
if(is_object($subtitle) && $subtitle->plaintext !== $item['title']) { $subtitle = $html->find('small.subtitle', 0);
$subtitle = '<p><em>' . $subtitle->plaintext . '</em></p>'; if(!is_object($subtitle) && !is_object($html->find($brief_selector, 0))) {
$subtitle = $html->find('small', 0);
}
if(!is_object($subtitle)) {
$content_wrapper = $html->find('div.content-wrapper', 0);
if (is_object($content_wrapper)) {
$subtitle = $content_wrapper->find('h2.title', 0);
}
}
if(is_object($subtitle) && (!isset($item['title']) || $subtitle->plaintext != $item['title'])) {
$subtitle = '<p><em>' . trim($subtitle->plaintext) . '</em></p>';
} else { } else {
$subtitle = ''; $subtitle = '';
} }
$postimg = $html->find( // Image
'div.container_main_image_article, div.image-brief-container, div.image-brief-side-container', 0 $postimg = $html->find('div.article-image, div.image-container', 0);
);
if(is_object($postimg)) { if(is_object($postimg)) {
$postimg = '<p><img src="' $postimg = $postimg->find('img', 0);
. $postimg->find('img.dedicated', 0)->src if (!empty($postimg->src)) {
. '" alt="-" /></p>'; $postimg = $postimg->src;
} else {
$postimg = $postimg->srcset; //"url 355w, url 1003w, url 748w"
$postimg = explode(', ', $postimg); //split by ', ' to get each url separately
$postimg = end($postimg); //Get last item: "url 748w" which is of largest size
$postimg = explode(' ', $postimg); //split by ' ' to separate url from res
$postimg = array_reverse($postimg); //reverse array content to have url last
$postimg = end($postimg); //Get last item of array: "url"
}
$postimg = '<p><img src="' . $postimg . '" alt="-" /></p>';
} else { } else {
$postimg = ''; $postimg = '';
} }
// Paywall
$paywall = $html->find('div.paywall-restriction', 0);
if (is_object($paywall) && is_object($paywall->find('p.red-msg', 0))) {
$paywall = '<p><em>' . $paywall->find('span.head-mention', 0)->innertext . '</em></p>';
} else {
$paywall = '';
}
// Content
$article_content = $article_content->outertext;
$article_content = str_replace('>Signaler une erreur</span>', '></span>', $article_content);
// Result
$text = $subtitle $text = $subtitle
. $postimg . $postimg
. $html->find('div[itemprop=articleBody], div.brief-inner-content', 0)->outertext; . $article_content
. $paywall;
} else { } else {
$text = $item['content'] $text = '<p><em>Failed to retrieve full article content</em></p>';
. '<p><em>Failed retrieve full article content</em></p>'; if (isset($item['content'])) {
} $text = $item['content'] . $text;
}
$premium_article = $html->find('h2.title_reserve_article', 0);
if (is_object($premium_article)) {
$text .= '<p><em>' . $premium_article->innertext . '</em></p>';
} }
return $text; return $text;

View File

@@ -3,7 +3,7 @@ class NiceMatinBridge extends FeedExpander {
const MAINTAINER = 'pit-fgfjiudghdf'; const MAINTAINER = 'pit-fgfjiudghdf';
const NAME = 'NiceMatin'; const NAME = 'NiceMatin';
const URI = 'http://www.nicematin.com/'; const URI = 'https://www.nicematin.com/';
const DESCRIPTION = 'Returns the 10 newest posts from NiceMatin (full text)'; const DESCRIPTION = 'Returns the 10 newest posts from NiceMatin (full text)';
public function collectData(){ public function collectData(){

View File

@@ -17,6 +17,15 @@ class NineGagBridge extends BridgeAbstract {
'Fresh' => 'fresh', 'Fresh' => 'fresh',
), ),
), ),
'video' => array(
'name' => 'Filter Video',
'type' => 'list',
'values' => array(
'NotFiltred' => 'none',
'VideoFiltred' => 'without',
'VideoOnly' => 'only',
),
),
'p' => array( 'p' => array(
'name' => 'Pages', 'name' => 'Pages',
'type' => 'number', 'type' => 'number',
@@ -121,13 +130,32 @@ class NineGagBridge extends BridgeAbstract {
} }
foreach ($posts as $post) { foreach ($posts as $post) {
$item['uri'] = $post['url']; $AvoidElement = false;
$item['title'] = $post['title']; switch ($this->getInput('video')) {
$item['content'] = self::getContent($post); case 'without':
$item['categories'] = self::getCategories($post); if ($post['type'] === 'Animated') {
$item['timestamp'] = self::getTimestamp($post); $AvoidElement = true;
}
break;
case 'only':
echo $post['type'];
if ($post['type'] !== 'Animated') {
$AvoidElement = true;
}
break;
case 'none': default:
break;
}
$this->items[] = $item; if (!$AvoidElement) {
$item['uri'] = preg_replace('/^http:/i', 'https:', $post['url']);
$item['title'] = $post['title'];
$item['content'] = self::getContent($post);
$item['categories'] = self::getCategories($post);
$item['timestamp'] = self::getTimestamp($post);
$this->items[] = $item;
}
} }
} }

View File

@@ -0,0 +1,131 @@
<?php
ini_set('max_execution_time', '300');
class NordbayernBridge extends BridgeAbstract {
const MAINTAINER = 'schabi.org';
const NAME = 'Nordbayern Bridge';
const CACHE_TIMEOUT = 3600;
const URI = 'https://www.nordbayern.de';
const DESCRIPTION = 'Bridge for Bavarian reginoal news site nordbayern.de';
const PARAMETERS = array( array(
'region' => array(
'name' => 'region',
'type' => 'list',
'exampleValue' => 'Nürnberg',
'title' => 'Select a region',
'values' => array(
'Nürnberg' => 'nuernberg',
'Fürth' => 'fuerth',
'Altdorf' => 'altdorf',
'Ansbach' => 'ansbach',
'Bad Windsheim' => 'bad-windsheim',
'Bamberg' => 'bamberg',
'Dinkelsbühl/Feuchtwangen' => 'dinkelsbuehl-feuchtwangen',
'Feucht' => 'feucht',
'Forchheim' => 'forchheim',
'Gunzenhausen' => 'gunzenhausen',
'Hersbruck' => 'hersbruck',
'Herzogenaurach' => 'herzogenaurach',
'Hilpolstein' => 'holpolstein',
'Höchstadt' => 'hoechstadt',
'Lauf' => 'lauf',
'Neumarkt' => 'neumarkt',
'Neustadt/Aisch' => 'neustadt-aisch',
'Pegnitz' => 'pegnitz',
'Roth' => 'roth',
'Rothenburg o.d.T.' => 'rothenburg-o-d-t',
'Schwabach' => 'schwabach',
'Treuchtlingen' => 'treuchtlingen',
'Weißenburg' => 'weissenburg'
)
),
'policeReports' => array(
'name' => 'Police Reports',
'type' => 'checkbox',
'exampleValue' => 'checked',
'title' => 'Read Police Reports',
)
));
private function getImageUrlFromScript($script) {
preg_match(
"#src=\\\\'(https:[-:\\.\\\\/a-zA-Z0-9%_]*\\.(jpg|JPG))#",
$script->innertext,
$matches,
PREG_OFFSET_CAPTURE
);
if(isset($matches[1][0])) {
return stripcslashes($matches[1][0]) . '?w=800';
}
return null;
}
private function handleArticle($link) {
$item = array();
$article = getSimpleHTMLDOM($link);
$content = $article->find('div[class*=article-content]', 0);
$item['uri'] = $link;
$item['title'] = $article->find('h1', 0)->innertext;
$item['content'] = '';
//first get image from block/modul
$figure = $article->find('figure[class*=panorama]', 0);
if($figure !== null) {
$imgUrl = self::getImageUrlFromScript($figure->find('script', 0));
if($imgUrl === null) {
$imgUrl = self::getImageUrlFromScript($figure->find('script', 1));
}
$item['content'] .= '<img src="' . $imgUrl . '">';
}
// get regular paragraphs
foreach($content->children() as $child) {
if($child->tag === 'p') {
$item['content'] .= $child;
}
}
//get image divs
foreach($content->find('div[class*=article-slideshow]') as $slides) {
foreach($slides->children() as $child) {
switch($child->tag) {
case 'p':
$item['content'] .= $child;
break;
case 'h5':
$item['content'] .= '<h5><a href="'
. self::URI . $child->find('a', 0)->href . '">' . $child->plaintext . '</a></h5>';
break;
case 'a':
$url = self::getImageUrlFromScript($child->find('script', 0));
$item['content'] .= '<img src="' . $url . '">';
break;
}
}
}
$this->items[] = $item;
$article->clear();
}
private function handleNewsblock($listSite, $readPoliceReports) {
$newsBlocks = $listSite->find('section[class*=newsblock]');
$regionalNewsBlock = $newsBlocks[0];
$policeBlock = $newsBlocks[1];
foreach($regionalNewsBlock->find('h2') as $headline) {
self::handleArticle(self::URI . $headline->find('a', 0)->href);
}
if($readPoliceReports === true) {
foreach($policeBlock->find('h2') as $headline) {
self::handleArticle(self::URI . $headline->find('a', 0)->href);
}
}
}
public function collectData() {
$item = array();
$region = $this->getInput('region');
$listSite = getSimpleHTMLDOM(self::URI . '/region/' . $region);
self::handleNewsblock($listSite, $this->getInput('policeReports'));
}
}

View File

@@ -100,7 +100,9 @@ class NyaaTorrentsBridge extends BridgeAbstract {
//Retrieve data from page contents //Retrieve data from page contents
$item_title = str_replace(' :: Nyaa', '', $item_html->find('title', 0)->plaintext); $item_title = str_replace(' :: Nyaa', '', $item_html->find('title', 0)->plaintext);
$item_desc = str_get_html(markdownToHtml($item_html->find('#torrent-description', 0)->innertext)); $item_desc = str_get_html(
markdownToHtml(html_entity_decode($item_html->find('#torrent-description', 0)->innertext))
);
$item_author = extractFromDelimiters($item_html->outertext, 'href="/user/', '"'); $item_author = extractFromDelimiters($item_html->outertext, 'href="/user/', '"');
$item_date = intval(extractFromDelimiters($item_html->outertext, 'data-timestamp="', '"')); $item_date = intval(extractFromDelimiters($item_html->outertext, 'data-timestamp="', '"'));

View File

@@ -1,9 +1,9 @@
<?php <?php
class WhydBridge extends BridgeAbstract { class OpenwhydBridge extends BridgeAbstract {
const MAINTAINER = 'kranack'; const MAINTAINER = 'kranack';
const NAME = 'Whyd Bridge'; const NAME = 'Openwhyd Bridge';
const URI = 'http://www.whyd.com/'; const URI = 'https://openwhyd.org';
const CACHE_TIMEOUT = 600; // 10min const CACHE_TIMEOUT = 600; // 10min
const DESCRIPTION = 'Returns 10 newest music from user profile'; const DESCRIPTION = 'Returns 10 newest music from user profile';
@@ -17,8 +17,7 @@ class WhydBridge extends BridgeAbstract {
private $userName = ''; private $userName = '';
public function getIcon() { public function getIcon() {
return self::URI . 'assets/favicons/ return self::URI . '/images/favicon.ico';
32-6b62a9f14d5e1a9213090d8f00f286bba3a6022381a76390d1d0926493b12593.png?v=6';
} }
public function collectData(){ public function collectData(){
@@ -26,11 +25,11 @@ class WhydBridge extends BridgeAbstract {
if(strlen(preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))) == 24) { if(strlen(preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))) == 24) {
// is input the userid ? // is input the userid ?
$html = getSimpleHTMLDOM( $html = getSimpleHTMLDOM(
self::URI . 'u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u')) self::URI . '/u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))
) or returnServerError('No results for this query.'); ) or returnServerError('No results for this query.');
} else { // input may be the username } else { // input may be the username
$html = getSimpleHTMLDOM( $html = getSimpleHTMLDOM(
self::URI . 'search?q=' . urlencode($this->getInput('u')) self::URI . '/search?q=' . urlencode($this->getInput('u'))
) or returnServerError('No results for this query.'); ) or returnServerError('No results for this query.');
for($j = 0; $j < 5; $j++) { for($j = 0; $j < 5; $j++) {
@@ -57,6 +56,6 @@ class WhydBridge extends BridgeAbstract {
} }
public function getName(){ public function getName(){
return (!empty($this->userName) ? $this->userName . ' - ' : '') . 'Whyd Bridge'; return (!empty($this->userName) ? $this->userName . ' - ' : '') . 'Openwhyd Bridge';
} }
} }

View File

@@ -0,0 +1,37 @@
<?php
class OpenwrtSecurityBridge extends BridgeAbstract {
const NAME = 'OpenWrt Security Advisories';
const URI = 'https://openwrt.org/advisory/start';
const DESCRIPTION = 'Security Advisories published by openwrt.org';
const MAINTAINER = 'mschwld';
const CACHE_TIMEOUT = 3600;
const WEBROOT = 'https://openwrt.org';
public function collectData() {
$item = array();
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request entries');
$advisories = $html->find('div[class=plugin_nspages]', 0);
foreach($advisories->find('a[class=wikilink1]') as $element) {
$item = array();
$row = $element->innertext;
$item['title'] = substr($row, 0, strpos($row, ' - '));
$item['timestamp'] = $this->getDate($element->href);
$item['uri'] = self::WEBROOT . $element->href;
$item['uid'] = self::WEBROOT . $element->href;
$item['content'] = substr($row, strpos($row, ' - ') + 3);
$item['author'] = 'OpenWrt Project';
$this->items[] = $item;
}
}
private function getDate($href) {
$date = substr($href, -12);
return $date;
}
}

View File

@@ -0,0 +1,175 @@
<?php
class OtrkeyFinderBridge extends BridgeAbstract {
const MAINTAINER = 'mibe';
const NAME = 'OtrkeyFinder';
const URI = 'https://otrkeyfinder.com';
const URI_TEMPLATE = 'https://otrkeyfinder.com/en/?search=%s&order=&page=%d';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns the newest .otrkey files matching the search criteria.';
const PARAMETERS = array(
array(
'searchterm' => array(
'name' => 'Search term',
'exampleValue' => 'Terminator',
'title' => 'The search term is case-insensitive',
),
'station' => array(
'name' => 'Station name',
'exampleValue' => 'ARD',
),
'type' => array(
'name' => 'Media type',
'type' => 'list',
'values' => array(
'any' => '',
'Detail' => array(
'HD' => 'HD.avi',
'AC3' => 'HD.ac3',
'HD &amp; AC3' => 'HD.',
'HQ' => 'HQ.avi',
'AVI' => 'g.avi', // 'g.' to exclude HD.avi and HQ.avi (filename always contains 'mpg.')
'MP4' => '.mp4',
),
),
),
'minTime' => array(
'name' => 'Min. running time',
'type' => 'number',
'title' => 'The minimum running time in minutes. The resolution is 5 minutes.',
'exampleValue' => '90',
'defaultValue' => '0',
),
'maxTime' => array(
'name' => 'Max. running time',
'type' => 'number',
'title' => 'The maximum running time in minutes. The resolution is 5 minutes.',
'exampleValue' => '120',
'defaultValue' => '0',
),
'pages' => array(
'name' => 'Number of pages',
'type' => 'number',
'title' => 'Specifies the number of pages to fetch. Increase this value if you get an empty feed.',
'exampleValue' => '5',
'defaultValue' => '5',
),
)
);
// Example: Terminator_20.04.13_02-25_sf2_100_TVOON_DE.mpg.avi.otrkey
// The first group is the running time in minutes
const FILENAME_REGEX = '/_(\d+)_TVOON_DE\.mpg\..+\.otrkey/';
// year.month.day_hour-minute with leading zeros
const TIME_REGEX = '/\d{2}\.\d{2}\.\d{2}_\d{2}-\d{2}/';
const CONTENT_TEMPLATE = '<ul>%s</ul>';
const MIRROR_TEMPLATE = '<li><a href="https://otrkeyfinder.com%s">%s</a></li>';
public function collectData() {
$pages = $this->getInput('pages');
for($page = 1; $page <= $pages; $page++) {
$uri = $this->buildUri($page);
$html = getSimpleHTMLDOMCached($uri, self::CACHE_TIMEOUT)
or returnServerError('Could not request ' . $uri);
$keys = $html->find('div.otrkey');
foreach($keys as $key) {
$temp = $this->buildItem($key);
if ($temp != null)
$this->items[] = $temp;
}
// Sleep for 0.5 seconds to don't hammer the server.
usleep(500000);
}
}
private function buildUri($page) {
$searchterm = $this->getInput('searchterm');
$station = $this->getInput('station');
$type = $this->getInput('type');
// Combine all three parts to a search query by separating them with white space
$search = implode(' ', array($searchterm, $station, $type));
$search = trim($search);
$search = urlencode($search);
return sprintf(self::URI_TEMPLATE, $search, $page);
}
private function buildItem(simple_html_dom_node $node) {
$file = $this->getFilename($node);
if ($file == null)
return null;
$minTime = $this->getInput('minTime');
$maxTime = $this->getInput('maxTime');
// Do we need to check the running time?
if ($minTime != 0 || $maxTime != 0) {
if ($maxTime > 0 && $maxTime < $minTime)
returnClientError('The minimum running time must be less than the maximum running time.');
preg_match(self::FILENAME_REGEX, $file, $matches);
if (!isset($matches[1]))
return null;
$time = (integer)$matches[1];
// Check for minimum running time
if ($minTime > 0 && $minTime > $time)
return null;
// Check for maximum running time
if ($maxTime > 0 && $maxTime < $time)
return null;
}
$item = array();
$item['title'] = $file;
// The URI_TEMPLATE for querying the site can be reused here
$item['uri'] = sprintf(self::URI_TEMPLATE, $file, 1);
$content = $this->buildContent($node);
if ($content != null)
$item['content'] = $content;
if (preg_match(self::TIME_REGEX, $file, $matches) === 1) {
$item['timestamp'] = DateTime::createFromFormat(
'y.m.d_H-i',
$matches[0],
new DateTimeZone('Europe/Berlin')
)->getTimestamp();
}
return $item;
}
private function getFilename(simple_html_dom_node $node) {
$file = $node->find('.file', 0);
if ($file == null)
return null;
else
return trim($file->innertext);
}
private function buildContent(simple_html_dom_node $node) {
$mirrors = $node->find('div.mirror');
$list = '';
// Build list of available mirrors
foreach($mirrors as $mirror) {
$anchor = $mirror->find('a', 0);
$list .= sprintf(self::MIRROR_TEMPLATE, $anchor->href, $anchor->innertext);
}
return sprintf(self::CONTENT_TEMPLATE, $list);
}
}

View File

@@ -3,7 +3,7 @@ class ParuVenduImmoBridge extends BridgeAbstract {
const MAINTAINER = 'polo2ro'; const MAINTAINER = 'polo2ro';
const NAME = 'Paru Vendu Immobilier'; const NAME = 'Paru Vendu Immobilier';
const URI = 'http://www.paruvendu.fr'; const URI = 'https://www.paruvendu.fr';
const CACHE_TIMEOUT = 10800; // 3h const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the ads from the first page of search result.'; const DESCRIPTION = 'Returns the ads from the first page of search result.';

View File

@@ -2,22 +2,43 @@
class PcGamerBridge extends BridgeAbstract class PcGamerBridge extends BridgeAbstract
{ {
const NAME = 'PC Gamer'; const NAME = 'PC Gamer';
const URI = 'https://www.pcgamer.com/'; const URI = 'https://www.pcgamer.com/archive/';
const DESCRIPTION = 'PC Gamer Most Read Stories'; const DESCRIPTION = 'PC Gamer Most Read Stories';
const MAINTAINER = 'mdemoss'; const CACHE_TIMEOUT = 3600;
const MAINTAINER = 'IceWreck, mdemoss';
public function collectData() public function collectData()
{ {
$html = getSimpleHTMLDOMCached($this->getURI(), 300); $html = getSimpleHTMLDOMCached($this->getURI(), 300);
$stories = $html->find('div#popularcontent li.most-popular-item'); $stories = $html->find('ul.basic-list li.day-article');
$i = 0;
// Find induvidual stories in the archive page
foreach ($stories as $element) { foreach ($stories as $element) {
if($i == 15) break;
$item['uri'] = $element->find('a', 0)->href; $item['uri'] = $element->find('a', 0)->href;
// error_log(print_r($item['uri'], TRUE));
$articleHtml = getSimpleHTMLDOMCached($item['uri']); $articleHtml = getSimpleHTMLDOMCached($item['uri']);
$item['title'] = $element->find('h4 a', 0)->plaintext; $item['title'] = $element->find('a', 0)->plaintext;
$item['timestamp'] = strtotime($articleHtml->find('meta[name=pub_date]', 0)->content); $item['timestamp'] = strtotime($articleHtml->find('meta[name=pub_date]', 0)->content);
$item['content'] = $articleHtml->find('meta[name=description]', 0)->content; $item['author'] = $articleHtml->find('span.by-author a', 0)->plaintext;
$item['author'] = $articleHtml->find('a[itemprop=author]', 0)->plaintext;
// Get the article content
$articleContents = $articleHtml->find('#article-body', 0);
/*
By default the img src has a link to an error image and then the actual image
is added in by JS. So we replace the error image with the actual full size image
whoose link is in one of the attributes of the img tag
*/
foreach($articleContents->find('img') as $img) {
$imgsrc = $img->getAttribute('data-original-mos');
// error_log($imgsrc);
$img->src = $imgsrc;
}
$item['content'] = $articleContents;
$this->items[] = $item; $this->items[] = $item;
$i++;
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More