1
0
mirror of https://github.com/RSS-Bridge/rss-bridge.git synced 2025-08-16 13:34:11 +02:00

Compare commits

...

216 Commits

Author SHA1 Message Date
logmanoriginal
de7622ebbf version: Bump to 2018-08-07 2018-08-07 18:37:38 +02:00
logmanoriginal
09c9d015b4 [ForGifsBridge] Add new bridge 2018-08-04 23:42:58 +02:00
logmanoriginal
3a496e3b18 [FilterBridge] Add option to build title from content
Adds a new option '&title_from_content=on' to build the title for feed
items from the feeds content. The title is generated from the first
whitespace after 50 characters of the content or the entire content if
the total size is lower than 50 characters.

References #587
2018-08-04 20:46:59 +02:00
Eugene Molotov
df58f5bbdb [core] Add urljoin (#756)
Adds php-urljoin from https://github.com/fluffy-critter/php-urljoin to replace the custom implementation of 'defaultLinkTo'
2018-08-02 06:31:56 +02:00
logmanoriginal
9d0452d11b [.travis] Use composer for HHVM
This fixes the HHVM build failing because pear doesn't exist in HHVM.
2018-08-01 19:37:10 +02:00
sublimz
f92ac49947 [LeBonCoinBridge] Add cities support (#751) 2018-08-01 17:25:18 +02:00
Benasse
a574fa15ac [YGGTorrentBridge] Order search result by publish date (#762) 2018-07-31 21:46:10 +02:00
Nemo
8f9a385b4d [AmazonPriceTrackerBridge] Improve Amazon scraper logic (#761)
- Now works on all websites, and even with products
  with multiple prices
- Closes #750
2018-07-31 21:44:37 +02:00
logmanoriginal
53bdfa3bf0 [GooglePlusPostBridge] Skip posts without message 2018-07-31 19:15:09 +02:00
logmanoriginal
53278b2eed [GooglePlusPostBridge] Add option to include image in content
References #600
2018-07-31 19:09:12 +02:00
logmanoriginal
5f3c55b808 [GooglePlusPostBridge] General cleanup 2018-07-31 18:55:35 +02:00
logmanoriginal
fb79a67370 [GooglePlusPostBridge] Normalize static::URI usage
This commit fixes a few things related to static::URI

1) Remove trailing slash from the URI to simplify using 'defaultLinkTo'
2) Use static::URI instead of self::URI for consistency
3) Remove custom implementation of 'defaultLinkTo'
2018-07-31 18:29:14 +02:00
logmanoriginal
3c4e12ceba [GooglePlusPostBridge] Add images to enclosures
Images are collected for each post and added to enclosures. Images or
animtions from lh3.googleusercontent.com are specifically handled in
order to return the animated version of the gif and the original sized
image (this is normally taken care of by JS in the browser).
2018-07-31 18:18:22 +02:00
logmanoriginal
0d1923c52f [GitHubGistBridge] Add new bridge
Adds a new bridge for https://gist.github.com

The bridge generates feeds for comments on a particular gist based on
the gist ID or full URI. For better readability the general behavior
of code sections is manually restored with the original CSS styles
from GitHub.
2018-07-29 16:31:47 +02:00
logmanoriginal
ce896b4247 [SkimfeedBridge] Add new bridge
New bridge for Skimfeed: https://skimfeed.com

Generates feeds for all features of Skimfeed:

- News (the ones displayed on the front page)
- Hot topics ("What's Hot" section on the front page)
- Tech news (preconfigured feeds in the menu bar)
- Custom feeds (using the configuration system of Skimfeed), see
https://skimfeed.com/custom.php

The number of items returned by the bridge can be limited for all
categories ('&limit=...'). This parameter is optional, all categories
are unlimited by default!

Authors are added with HTML anchors in order to allow quick navigation
to source channels.

The bridge ships with developer tools to auto-generate lists in the
future (especially useful for 'Tech news'!)

References #748
2018-07-27 23:18:32 +02:00
sysadminstory
a4b2d88dbe [DealabsBridge] Follow website change (#758) 2018-07-25 20:02:31 +02:00
logmanoriginal
65ec04ea98 [contents] Remove superfluous debug log from getContents
References #757
2018-07-25 19:56:46 +02:00
logmanoriginal
afb4de318b [FlickrBridge] Fix missing scheme for image URLs
References #754
2018-07-23 20:14:46 +02:00
Eugene Molotov
43bb17f995 [VkBridge] Converting hashtags to categories (#755)
* [VkBridge] Converting hashtags to categories
2018-07-22 16:43:00 +02:00
logmanoriginal
bae7a5879f [FlickrBridge] Fixed broken bridge
Following changes in the JSON data and selecting images for the
content (320x240 or bigger) and enclosure (largest version). All of
the data is now extracted from the JSON data instead of parsing the
DOM.

References #754
2018-07-22 14:06:04 +02:00
LogMANOriginal
bd760cbcee [README] Add docker build status 2018-07-21 21:59:48 +02:00
LogMANOriginal
cd20b4476f [README] Add label for latest release 2018-07-21 21:54:46 +02:00
LogMANOriginal
d83f2f285b Separate index and bridge card generating code into a separate classes (#734)
[html] Generate index and bridge cards using separate clases

Move HTML generating code from 'index.php' to 'Index.php', separating components into static functions.

Move HTML generation code for bridge cards from 'html.php' to 'BridgeCard.php', separating components into static functions.
2018-07-21 18:15:07 +02:00
logmanoriginal
15e6d77569 [FierPandaBridge] Fix bridge
This bridge now returns all articles from the front page, following
layout changes in the past.

References #679
2018-07-21 18:07:03 +02:00
logmanoriginal
f97d2ef254 [Torrent9Bridge] Remove bridge
The site moved from www.torrent9.pe to www.t9.pe and is now protected
by Cloudflare challenges, making it inaccessible to RSS-Bridge.
2018-07-21 17:45:22 +02:00
logmanoriginal
91ae2a23d7 [CpasbienBridge] Remove bridge
Removing this bridge for two reasons:

1) The service moved from www.cpasbien.cm to www.torrents9.blue,
changing the layout in the process (incompatible).

2) The new site is permanently protected by Cloudflare IUAM, making
it inaccessible by RSS-Bridge.

While it would certainly be possible to rewrite the bridge to work
with the new layout, the site is still inaccessible.

References #605
2018-07-21 17:43:29 +02:00
logmanoriginal
066ef1d7db [contents] Add Cloudflare challenge detection
Adds detection for servers responding with Cloudflare challenges,
throwing a server error if detected:

"The server responded with a Cloudflare challenge, which is not
supported by RSS-Bridge! If this error persists longer than a week,
please consider opening an issue on GitHub!"

This is supposed to support maintainers to identify broken bridges
for sites with Cloudflare enabled permanently. It doesn't circumvent
the protection in any form or shape!

The Cloudflare challenge is detected by analyzing the last response
header received from the server. If the HTTP Code is not 200 (OK)
and the server name contains 'cloudflare' ('Server: cloudflare'),
RSS-Bridge assumes the server responded with a challenge.

The header parsing is based on https://stackoverflow.com/a/18682872
2018-07-21 17:43:29 +02:00
LogMANOriginal
4facbf32e3 [InstructableBridge] Add new bridge (#724)
This commit adds a new bridge for http://www.instructables.com. This bridge
currently supports fetching content by category (all categories available 200+),
using available filters (featured, recent, popular, views, contest winners).
2018-07-21 15:25:13 +02:00
logmanoriginal
6bd76af326 [YoutubeBridge] Add duration limits for all modes
Adds duration limits (minimum duration, maximum duration) for all
modes (user/id/playlist/search). Duration limits are optional, so
existing subscriptions don't break.

The limits are specified by two separate parameters, each of which
is optional:

- `&duration_min=` (minimum duration in minutes, default: -1)
- `&duration_max=` (maximum duration in minutes, default: INF)

If duration limits are specified in either user, id or playlist mode,
the bridge defaults to fetching data from HTML intead of XML feeds,
which requires more bandwidth and takes longer, because each video is
loaded individually!

References #670
2018-07-21 14:33:07 +02:00
logmanoriginal
caa622ffec [search] Support searching by URI
Adds matching for URIs to the search bar, using the format
<scheme>://<host>/<path>

Searching by URI scheme is also supported:

"http://"  (returns all bridges with 'http'  scheme)
"https://" (returns all bridges with 'https' scheme)

The following examples are equivalent and will return both of the
Facebook bridges (FacebookBridge and FB2Bridge):

"https://www.facebook.com/facebook"
"https://www.facebook.com/facebook?..."
"https://www.facebook.com"
"http://www.facebook.com"
"https://facebook.com"
"http://facebook.com"
"facebook.com"
"facebook"

Notice: When the URI scheme is omitted, the search algorithm falls back
to regex matching. Searching for "www.facebook.com" doesn't work, as it
is missing the schema and doesn't match via regex!

Omitting the 'www.', however, does work. This was a design decision for
some bridges specify their URI with and others without 'www.'

A search term can still be specified in the browser URL using parameter
'q' => '?q=searchterm'.

References #743
2018-07-20 22:44:13 +02:00
teromene
c4d489f018 Add URI to ElloBridge elements. 2018-07-19 17:07:54 +02:00
logmanoriginal
6a98293fb3 [Configuration] Bump version to 2018-07-17 2018-07-17 20:44:01 +02:00
logmanoriginal
d79630e3b8 [Configuration] Remove check for allow_url_fopen
This commit follows the changes done in commits

fbf874cb29
ead7b2e8de
2018-07-17 20:39:14 +02:00
teromene
1f2fe25471 Fix LeBonCoinBridge, now uses getContents correctly, 2018-07-17 10:50:30 +02:00
Antoine Cadoret
87fc9e9156 fix LeBonCoin bridge (#747) 2018-07-16 20:13:08 +02:00
Nemo
c7b0c9fd31 Amazon Price Tracker Bridge (#741)
* [amazonprice] Adds AmazonPriceTracker bridge
2018-07-16 14:54:52 +02:00
Teromene
fbf874cb29 Update README.md
Remove allow_url_fopen requirement. This should no longer be necessary. Added requirement for curl.
2018-07-16 12:37:09 +02:00
Eugene Molotov
049ee52fb5 Implemented feed item categories (#746) 2018-07-16 12:32:24 +02:00
TheRadialActive
3f41d0593a Added RSS bridge for zenodo.org (#749)
* added RSS bridge for zenodo.org
2018-07-16 12:02:41 +02:00
sysadminstory
7126f5e838 [DealabsBridge] First version of the generic "Pepper" Bridge (#726)
* [DealabsBridge] First version of the generic "Pepper" Bridge
2018-07-13 00:35:13 +01:00
Nemo
ead7b2e8de [fb2] Switches to getContents (#742) 2018-07-10 02:29:47 +01:00
LogMANOriginal
0d80a19e84 [FacebookBridge] Add context for public Facebook groups (#739)
The previous context is now labeled 'User', while the new context is
labeled 'Group'. The existing code was not changed, instead new group*
functions were implemented to handle groups.

The general principle of capturing groups is the same as done for users
with adjustments to account for different HTML structures.

Captcha responses are currently not supported for groups! There doesn't
seem to be a way to trigger them consistently, which makes it hard to
handle them properly.

Features of the group context:

- The feed title is based on the group name
- The group URI used for capturing is returned for the feed URI
- Author names and timestamps are reproduced from the source
- Post titles are reproduced from the source if they exist, otherwise
the title is build manually from the author name and the content
- Original contents are included with the feed
- All images are attached as enclosures as well

Closes #
2018-07-08 17:16:00 +02:00
logmanoriginal
42c699f474 formats: Fix favicon not found if url contains path 2018-06-30 10:27:05 +02:00
logmanoriginal
2bc8daa101 [JustETFBridge] Add new bridge
Supports latest news and profiling a given ETF in Englisch, German
or Italian language. Cover images are attached as enclosures and not
as part of the content.

News:

Optionally loads the full article for each news item. Some articles
may include scripts to provide interactive graphs. These scripts are
removed as they would be rendered as pure text and a message is shown
instead: "[Content removed! Visit site to see full contents!]"

Profile:

Optionally includes the ETF strategy and description.
2018-06-30 10:27:05 +02:00
logmanoriginal
bca79d3f88 [KununuBridge] Fix broken page layout and sort reviews 2018-06-30 10:27:05 +02:00
logmanoriginal
90dc968fd1 Fix PHPCS error 2018-06-30 10:26:48 +02:00
Teromene
da6b98851c Add recuperation of the current version from git if available (#731)
* Add recuperation of the current version from git if available
* Include version when auto-reporting an error
2018-06-30 10:24:22 +02:00
teromene
71c29d4192 Fix phpcs for master. 2018-06-29 23:15:22 +01:00
LogMANOriginal
193ca87afa [phpcs] enforce single quotes (#732)
* [phpcs] Add rule to enforce single quoted strings
2018-06-29 22:55:33 +01:00
Nemo
5ea79ac1fc Add markdown support to Container Linux Feed (#730) 2018-06-28 20:54:42 +02:00
Teromene
937ea49271 Add basic authentication support (#728)
* Move configuration in its own class in order to reduce the verbosity of index.php
* Add authentication mechanism using HTTP auth
* Add a method to get the config parameters
* Remove the installation checks from the index page
* Log all failed authentication attempts
2018-06-27 19:09:41 +02:00
logmanoriginal
95686b803c [IsoHuntBridge] Remove bridge
isoHunt has discontinued services due to legal reasons and is now
accessible via https://isohunts.to

While it is certainly possible to rewrite the bridge to fetch some
information from the new site, it wouldn't be able to provide as
much functionality as before. This is due to isoHunt having removed
all searching and filtering options, only providing static HTML pages
for general categories (anime, movies, etc...). Those pages, however,
are heavily broken.

Unless someone is interested in monitoring the general categories
the effort of upgrading the bridge to the new site is not worth taking
time for.

Users of isoHunt are asked to make use of their client application,
as they don't provide online services anymore (it's now in the darknet)

Here is the statement from isoHunt:

"Due to hard regulations and security issues for bittorrent users, we
have moved into a more secure and even faster district of the internet!

[...]

Torrent Downloads have a high risk of getting legal problems. That is
why we do not offer torrentfiles any more. [...]"

-- source: https://isohunts.to
2018-06-24 18:33:50 +02:00
logmanoriginal
5087f5f79e [FacebookBridge] Support facebook links as user name
Allows users to paste facebook links as user name. The link must contain
the correct host (www.facebook.com) and a valid path (/user-name/...).
The first part of the path is used for the user name. Errors are returned
in case something went wrong.

References #706
2018-06-24 11:14:08 +02:00
logmanoriginal
4a5f190e0e [FacebookBridge] Add option to skip reviews
Reviews are provided the same way as summary posts and therefore returned
as separate feed item for each review. This commit adds a new option
'&skip_reviews=on' to skip reviews entirely.

References #706
2018-06-24 10:52:22 +02:00
logmanoriginal
01a2746715 [YoutubeBridge] Fix sniff violation
This is a fix for a sniff violation not detected by newer versions
of phpcs (not sure why though, it's detected in version 2.7.1).
2018-06-23 21:28:30 +02:00
Nemo
f4a60c1777 Add dockerfile to create an official docker image (#720) 2018-06-23 16:51:48 +02:00
sysadminstory
1b08bce779 [DealabsBridge] Follow site changes (#721)
- Changed some CSS class to follow the website changes (again)
2018-06-21 13:14:59 +01:00
Nemo
9fa74a36c6 Adds Container Linux releases RSS Feed (#718)
* Adds Container Linux releases RSS Feed
2018-06-19 19:39:08 +01:00
Corentin Garcia
7493e2b5b8 [GrandComicsDatabaseBridge] Add bridge (#717)
closes #709
2018-06-15 21:09:09 +02:00
Corentin Garcia
8e468a9ca7 [SuperSmashBlogBridge] Added bridge (#716) 2018-06-15 21:05:31 +02:00
Joe Digilio
50924b9213 Abort on parse error of config.default.ini.php (#714)
If there is an error parsing the default config file, then abort.
2018-06-15 21:02:06 +02:00
logmanoriginal
4c5013bc82 [index] Bump release version to 2018-06-10 2018-06-10 22:14:58 +02:00
Eugene Molotov
7dc09db9ca [VkBridge] More beatifications and fixes (#712)
* Add one more selector for article_author_selector
* Extend video parsing
* Add poll parsing
2018-06-10 22:09:50 +02:00
hunhejj
d92da8f0f7 Add cUrl error message and code to the debugMessage (#711) 2018-06-10 22:08:45 +02:00
logmanoriginal
064ba456e8 [InstagramBridge] Fix broken compatibility for media_type parameter
The media_type parameter was recently replaced by media_type_u (for
user mode) and media_type_h (for hashtag mode). This was necessary
in order to add the media type 'story' only for the user mode.

"The reason for that is that RSS-Bridge supports multiple parameters
with the same name if and only if they contain the exact same value.
Here, hashtags don't have stories, so it would not be possible to
pass "story" as a parameter. This is a design mistake that I made
when I added support for hashtags."

-- 8770c87389 (r28871502)

However as pointed out this change breaks existing feeds as the
parameter name is no longer compatible to previous implementations.

This commit changes the implementation to provide the old media_type
parameter globally and check for invalid options on each request. If
a user uses the 'story' option in history mode the bridge returns a
client error.

references 8770c87
references #694
fixes #696
fixes #699
fixes #701
2018-05-29 12:52:31 +02:00
LogMANOriginal
8ac8e08abf Add user config (#653)
Uses the parse_ini_file function to load default settings from the default configuration file 'config.default.ini.php'. Optionally loads custom settings from 'config.ini.php' to replace the default
values.
2018-05-29 11:52:17 +02:00
rogerdc
c4f32c31a8 Add ChristianDailyReporterBridge (#697) 2018-05-29 11:28:22 +02:00
Eugene Molotov
4369e077c2 [VkBridge] Fixed image src link generating for photo (#700) 2018-05-29 11:01:54 +02:00
sysadminstory
1045850043 [DealabsBridge] Follow site changes, fix unhandled case (#703)
* [DealabsBridge] Follow site changes, fix unhandled case

- Fixed the case where no discount was shown
- Changed some CSS class to follow the website changes
2018-05-29 10:52:13 +02:00
teromene
2d8f4dc3c5 Fix space in URL resulting in API errors. 2018-05-05 18:10:19 +01:00
teromene
779b638fb4 Added ElloBridge. Closes #683 2018-05-05 18:06:27 +01:00
teromene
3ca59392c2 Fix for crashes when accessing FileCache in case it has been purged/not created yet. 2018-05-05 18:05:48 +01:00
teromene
9b34b68180 Do not use an external service in order to fetch the favicon. 2018-05-05 13:55:38 +01:00
teromene
79ebdc4b39 Warn the user when trying to fetch a non-public facebook page. 2018-05-05 13:49:49 +01:00
teromene
8770c87389 Added support for stories in InstagramBridge. Closes #665
Renamed parameters as stories are only available in user mode.
Use a regex instead of HTML parsing to extract the JSON, as it is way faster.
2018-05-05 13:00:59 +01:00
Eugene Molotov
c1e3352218 [VkBridge] Extended article link parsing (#685)
* [VkBridge] Extended article link parsing
2018-05-05 12:03:54 +02:00
Grégory T
00570ce1b4 [ETTVBridge] New bridge, first push (#680)
* [ETTVBridge] New bridge
2018-04-30 23:18:39 +02:00
teromene
df33dcff4e [YGGTorrentBridge] URL encode the first parts of the requests. 2018-04-26 22:57:18 +01:00
Nicolas Delsaux
e60b5ab193 Mise à jour du bridge pour WorldOfTanks (#527)
* Mise à jour de l'un de mes bridges fétiches
2018-04-22 12:58:07 +02:00
teromene
b0c7a62f74 [index] Bumped version to 2018-04-20 2018-04-20 17:15:25 +02:00
teromene
57b15a089e Added DiscogsBridge. Closes #615 2018-04-20 16:57:09 +02:00
teromene
4b7fbe4188 DansTonChatBridge: test before accessing plaintext 2018-04-19 21:00:18 +02:00
Teromene
2390fb58b3 Merge pull request #673 from GregThib/patch-1
DansTonChatBridge: Update to follow DTC website changes
2018-04-19 20:58:01 +02:00
Teromene
1e8d29f6ec Merge pull request #672 from em92/patch-3
[YoutubeBridge] Removed duration in titles on search mode
2018-04-19 20:56:34 +02:00
Eugene Molotov
644d13686c [YoutubeBridge] Removed duration in titles on search mode 2018-04-19 09:03:29 +05:00
teromene
aa0ff1c9b1 Added YGGTorrentBridge. 2018-04-18 21:57:27 +02:00
teromene
539d9f1f06 Add SupInfoBridge, fixes #668 2018-04-18 12:39:45 +02:00
teromene
5ece801ce7 Fix h* display size in HtmlFormat, and fix images being wider than the page. 2018-04-18 12:29:22 +02:00
GregThib
4dcea6d9c9 Update to follow DTC website changes
Now, entry title is optionnal and may be found in h3 HTML element.
Entry content is mandatory and may be found in div[class="item-content"] HTML element.

Moreover, the title may contain simple quotes (here, encoded) so the bridge have to decode first to apply format library function. In case we don't do that, the format function double encode the quote and something like &amp;#039; could appear.
2018-04-18 12:00:00 +02:00
teromene
d69e2521f1 Removed T411 bridge. Website was closed nearly one year ago. 2018-04-18 11:44:54 +02:00
teromene
7927d73719 Rewrote DemonoidBridge. Fixes #626. 2018-04-17 15:25:02 +02:00
teromene
0620f30ae0 Changed the API key used for SoundCloud bridge. Should fix #599 2018-04-17 14:24:00 +02:00
teromene
795494cfce Added enclosures to InstagramBridge. 2018-04-16 19:34:21 +02:00
teromene
ba8542156c Remove usage of function file_get_contents. 2018-04-16 19:27:20 +02:00
Eugene Molotov
55f112e034 [VkBridge] Rewrited bridge code (#667)
* [VkBridge] Convert special HTML entities to characters in pageName

* [VkBridge] Generate feed item title

* [VkBridge] Remove double backslashes in feed item link

* [VkBridge] Unpin post if pinned

* [VkBridge] Mark reposted messages

* [VkBridge] Correct external link parsing

* [VkBridge] Added article parsing

* [VkBridge] Added video parsing

* [VkBridge] Added photo parsing

* [VkBridge] Added album link parsing

* [VkBridge] Added one more external link selector

* [VkBridge] Using array of link selectors to remove

* [VkBridge] Added document parsing

* [VkBridge] Added sign parsing

* [VkBridge] Fixed incorrect sorting with pinned item

* [VkBridge] More methods to parse documents

* [VkBridge] Save fallback if page name element not found

* [VkBridge] Using post signed as feed item author

* [VkBridge] Fixed document link

* [VkBridge] Coding policy fixes
2018-04-16 10:55:31 +01:00
Mitsukarenai
208fff801d [FDroid] minor fixes for Travis CI 2018-04-15 13:21:48 +02:00
Mitsukarenai
3c9860de43 [FDroid] new bridge 2018-04-15 13:13:10 +02:00
Adam Tygart
a16ec196c5 [NotAlways] Add a bridge for the NotAlways family of sites (#537)
NotAlways right found it necessary to remove their RSS feeds recently. This is a *simple* bridge to grab the ones on the front page. It allows you to filter the articles based on their classification (right, working, romantic, related, learning, friendly, hopeless, unfiltered, or all).
2018-04-15 12:02:37 +01:00
teromene
887fc7b037 Fix GoComics, website completely changed. Fixes #663 2018-04-14 18:15:44 +01:00
teromene
1bd4a40f71 Added GNOME Builder configuration to gitignore. 2018-04-14 18:15:13 +01:00
teromene
494169f959 Added bridge for Pixiv.
This bridge is slow, as caching of images is required (REFERER header required to access the full size images)
2018-04-14 16:19:35 +01:00
logmanoriginal
178177e787 [index] Push version to 2018-04-06 2018-04-06 22:45:33 +02:00
logmanoriginal
1cb83ccea3 [IPBBridge] Use limit for the number of items
The limit was used to specify the number of pages to return from a given
topic which resulted in the number of returned items variing between one
and however many entries are listed on one page.

This commit changes the implementation for the limit to keep loading more
pages until the specified limit is reached. Excessive elements are removed
in order to return the exact amount of items specified by the limit.

This behavior is closer to how other bridges are implemented and makes it
more natural to use without being too confusing. Existing queries must be
updated to account for the new limit.

References #657
2018-04-06 22:25:49 +02:00
sysadminstory
c899399569 [DealabsBridge] Follow the website changes (#660) 2018-04-06 21:25:41 +02:00
LogMANOriginal
0f93370e92 Merge pull request #654 from LogMANOriginal/cURL
Use cURL instead of file_get_contents
2018-04-06 20:49:58 +02:00
logmanoriginal
45c3dcb636 [VkBridge] Simplify header specification 2018-04-06 20:42:19 +02:00
logmanoriginal
ecfc220b10 [KernelBugTrackerBridge] Fix too many parameters requesting HTML DOM 2018-04-06 20:42:19 +02:00
logmanoriginal
4b3efed7ec [YoutubeBridge] Fix too many parameters when using HTML mode 2018-04-06 20:42:19 +02:00
logmanoriginal
bc28c5da8e [contents] Set CURLOPT_HTTPHEADER only if the provided array contains data 2018-04-06 20:42:19 +02:00
logmanoriginal
5bd9c1611d [contents] Limit cURL protocols to HTTP and HTTPS 2018-04-06 20:42:19 +02:00
logmanoriginal
6caca4946b bridges: Fix bridges with custom headers and options
This commit fixes bridges which called getContents, getSimpleHTMLDOM
or getSimpleHTMLDOMCached with custom settings.
2018-04-06 20:42:19 +02:00
logmanoriginal
ee78e7613f [contents] Replace file_get_contents by cURL
cURL is a powerful library specifically designed to connect to many
different types of servers with different types of protocols. For
more detailed information refer to the PHP cURL manual:

- http://php.net/manual/en/book.curl.php

Due to this change some parameters for the getContents function were
necessary (also applies to getSimpleHTMLDOM and getSimpleHTMLDOMCached):

> $use_include_path removed

  This parameter has never been used and doesn't even make sense in
  this context; If set to true file_get_contents would also search
  for files in the include_path (specified in php.ini).

> $context replaced by $header and $opts

  The $context parameter allowed for customization of the request in
  order to change how file_get_contents would acquire the data (i.e.
  using POST instead of GET, sending custom header, etc...)

  cURL also provides facilities to specify custom headers and change
  how it communicates to severs. cURL, however, is much more advanced.

  - $header is an optional parameter (empty by default). It receives
    an array of strings to send in the HTTP request header.

    See 'CURLOPT_HTTPHEADER':

    "An array of HTTP header fields to set, in the format
    array('Content-type: text/plain', 'Content-length: 100')"

    - php.net/manual/en/function.curl-setopt.php

  - $opts is an optional parameter (empty by default). It receives
    an array of options, where each option is a key-value-pair of
    a cURL option (CURLOPT_*) and it's associated parameter. This
    parameter accepts any of the CURLOPT_* settings.

    Example (sending POST instead of GET):

    $opts = array(
      CURLOPT_POST => 1,
      CURLOPT_POSTFIELDS => '&action=none'
    );
    $html = getContents($url, array(), $opts);

    Refer to the cURL setopt manual for more information:
    - php.net/manual/en/function.curl-setopt.php

> $offset and $maxlen removed

  These options were supported by file_get_contents, but there doesn't
  seem to be an equivalent in cURL. Since no caller uses them they are
  safe to remove.

Compressed data / Encoding

  By using cURL instead of file_get_contents RSS-Bridge no longer has
  to handle compressed data manually.

  See 'CURLOPT_ENCODING':

  "[...] Supported encodings are "identity", "deflate", and "gzip".
  If an empty string, "", is set, a header containing all supported
  encoding types is sent."

  - http://php.net/manual/en/function.curl-setopt.php

  Notice: By default all encoding types are accepted (""). This can
  be changed by setting a custom option via $opts.

    Example:

    $opts = array(CURLOPT_ENCODING => 'gzip');
    $html = getContents($url, array(), $opts);

Proxy

The proxy implementation should still work, but there doesn't seem
to be an equivalent for 'request_fulluri = true'. To my understanding
this isn't an issue because cURL knows how to handle proxy communication.
2018-04-06 20:42:19 +02:00
logmanoriginal
2df2623430 [index] Add 'curl' extension check 2018-04-06 20:42:19 +02:00
logmanoriginal
de5f850cdb [index] Fix indentation using tabs 2018-04-06 20:34:44 +02:00
teromene
ac6847045c Catch Errors in order to display a message in more cases. We also catch Exceptions to maintain compat with php 5.
Add check for simplexml extension.
2018-04-04 19:02:40 +01:00
logmanoriginal
df6da837dc [FacebookBridge] Return error if username starts with slash
Requesting a username with a leading slash would cause error 500
because the requested URI would contain two slashes in a row.

For example username "/test" would result in:
https://facebook.com//test

References #628
2018-03-23 21:23:30 +01:00
Eugene Molotov
41b7984a4e [YoutubeBridge] Playlist mode: faster feed generating if item count is less or equal to 15 (#648)
* [YoutubeBridge] Playlist mode: faster feed generating if item count is less or equal to 15
2018-03-19 12:41:52 +00:00
teromene
38c7e0272e Add hashtag support to InstagramBridge.
Fixes  #629
2018-03-19 12:29:24 +00:00
teromene
29c690dbcd Fix InstagramBridge, thanks to @pintassilgo comments.
Fixes #646
2018-03-19 12:17:42 +00:00
LogMANOriginal
8ba817478b Implement customizable cache timeout (#641)
* [BridgeAbstract] Implement customizable cache timeout

The customizable cache timeout is used instead of the default cache
timeout (CACHE_TIMEOUT) if specified by the caller.

* [index] Add new global parameter '_cache_timeout'

The _cache_timeout parameter is an optional parameter that can be
used to specify a custom cache timeout. This option is enabled by
default.

It can be disabled using the named constant 'CUSTOM_CACHE_TIMEOUT'
which supports two states:

> true: Enabled (default)
> false: Disabled

* [BridgeAbstract] Change scope of 'getCacheTimeout' to public

* [html] Add cache timeout parameter to all bridges

The timeout parameter only shows if CUSTOM_CACHE_TIMEOUT has been set
to true. The default value is automatically set to the value specified
in the bridge.

* [index] Disable custom cache timeout by default
2018-03-14 18:06:36 +01:00
Eugene Molotov
cacbe90102 [YoutubeBridge] Sort playlist items by publication date (#643) 2018-03-13 11:24:40 +00:00
Antoine Cadoret
cb91cd5d2f Fix SteamBridge (#637) (#639)
Fixes #639
2018-03-12 09:22:34 +00:00
sysadminstory
52dfa3fe76 [RadioMelodieBridge] Add new bridge (#640) 2018-03-11 15:38:07 +01:00
logmanoriginal
29a1c7ac09 [index.php] Add extension check for 'mbstring'
The mbstring extension is required by all formats in order to convert multi-
byte characters to UTF-8. This commit adds an extension check to throw an
error message if the extension is not enabled.
2018-03-07 19:11:47 +01:00
teromene
6eea51eeeb Fix SteamBridge.
Fixes #636
2018-03-07 10:24:33 +00:00
teromene
2149af0e74 Fix Pinterest bridge, remove the old JSON parsing, and return original sized image.
Fixes #632
2018-03-06 12:01:48 +00:00
teromene
142a647b7a Merge branch 'master' of github.com:RSS-Bridge/rss-bridge 2018-03-06 11:27:37 +00:00
teromene
6e916ddd35 Fix Arte7Bridge.
Fixes #633
2018-03-06 11:26:16 +00:00
Eugene Molotov
159b00145d [VkBridge] Setting feed title (#635)
* [VkBridge] Setting feed title
2018-03-05 09:46:15 +00:00
Mitsukarenai
26ce16baa2 [PlanetLibre] remove bridge (origin now has RSS) 2018-03-03 21:04:40 +01:00
sysadminstory
0622fe142b Dealabs : Added Groupes Feeds and Feed name is set according to parameters (#630)
* [DealabsBride] Added Groupes Feeds
2018-03-01 17:10:34 +00:00
logmanoriginal
4805b52d42 [YoutubeBridge] Fix typo 2018-02-16 22:35:00 +01:00
logmanoriginal
962617086e [YoutubeBridge] Remove superfluous div selectors 2018-02-16 22:31:47 +01:00
logmanoriginal
4f6277b6b5 [YoutubeBridge] Fix parsing author name breaks the bridge
The author name is parsed by searching a string within the entire
HTML document:

$author = $html->innertext;
$author = substr($author, strpos($author, '"author=') + 8);
$author = substr($author, 0, strpos($author, '\u0026'));

This solution will return big portions of the HTML document if
the strpos function returns zero (not found).

This commit replaces the previous implementation by searching for
a specific script tag and making use of the JSON data inside it.

References #580
2018-02-16 22:31:29 +01:00
logmanoriginal
5aaab9eb8c [YoutubeBridge] Skip unavailable videos 2018-02-16 22:11:03 +01:00
sysadminstory
ef402bb5c3 [DealabsBride] Fix for the new site (#595)
* [DealabsBride] Fix for the new site
2018-02-14 11:03:44 +00:00
LogMANOriginal
85ac9001d6 [IPBBridge] Add bridge (#564)
This bridge returns feeds for any URI that is compatible with the
IPB implementation (currently 4.x). Older versions might work, but
there is no guarantee.

Only forum and topic URIs are supported!

The bridge automatically checks if natural feeds are available (by
adding '.xml' to the URI). If so the feed is returned. Otherwise
the bridge will attempt to identify the content type and build a
feed accordingly.

Valid URIs are forums and topics. For forums the first page is
returned, for topics the last one. Elements are ordered such that
the latest entry is returned first (oldest-to-newest)

The optional parameter '&limit=' specifies how many pages should
be loaded (default: 1). Topics are loaded in reverse order.
=> Does not work with forums!

Images are provided as enclosures and scaled to a max-size of
400x400 pixels by default (Except for natural feeds).

The content is filtered before being returned:
- Unnecessary tags are removed (iframes, etc...)
- Styles for blockquotes are restored (grey background)

Closes #507
2018-02-13 21:46:33 +01:00
Mitsukarenai
7939bffcdd fix: TébéoBridge Travis cleanup 2018-02-11 19:08:19 +01:00
Mitsukarenai
bb58aa8e31 New bridge: Tébéo 2018-02-11 16:56:34 +01:00
Ruslan
1d35149191 Update VkBridge (#625) 2018-01-30 16:57:07 +00:00
Tameroski
be03764029 Fixing double quote issue at the end of URL (#623) 2018-01-23 11:27:45 +00:00
Matt DeMoss
a07874d468 Initial commit for Bloomberg bridge with top stories and search (#607)
* initial commit for Bloomberg bridge with top stories and search
2018-01-12 12:08:15 +00:00
Matt DeMoss
90d7ae8776 Fix twitter list filter test #613, fix and change getName() for lists. (#614) 2018-01-12 12:07:40 +00:00
Teromene
93e0562353 Merge pull request #610 from mdemoss/YouTubeTitle-#609
You tube title fix for #609
2018-01-11 12:09:38 +00:00
Teromene
4c5d547d9c Merge pull request #608 from mdemoss/PcGamerBridge
Pc gamer bridge
2018-01-11 12:08:10 +00:00
Teromene
9a3a64010f Merge pull request #620 from RSS-Bridge/teromene-patch-2
Update MixCloudBridge.php
2018-01-11 11:48:29 +00:00
Teromene
e59a6f4c9e Update MixCloudBridge.php
Fix whitespace at start of line
2018-01-11 11:44:51 +00:00
Teromene
1506e68587 Merge pull request #619 from RSS-Bridge/teromene-patch-1
Update .travis.yml
2018-01-11 11:44:37 +00:00
Teromene
671cba4f68 Update .travis.yml
Try to fix build failure
2018-01-11 11:41:25 +00:00
Teromene
374eb8f4bf Merge pull request #617 from adamchainz/patch-1
README - sort lists alphabetically
2018-01-10 14:05:05 +00:00
Adam Johnson
60f7a2b3e4 README - sort lists alphabetically
This makes them easier to scan and check "does rss-bridge support service X I'm interested in?" :)
2018-01-10 11:45:55 +00:00
Teromene
7744172c63 Merge pull request #616 from lalannev/patch-1
Update LegifranceJOBridge.php
2018-01-09 17:19:28 +00:00
lalannev
5a763aee8d Update LegifranceJOBridge.php 2018-01-09 14:57:17 +01:00
Matt DeMoss
c14b2c6905 address phpcs style errors 2017-12-28 20:20:24 -05:00
Matt DeMoss
0871376922 store feed name in new variable, switch getName on queriedContext, remove 'bridge' from name for feeds, fixes #609 2017-12-28 20:20:24 -05:00
Matt DeMoss
c5fe9a6dc0 mark places where a new variable is needed 2017-12-28 20:20:24 -05:00
Matt DeMoss
fbbcd02384 apply phpcbf for automatic style fixes 2017-12-24 16:45:56 -05:00
Matt DeMoss
d34987f9c1 PC Gamer bridge initial commit with most read stories 2017-12-24 16:40:59 -05:00
Teromene
9e0565c655 Merge pull request #604 from TwizzyDizzy/master
Fix double forward-slash in returned post URI leading to 404
2017-12-14 16:43:33 +00:00
Thomas Dalichow
443081c90b Fix double forward-slash in returned post URI leading to 404 2017-12-06 22:17:46 +01:00
Teromene
03fc09e3c6 Merge pull request #602 from TwizzyDizzy/master
Fake user agent as Mixcloud blocks certain User-Agents
2017-12-01 17:29:26 +00:00
Thomas Dalichow
45323c2b2f Fake user agent as Mixcloud blocks certain User-Agents 2017-12-01 17:28:57 +01:00
Teromene
67ee73782c Merge pull request #582 from sysadminstory/master
[DealabsBridge] Add new bridge
2017-10-18 10:53:46 +01:00
sysadminstory
2bb9a29ddc Delete usefull whitespace 2017-10-17 23:37:09 +02:00
sysadminstory
5cbd363597 Coding style fix
Fixed the bridge to follow the project coding style
2017-10-17 23:30:27 +02:00
Teromene
aa6ded0ea4 Merge pull request #593 from b1nj/master
Update saison AllocineFRBridge
2017-10-17 19:02:54 +01:00
sysadminstory
3c61dc2b57 Merge remote-tracking branch 'upstream/master' 2017-10-17 14:53:22 +02:00
B1nj
3e528ddccf Update saisons AllocineFRBridge 2017-10-16 22:24:49 -04:00
teromene
cba65d6d08 [Arte7Bridge] Fix Arte7 bridge, use the API 2017-10-12 18:12:31 +01:00
Teromene
8d418611a2 Merge pull request #589 from mickael-bertrand/patch-2
Updater torrent9 URI
2017-10-12 17:18:04 +01:00
teromene
98b0f0f8ba [Core] Verify the presence of the array keys before accessing them.
Fixes  #588
2017-10-12 17:14:34 +01:00
Teromene
6f66e6d9be Merge pull request #592 from ldidry/fix-gocomics
Update GoComicsBridge
2017-10-11 11:34:37 +01:00
Luc Didry
8b06299bad Update GoComicsBridge 2017-10-11 10:03:29 +02:00
MickaëlBERTRAND
5a99981827 Updater torrent9 URI 2017-10-08 19:21:10 +02:00
Teromene
e30ad3feb4 Add support for running rss-bridge from the CLI 2017-09-25 19:14:02 +02:00
logmanoriginal
77657a9154 [.travis.yml] Refactor script 2017-09-24 18:45:58 +02:00
logmanoriginal
3059b1ea80 [YoutubeBridge] Skip Ads
The search might return unrelated videos (Ads) that are inserted
between regular search results. This adds a check to skip Ads.

Closes #571
2017-09-24 17:25:47 +02:00
LogMANOriginal
4037c34393 [TwitterBridge] Add category for lists (#545)
This adds a new option to generate feeds from Twitter lists using
an optional filter (string comparison).
2017-09-24 16:59:45 +02:00
logmanoriginal
e671a2ad02 [.travis.yml] Fix configuration to work with Ubuntu Trusty
Travis CI upgraded the linux build environment from Ubuntu Precise
to Ubuntu Trusty with Trusty becoming the default build environment
as of August 2017:

  https://docs.travis-ci.com/user/reference/overview/

A bug in the configuration of the Ubuntu Trusty distro causes all
builds except nightly to fail. The PHP include_path is set to

  include_path='.:/home/travis/.phpenv/versions/5.6.31/share/pear'

instead of

  include_path='.:/home/travis/.phpenv/versions/5.6.31/lib/php/pear'

which causes phpcs to fail because it cannot resolve import paths.

This commit adds a hotfix to .travis.yml that circumvents the
issue by overwriting the include_path during initialization. This
hotfix should be removed once a solution is found.

This bug is tracked via
https://github.com/travis-ci/travis-ci/issues/8487
2017-09-23 20:48:28 +02:00
logmanoriginal
1ea091f215 [.travis.yml] Fix Tavis CI build error
Travis CI recently updated the default distribution from Ubuntu
Precise to Ubuntu Trusty, which causes all builds except nightly
to fail.

For unknown reasons phpcs is unable to locate "PHP/CodeSniffer/
autoload.php" causing it to fail with a fatal error

The root cause of the failure is unknown. We explicitly return to
the previous build system (Ubuntu Precise) for builds to work again.

See the migration guide for reference:
https://docs.travis-ci.com/user/precise-to-trusty-migration-guide/

Notice: Ubuntu Precise is retired as of September 2017 and will be
decommissioned in the near future:
https://docs.travis-ci.com/user/reference/overview/
2017-09-23 18:38:47 +02:00
logmanoriginal
87fa4ae3ac [.travis.yml] Fix warning channel pear.php.net has updated its protocol 2017-09-23 18:05:09 +02:00
sysadminstory
d7a1dca004 [DealabsBridge] Conform to coding policy
- If no there are no results, an explicit message is now returned
- Commas are now following the coding policy
- Lines are no longer more than 80 chars when possible
2017-09-19 02:08:22 +02:00
sysadminstory
fe48340327 [DealabsBridge] Add new bridge 2017-09-05 21:03:21 +02:00
logmanoriginal
b4c6aa41a7 [index] Return error if no format is specified when requesting a bridge 2017-08-28 20:45:06 +02:00
metaMMA
1696aee212 [DemonoidBridge] Add new bridge 2017-08-28 20:00:52 +02:00
metaMMA
585379d47a [ThePirateBayBridge] Add instructions
Added additional instructions for: 'username search' and 'category
search' next to instructions for 'keyword search'.

Changed variable name from underscore to camelCase.
2017-08-28 20:00:00 +02:00
logmanoriginal
2595b5d7d8 [index] Bump version 2017-08-19 21:09:48 +02:00
logmanoriginal
f858adc884 [CHANGELOG] Add 2017-08-19 2017-08-19 21:08:44 +02:00
logmanoriginal
44e135ce1e [CHANGELOG] Fix layout 2017-08-19 20:12:17 +02:00
logmanoriginal
9a9ce30b16 [YoutubeBridge] Fix issues loading playlists
Videos that are part of a playlist have the playlist ID encoded in
the URI. When loading the video info the page contents change unex-
pectedly due to the playlist being part of the page.

This removes any trailing parameters from the video ID in order to
ensure only pure videos are loaded at all times.
2017-08-19 18:51:30 +02:00
logmanoriginal
0e2b80d5d7 [YoutubeBridge] Fix error on certain keywords
References #569
2017-08-17 19:26:04 +02:00
logmanoriginal
1b1ab6a66e [validation] Fix error on undefined optional numeric value
Providing no value for an optional numeric parameter results in
error "Parameter *** is invalid!"

This is caused by the validation function ignoring the 'required'
attribute when loading and checking input parameters.

This commit adds checks to determine whether the 'required' attri-
bute is defined and active before returning the error message.

References #570:
2017-08-17 19:02:50 +02:00
mcbyte-it
0284e9d488 [GoComicsBridge] Fix for page structure changes (#568)
GoComics changed comic page structure, so this patch fixes it

Closes #565
2017-08-17 18:35:41 +02:00
logmanoriginal
f91309c7e4 [index] Use constant WHITELIST_FILE all the way 2017-08-12 19:15:16 +02:00
logmanoriginal
cd012e115b [index] Show bridge options when loading with URL fragment
Loading the page with an URL fragement (#bridge-*) should result in
the bridge showing all parameters by default. Unfortunately this is
not possible using PHP, which is why a new JavaScript function is
needed (select.js)

That way, when returning from a bridge ('back to rss-bridge') will
keep the selected bridge active (only works for HTML format).
2017-08-11 19:39:16 +02:00
logmanoriginal
df9e3968dc [index] Add GET parameter 'q' for search queries 2017-08-11 17:43:15 +02:00
logmanoriginal
c237eaa254 [style] Fix All input boxes are center aligned
f757d7d1a5 introduced a bug where all
text input boxes were centered instead of just the search bar.

In order for this to work properly the global styles must be applied
before specific styles for the search bar.
2017-08-10 20:27:27 +02:00
logmanoriginal
f757d7d1a5 [style] Center search cursor and hide placeholder
The search bar doesn't feel right if the placeholder is centered,
while the text cursor is left-aligned. The cursor should appear
instead of the placeholder (at the same position).

Added styles to center the text cursor and hide the placeholder
when selecting the input field.

Tested in:
 - Firefox 54 & 55
 - Chromium 60 (compatible with Chrome 60)
 - Microsoft Edge (partially working!)

--- Microsoft Edge ---

Due to a bug in the Microsoft Edge browser, the text cursor is not
centered as long as the placeholder is defined (which it is always)

More information:
  https://stackoverflow.com/a/33224868

Official bug report:
  https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4468563/

----------------------
2017-08-10 14:35:09 +02:00
logmanoriginal
4fb1366aaf [FeedExpander] Fix Serialization of 'SimpleXMLElement' is not allowed 2017-08-10 13:35:19 +02:00
logmanoriginal
8166e33e7f [FeedExpander] Remove whitespace from source content
Whitespace at the beginning of feeds causes parsing errors. This is
an example using an ill-formatted RSS feed:

   "XML or text declaration not at start of entity"
-- https://validator.w3.org

This commit automatically removes all proceeding and trailing white-
space from the source content before resume parsing.
2017-08-10 13:20:35 +02:00
Quentin de Longraye
ff3b1c9eb2 [DribbbleBridge] Add dribble bridge listing last dribble popular shots (#558) 2017-08-06 20:29:21 +02:00
logmanoriginal
4924769549 [validation] Remove superfluous if-statement 2017-08-06 13:45:24 +02:00
logmanoriginal
e4fa963bdf [validation] Return null on invalid number 2017-08-06 13:43:23 +02:00
logmanoriginal
54e8bb2228 [VineBridge] Remove bridge
On Oct 27, 2016 the discontinuation of Vine was announced:
https://medium.com/@vine/important-news-about-vine-909c5f4ae7a7

"Today, we are sharing the news that in the coming months we’ll be
discontinuing the mobile app."

https://vine.co/ is still online, but has been put into an archive
indefinitely. As the site does not allow further uploads, this
bridge serves no further purpose.
2017-08-06 13:03:10 +02:00
logmanoriginal
99e7e7876e exception: Use built-in HTTP response codes
PHP >= 5.4 provides a built-in function to generate valid HTTP
error header including the error description: http_response_code()

See: http://php.net/manual/en/function.http-response-code.php
See also: https://stackoverflow.com/a/12018482

This commit removes the '\Http' utility class and replaces all
calls to 'Http::getMessageForCode()' by 'http_response_code()'
2017-08-06 12:55:11 +02:00
logmanoriginal
62c190d841 [Bridge] Remove superfuous variables and statements 2017-08-06 00:04:07 +02:00
logmanoriginal
84d2c02a09 whitelist: Do case-insensitive whitelist matching
Matching whitelisted bridges using a case-insensitive match makes
sense for following reasons:

- Wrong upper/lower case spelling in the whitelist is not easily
discovered. Example: Misspelling 'Youtube' as 'YouTube' will not
show the 'Youtube' bridge (while it is expected to show)

- Two bridges with the same name but different letter casing are
discouraged to prevent confusion and keep the project compatible
with Windows machines
2017-08-06 00:01:32 +02:00
logmanoriginal
fc0ae42450 [GelbooruBridge] Fix bridge not getting tags correctly
Tags are embedded in the 'title' attribute instead of 'alt' as
defined by the ancestor (DanbooruBridge).

The 'title' attribute also contains statistics data ('score:...',
'rating:...') that is now filtered by a custom implementation of
the 'getTags' function (elements that contain a colon are removed)

Closes #560
2017-08-05 22:38:24 +02:00
logmanoriginal
9599f921a5 [DanbooruBridge] Allow descendant classes to override tag collection
Add protected function 'getTags' that receives the current element
and returns a string containing all tags.

References #560
2017-08-05 22:36:14 +02:00
logmanoriginal
e125e9aba1 [LeBonCoinBridge] Fix bridge is marked executable
Closes #561
2017-08-05 22:00:58 +02:00
Pierre Mazière
55a77c734d [LWNprevBridge] Fix everchanging url
Signed-off-by: Pierre Mazière <pierre.maziere@gmx.com>

Closes #563
2017-08-05 15:56:35 +02:00
logmanoriginal
ccd8af09b9 [index] Use single quotes instead of double quotes 2017-08-05 15:46:16 +02:00
logmanoriginal
f2d02a4187 [index] Simplify debug mode detection
This removes superfluous variables and if-statements when checking
whether the debug mode is active or not.
2017-08-05 15:43:48 +02:00
logmanoriginal
f19d34a5a1 [index] Check permissions for cache folder and whitelist file
* The cache folder requires write permissions at all times
* The whitelist file requires write permissions if it does not
exist (can be created manually)
2017-08-05 15:23:30 +02:00
logmanoriginal
f1534c91e2 [index] Use constant instead of variable for the whitelist file path
Like the cache folder the whitelist file is assumed static and thus
should be defined as constant.
2017-08-05 15:23:08 +02:00
logmanoriginal
cbda060b86 [FacebookBridge] Fix &amp; in URLs
All formats except HTML return &amp; instead of & in URLs causing
all links with parameters (...&id=...) to break.

Facebook does not return valid HTML URIs but instead provides them
with all special characters encoded (like using htmlspecialchars).
This seems to be related to the page being build almost entirely of
script blocks.

This commit adds htmlspecialchars_decode() to URI and content to
reverse the encoding.

References #550
2017-08-04 21:12:48 +02:00
114 changed files with 8765 additions and 2241 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
.git
cache/*
DEBUG
Dockerfile
whitelist.txt
phpcs.xml
CHANGELOG.md
CONTRIBUTING.md

6
.gitignore vendored
View File

@@ -227,8 +227,12 @@ pip-log.txt
/cache
/whitelist.txt
DEBUG
config.ini.php
######################
## VisualStudioCode ##
######################
.vscode/*
.vscode/*
#Builder
.buildconfig

View File

@@ -1,19 +1,32 @@
dist: trusty
sudo: false
language: php
php:
- '5.6'
- '7.0'
- hhvm
- nightly
install:
- pear install PHP_CodeSniffer
- if [[ $TRAVIS_PHP_VERSION == "hhvm" ]]; then
composer global require squizlabs/PHP_CodeSniffer;
else
pear channel-update pear.php.net;
pear install PHP_CodeSniffer;
fi
script:
- phpenv rehash
- phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p
- if [[ $TRAVIS_PHP_VERSION == "hhvm" ]]; then
/home/travis/.composer/vendor/bin/phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
else
phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
fi
matrix:
fast_finish: true
include:
- php: 5.6
- php: 7.0
- php: hhvm
- php: nightly
allow_failures:
- php: hhvm
- php: nightly
- php: nightly

View File

@@ -1,75 +1,105 @@
rss-bridge Changelog
===
RSS-Bridge 2017-08-19
==
## General changes
* whitelist: Do case-insensitive whitelist matching
* [FeedExpander] Fix Serialization of 'SimpleXMLElement' is not allowed
* [FeedExpander] Remove whitespace from source content
* [index] Add GET parameter 'q' for search queries
- **Example**: You can now add `&q=Twitter` to load into the search field
* [index] Check permissions for cache folder and whitelist file
* [index] Show bridge options when loading with URL fragment
- **Example**: You can now add `#bridge-Twitter` to load the card with all
parameters visible
* [style] Center search cursor and hide placeholder
* [validation] Fix error on undefined optional numeric value
## Modified bridges
* [DanbooruBridge] Allow descendant classes to override tag collection
* [DribbbleBridge] Add dribble bridge listing last dribble popular shots (#558)
* [FacebookBridge] Fix &amp; in URLs
* [GelbooruBridge] Fix bridge not getting tags correctly
* [GoComicsBridge] Fix for page structure changes (#568)
* [LeBonCoinBridge] Fix bridge is marked executable
* [LWNprevBridge] Fix everchanging url
* [YoutubeBridge] Fix error on certain keywords
* [YoutubeBridge] Fix issues loading playlists
## Removed bridges
* VineBridge
RSS-Bridge 2017-08-03
==
## Important changes
RSS-Bridge now has [contribution guidelines](CONTRIBUTING.md)
[phpcs rules](phpcs.xml) follow the [contribution guidelines](CONTRIBUTING.md)
* RSS-Bridge now has [contribution guidelines](CONTRIBUTING.md)
* [phpcs rules](phpcs.xml) follow the [contribution guidelines](CONTRIBUTING.md)
## General changes
Added a search bar to make searching for bridges easier
Added user friendly error page for when a bridge fails
Added caching of extraInfos (name, uri)
Added an indicator to warn for bridges using HTTP instead of HTTPS
Various bug fixes and improvements
* Added a search bar to make searching for bridges easier
* Added user friendly error page for when a bridge fails
* Added caching of extraInfos (name, uri)
* Added an indicator to warn for bridges using HTTP instead of HTTPS
* Various bug fixes and improvements
## Modified bridges
[AllocineFRBridge] Update Faux Raccord link
[DanbooruBridge] Fix broken URI
[DuckDuckGoBridge] Disable DuckDuckGo redirects so that the links returned are correct.
[FacebookBridge] Add option to hide posts with facebook videos
[FacebookBridge] Add requester languages to HTTP header
[FacebookBridge] Handle summary posts
[FacebookBridge] Replace 'novideo' with 'media_type'
[FilterBridge] Initial implementation of basic title permit and block
[FlickrTagBridge] Fix and improve bridge by using the FlickrExploreBridge approach
[GooglePlusPostBridge] Autofix user names
[GooglePlusPostBridge] Fix bridge implementation
[GooglePlusPostBridge] Fix content loading
[InstagramBridge] Add option to filter for videos and pictures
[LWNprevBridge] full rewrite
[MangareaderBridge] Fix double forward slashes
[NasaApodBridge] Use HTTPS instead of HTTP
[PinterestBridge] Fix checkbox not working
[PinterestBridge] Fix implementation after DOM changes
[RTBFBridge] Update URI
[SexactuBridge] Fix URI and timestamp
[SexactuBridge] Use most modern version of bridge api and cached pages (#504)
[ShanaprojectBridge] Don't throw error if timestamp is missing
[TwitterBridge] Add option to hide retweets
[TwitterBridge] Avoid empty content caused by new login policy
[TwitterBridge] Fix double slashes in URI
[TwitterBridge] Fix missing spaces
[TwitterBridge] Fix title includes anchors in plaintext format
[TwitterBridge] ignore promoted tweets
[TwitterBridge] Optimize returned image sizes
[TwitterBridge] Show quotes and pictures
[WebfailBridge] Properly handle gifs (DOM changed)
[YoutubeBridge] Improve readability of feed contents
[YoutubeBridge] Improve URL handling in video descriptions
* AllocineFRBridge] Update Faux Raccord link
* [DanbooruBridge] Fix broken URI
* [DuckDuckGoBridge] Disable DuckDuckGo redirects so that the links returned are correct.
* [FacebookBridge] Add option to hide posts with facebook videos
* [FacebookBridge] Add requester languages to HTTP header
* [FacebookBridge] Handle summary posts
* [FacebookBridge] Replace 'novideo' with 'media_type'
* [FilterBridge] Initial implementation of basic title permit and block
* [FlickrTagBridge] Fix and improve bridge by using the FlickrExploreBridge approach
* [GooglePlusPostBridge] Autofix user names
* [GooglePlusPostBridge] Fix bridge implementation
* [GooglePlusPostBridge] Fix content loading
* [InstagramBridge] Add option to filter for videos and pictures
* [LWNprevBridge] full rewrite
* [MangareaderBridge] Fix double forward slashes
* [NasaApodBridge] Use HTTPS instead of HTTP
* [PinterestBridge] Fix checkbox not working
* [PinterestBridge] Fix implementation after DOM changes
* [RTBFBridge] Update URI
* [SexactuBridge] Fix URI and timestamp
* [SexactuBridge] Use most modern version of bridge api and cached pages (#504)
* [ShanaprojectBridge] Don't throw error if timestamp is missing
* [TwitterBridge] Add option to hide retweets
* [TwitterBridge] Avoid empty content caused by new login policy
* [TwitterBridge] Fix double slashes in URI
* [TwitterBridge] Fix missing spaces
* [TwitterBridge] Fix title includes anchors in plaintext format
* [TwitterBridge] ignore promoted tweets
* [TwitterBridge] Optimize returned image sizes
* [TwitterBridge] Show quotes and pictures
* [WebfailBridge] Properly handle gifs (DOM changed)
* [YoutubeBridge] Improve readability of feed contents
* [YoutubeBridge] Improve URL handling in video descriptions
## New bridges
AmazonBridge
DiceBridge
EtsyBridge
FB2Bridge
FilterBridge
FlickrBridge
GithubSearchBridge
GoComicsBridge
KATBridge
KernelBugTrackerBridge
MixCloudBridge
MoinMoinBridge
RainbowSixSiegeBridge
SteamBridge
TheTVDBBridge
Torrent9Bridge
UsbekEtRicaBridge
WikiLeaksBridge
WordPressPluginUpdateBridge
* AmazonBridge
* DiceBridge
* EtsyBridge
* FB2Bridge
* FilterBridge
* FlickrBridge
* GithubSearchBridge
* GoComicsBridge
* KATBridge
* KernelBugTrackerBridge
* MixCloudBridge
* MoinMoinBridge
* RainbowSixSiegeBridge
* SteamBridge
* TheTVDBBridge
* Torrent9Bridge
* UsbekEtRicaBridge
* WikiLeaksBridge
* WordPressPluginUpdateBridge
Alpha 0.2
===

5
Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM ulsmith/alpine-apache-php7
COPY ./ /app/public/
RUN chown -R apache:root /app/public

View File

@@ -1,29 +1,29 @@
rss-bridge
===
[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge)
[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge) [![Docker Build Status](https://img.shields.io/docker/build/rssbridge/rss-bridge.svg)](https://hub.docker.com/r/rssbridge/rss-bridge/)
rss-bridge is a PHP project capable of generating ATOM feeds for websites which don't have one.
Supported sites/pages (main)
===
* `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr
* `GoogleSearch` : Most recent results from Google Search
* `GooglePlus` : Most recent posts of user timeline
* `Twitter` : Return keyword/hashtag search or user timeline
* `Identi.ca` : Identica user timeline (Should be compatible with other Pump.io instances)
* `YouTube` : YouTube user channel, playlist or search
* `Cryptome` : Returns the most recent documents from [Cryptome.org](http://cryptome.org/)
* `DansTonChat`: Most recent quotes from [danstonchat.com](http://danstonchat.com/)
* `DuckDuckGo`: Most recent results from [DuckDuckGo.com](https://duckduckgo.com/)
* `Instagram`: Most recent photos from an Instagram user
* `OpenClassrooms`: Lastest tutorials from [fr.openclassrooms.com](http://fr.openclassrooms.com/)
* `Pinterest`: Most recent photos from user or search
* `ScmbBridge`: Newest stories from [secouchermoinsbete.fr](http://secouchermoinsbete.fr/)
* `Wikipedia`: highlighted articles from [Wikipedia](https://wikipedia.org/) in English, German, French or Esperanto
* `Bandcamp` : Returns last release from [bandcamp](https://bandcamp.com/) for a tag
* `ThePirateBay` : Returns the newest indexed torrents from [The Pirate Bay](https://thepiratebay.se/) with keywords
* `Facebook` : Returns the latest posts on a page or profile on [Facebook](https://facebook.com/)
* `Bandcamp` : Returns last release from [bandcamp](https://bandcamp.com/) for a tag
* `Cryptome` : Returns the most recent documents from [Cryptome.org](http://cryptome.org/)
* `DansTonChat`: Most recent quotes from [danstonchat.com](http://danstonchat.com/)
* `DuckDuckGo`: Most recent results from [DuckDuckGo.com](https://duckduckgo.com/)
* `Facebook` : Returns the latest posts on a page or profile on [Facebook](https://facebook.com/)
* `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr
* `GooglePlus` : Most recent posts of user timeline
* `GoogleSearch` : Most recent results from Google Search
* `Identi.ca` : Identica user timeline (Should be compatible with other Pump.io instances)
* `Instagram`: Most recent photos from an Instagram user
* `OpenClassrooms`: Lastest tutorials from [fr.openclassrooms.com](http://fr.openclassrooms.com/)
* `Pinterest`: Most recent photos from user or search
* `ScmbBridge`: Newest stories from [secouchermoinsbete.fr](http://secouchermoinsbete.fr/)
* `ThePirateBay` : Returns the newest indexed torrents from [The Pirate Bay](https://thepiratebay.se/) with keywords
* `Twitter` : Return keyword/hashtag search or user timeline
* `Wikipedia`: highlighted articles from [Wikipedia](https://wikipedia.org/) in English, German, French or Esperanto
* `YouTube` : YouTube user channel, playlist or search
Plus [many other bridges](bridges/) to enable, thanks to the community
@@ -31,11 +31,11 @@ Output format
===
Output format can take several forms:
* `Atom` : ATOM Feed, for use in RSS/Feed readers
* `Mrss` : MRSS Feed, for use in RSS/Feed readers
* `Json` : Json, for consumption by other applications.
* `Html` : Simple html page.
* `Plaintext` : raw text (php object, as returned by print_r)
* `Atom` : ATOM Feed, for use in RSS/Feed readers
* `Html` : Simple html page.
* `Json` : Json, for consumption by other applications.
* `Mrss` : MRSS Feed, for use in RSS/Feed readers
* `Plaintext` : raw text (php object, as returned by print_r)
Screenshot
===
@@ -53,7 +53,7 @@ Requirements
* PHP 5.6, e.g. `AddHandler application/x-httpd-php56 .php` in `.htaccess`
* `openssl` extension enabled in PHP config (`php.ini`)
* `allow_url_fopen=1` in `php.ini`
* `curl` extension enabled in PHP config (`php.ini`)
Enabling/Disabling bridges
===

View File

@@ -26,7 +26,7 @@ class AllocineFRBridge extends BridgeAbstract {
switch($this->getInput('category')) {
case 'faux-raccord':
$uri = static::URI . 'video/programme-12284/saison-29841/';
$uri = static::URI . 'video/programme-12284/saison-32180/';
break;
case 'top-5':
$uri = static::URI . 'video/programme-12299/saison-29561/';
@@ -64,7 +64,7 @@ class AllocineFRBridge extends BridgeAbstract {
self::PARAMETERS[$this->queriedContext]['category']['values']
);
foreach($html->find('figure.media-meta-fig') as $element) {
foreach($html->find('.media-meta-list figure.media-meta-fig') as $element) {
$item = array();
$title = $element->find('div.titlebar h3.title a', 0);

View File

@@ -0,0 +1,187 @@
<?php
class AmazonPriceTrackerBridge extends BridgeAbstract {
const MAINTAINER = 'captn3m0';
const NAME = 'Amazon Price Tracker';
const URI = 'https://www.amazon.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Tracks price for a single product on Amazon';
const PARAMETERS = array(
array(
'asin' => array(
'name' => 'ASIN',
'required' => true,
'exampleValue' => 'B071GB1VMQ',
// https://stackoverflow.com/a/12827734
'pattern' => 'B[\dA-Z]{9}|\d{9}(X|\d)',
),
'tld' => array(
'name' => 'Country',
'type' => 'list',
'required' => true,
'values' => array(
'Australia' => 'com.au',
'Brazil' => 'com.br',
'Canada' => 'ca',
'China' => 'cn',
'France' => 'fr',
'Germany' => 'de',
'India' => 'in',
'Italy' => 'it',
'Japan' => 'co.jp',
'Mexico' => 'com.mx',
'Netherlands' => 'nl',
'Spain' => 'es',
'United Kingdom' => 'co.uk',
'United States' => 'com',
),
'defaultValue' => 'com',
),
));
protected $title;
/**
* Generates domain name given a amazon TLD
*/
private function getDomainName() {
return 'https://www.amazon.' . $this->getInput('tld');
}
/**
* Generates URI for a Amazon product page
*/
public function getURI() {
if (!is_null($this->getInput('asin'))) {
return $this->getDomainName() . '/dp/' . $this->getInput('asin') . '/';
}
return parent::getURI();
}
/**
* Scrapes the product title from the html page
* returns the default title if scraping fails
*/
private function getTitle($html) {
$titleTag = $html->find('#productTitle', 0);
if (!$titleTag) {
return $this->getDefaultTitle();
} else {
return trim(html_entity_decode($titleTag->innertext, ENT_QUOTES));
}
}
/**
* Title used by the feed if none could be found
*/
private function getDefaultTitle() {
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('asin');
}
/**
* Returns name for the feed
* Uses title (already scraped) if it has one
*/
public function getName() {
if (isset($this->title)) {
return $this->title;
} else {
return parent::getName();
}
}
private function parseDynamicImage($attribute) {
$json = json_decode(html_entity_decode($attribute), true);
if ($json and count($json) > 0) {
return array_keys($json)[0];
}
}
/**
* Returns a generated image tag for the product
*/
private function getImage($html) {
$imageSrc = $html->find('#main-image-container img', 0);
if ($imageSrc) {
$hiresImage = $imageSrc->getAttribute('data-old-hires');
$dynamicImageAttribute = $imageSrc->getAttribute('data-a-dynamic-image');
$image = $hiresImage ?: $this->parseDynamicImage($dynamicImageAttribute);
}
$image = $image ?: 'https://placekitten.com/200/300';
return <<<EOT
<img width="300" style="max-width:300;max-height:300" src="$image" alt="{$this->title}" />
EOT;
}
/**
* Return \simple_html_dom object
* for the entire html of the product page
*/
private function getHtml() {
$uri = $this->getURI();
return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request Amazon.');
}
private function scrapePriceFromMetrics($html) {
$asinData = $html->find('#cerberus-data-metrics', 0);
// <div id="cerberus-data-metrics" style="display: none;"
// data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0"
// data-asin-currency-code="USD" data-substitute-count="-1" ... />
if ($asinData) {
return [
'price' => $asinData->getAttribute('data-asin-price'),
'currency' => $asinData->getAttribute('data-asin-currency-code'),
'shipping' => $asinData->getAttribute('data-asin-shipping')
];
}
return false;
}
private function scrapePriceGeneric($html) {
$priceDiv = $html->find('span.offer-price', 0) ?: $html->find('.a-color-price', 0);
preg_match('/^\s*([A-Z]{3}|£|\$)\s?([\d.,]+)\s*$/', $priceDiv->plaintext, $matches);
if (count($matches) === 3) {
return [
'price' => $matches[2],
'currency' => $matches[1],
'shipping' => '0'
];
}
return false;
}
/**
* Scrape method for Amazon product page
* @return [type] [description]
*/
public function collectData() {
$html = $this->getHtml();
$this->title = $this->getTitle($html);
$imageTag = $this->getImage($html);
$data = $this->scrapePriceFromMetrics($html) ?: $this->scrapePriceGeneric($html);
$item = array(
'title' => $this->title,
'uri' => $this->getURI(),
'content' => "$imageTag<br/>Price: {$data['price']} {$data['currency']}",
);
if ($data['shipping'] !== '0') {
$item['content'] .= "<br>Shipping: {$data['shipping']} {$data['currency']}</br>";
}
$this->items[] = $item;
}
}

View File

@@ -3,24 +3,28 @@ class Arte7Bridge extends BridgeAbstract {
const MAINTAINER = 'mitsukarenai';
const NAME = 'Arte +7';
const URI = 'http://www.arte.tv/';
const URI = 'https://www.arte.tv/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns newest videos from ARTE +7';
const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA';
const PARAMETERS = array(
'Catégorie (Français)' => array(
'catfr' => array(
'type' => 'list',
'name' => 'Catégorie',
'values' => array(
'Toutes les vidéos (français)' => 'toutes-les-videos',
'Actu & société' => 'actu-société',
'Séries & fiction' => 'séries-fiction',
'Cinéma' => 'cinéma',
'Arts & spectacles classiques' => 'arts-spectacles-classiques',
'Culture pop' => 'culture-pop',
'Découverte' => 'découverte',
'Histoire' => 'histoire',
'Junior' => 'junior'
'Toutes les vidéos (français)' => null,
'Actu & société' => 'ACT',
'Séries & fiction' => 'SER',
'Cinéma' => 'CIN',
'Arts & spectacles classiques' => 'ARS',
'Culture pop' => 'CPO',
'Découverte' => 'DEC',
'Histoire' => 'HIST',
'Science' => 'SCI',
'Autre' => 'AUT'
)
)
),
@@ -29,15 +33,16 @@ class Arte7Bridge extends BridgeAbstract {
'type' => 'list',
'name' => 'Catégorie',
'values' => array(
'Alle Videos (deutsch)' => 'alle-videos',
'Aktuelles & Gesellschaft' => 'aktuelles-gesellschaft',
'Fernsehfilme & Serien' => 'fernsehfilme-serien',
'Kino' => 'kino',
'Kunst & Kultur' => 'kunst-kultur',
'Popkultur & Alternativ' => 'popkultur-alternativ',
'Entdeckung' => 'entdeckung',
'Geschichte' => 'geschichte',
'Junior' => 'junior'
'Alle Videos (deutsch)' => null,
'Aktuelles & Gesellschaft' => 'ACT',
'Fernsehfilme & Serien' => 'SER',
'Kino' => 'CIN',
'Kunst & Kultur' => 'ARS',
'Popkultur & Alternativ' => 'CPO',
'Entdeckung' => 'DEC',
'Geschichte' => 'HIST',
'Wissenschaft' => 'SCI',
'Sonstiges' => 'AUT'
)
)
)
@@ -55,44 +60,37 @@ class Arte7Bridge extends BridgeAbstract {
break;
}
$url = self::URI . 'guide/' . $lang . '/plus7/' . $category;
$input = getContents($url) or die('Could not request ARTE.');
$url = 'https://api.arte.tv/api/opa/v3/videos?sort=-lastModified&limit=10&language='
. $lang
. ($category != null ? '&category.code=' . $category : '');
if(strpos($input, 'categoryVideoSet') !== false) {
$input = explode('categoryVideoSet="', $input);
$input = explode('}}', $input[1]);
$input = $input[0] . '}}';
} else {
$input = explode('videoSet="', $input);
$input = explode('}]}', $input[1]);
$input = $input[0] . '}]}';
}
$header = array(
'Authorization: Bearer ' . self::API_TOKEN
);
$input_json = json_decode(html_entity_decode($input, ENT_QUOTES), true);
$input = getContents($url, $header) or die('Could not request ARTE.');
$input_json = json_decode($input, true);
foreach($input_json['videos'] as $element) {
$item = array();
$item['uri'] = str_replace("autoplay=1", "", $element['url']);
$item['uri'] = $element['url'];
$item['id'] = $element['id'];
$hack_broadcast_time = $element['rights_end'];
$hack_broadcast_time = strtok($hack_broadcast_time, 'T');
$hack_broadcast_time = strtok('T');
$item['timestamp'] = strtotime($element['scheduled_on'] . 'T' . $hack_broadcast_time);
$item['timestamp'] = strtotime($element['videoRightsBegin']);
$item['title'] = $element['title'];
if(!empty($element['subtitle']))
$item['title'] = $element['title'] . ' | ' . $element['subtitle'];
$item['duration'] = round((int)$element['duration'] / 60);
$item['content'] = $element['teaser']
$item['duration'] = round((int)$element['durationSeconds'] / 60);
$item['content'] = $element['teaserText']
. '<br><br>'
. $item['duration']
. 'min<br><a href="'
. $item['uri']
. '"><img src="'
. $element['thumbnail_url']
. $element['mainImage']['url']
. '" /></a>';
$this->items[] = $item;

View File

@@ -19,7 +19,7 @@ class BlaguesDeMerdeBridge extends BridgeAbstract {
$item['content'] = trim($element->find('div.joke_text_contener', 0)->innertext);
$uri = $temp[2]->href;
$item['uri'] = $uri;
$item['title'] = substr($uri, (strrpos($uri, "/") + 1));
$item['title'] = substr($uri, (strrpos($uri, '/') + 1));
$date = $element->find('li.bdm_date', 0)->innertext;
$time = mktime(0, 0, 0, substr($date, 3, 2), substr($date, 0, 2), substr($date, 6, 4));
$item['timestamp'] = $time;

View File

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

View File

@@ -23,14 +23,14 @@ class CADBridge extends FeedExpander {
if($html3 == false)
return 'Daily comic not released yet';
$htmlpart = explode("/", $url);
$htmlpart = explode('/', $url);
switch ($htmlpart[3]) {
case 'cad':
preg_match_all("/http:\/\/cdn2\.cad-comic\.com\/comics\/cad-\S*png/", $html3, $url2);
preg_match_all('/http:\/\/cdn2\.cad-comic\.com\/comics\/cad-\S*png/', $html3, $url2);
break;
case 'sillies':
preg_match_all("/http:\/\/cdn2\.cad-comic\.com\/comics\/sillies-\S*gif/", $html3, $url2);
preg_match_all('/http:\/\/cdn2\.cad-comic\.com\/comics\/sillies-\S*gif/', $html3, $url2);
break;
default:
return 'Daily comic not released yet';

View File

@@ -0,0 +1,25 @@
<?php
class ChristianDailyReporterBridge extends BridgeAbstract {
const MAINTAINER = 'rogerdc';
const NAME = 'Christian Daily Reporter Unofficial RSS';
const URI = 'https://www.christiandailyreporter.com/';
const DESCRIPTION = 'The Unofficial Christian Daily Reporter RSS';
// const CACHE_TIMEOUT = 86400; // 1 day
public function collectData() {
$uri = 'https://www.christiandailyreporter.com/';
$html = getSimpleHTMLDOM($uri)
or returnServerError('Could not request Christian Daily Reporter.');
foreach($html->find('div.top p a,div.column p a') as $element) {
$item = array();
// Title
$item['title'] = $element->innertext;
// URL
$item['uri'] = $element->href;
$this->items[] = $item;
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
class ContainerLinuxReleasesBridge extends BridgeAbstract {
const MAINTAINER = 'captn3m0';
const NAME = 'Core OS Container Linux Releases Bridge';
const URI = 'https://coreos.com/releases/';
const DESCRIPTION = 'Returns the releases notes for Container Linux';
const STABLE = 'stable';
const BETA = 'beta';
const ALPHA = 'alpha';
const PARAMETERS = [
[
'channel' => [
'name' => 'Release Channel',
'type' => 'list',
'required' => true,
'defaultValue' => self::STABLE,
'values' => [
'Stable' => self::STABLE,
'Beta' => self::BETA,
'Alpha' => self::ALPHA,
],
]
]
];
public function getReleaseFeed($jsonUrl) {
$json = getContents($jsonUrl)
or returnServerError('Could not request Core OS Website.');
return json_decode($json, true);
}
public function collectData() {
$data = $this->getReleaseFeed($this->getJsonUri());
foreach ($data as $releaseVersion => $release) {
$item = [];
$item['uri'] = "https://coreos.com/releases/#$releaseVersion";
$item['title'] = $releaseVersion;
$content = $release['release_notes'];
$content .= <<<EOT
Major Software:
* Kernel: {$release['major_software']['kernel'][0]}
* Docker: {$release['major_software']['docker'][0]}
* etcd: {$release['major_software']['etcd'][0]}
EOT;
$item['timestamp'] = strtotime($release['release_date']);
// Based on https://gist.github.com/jbroadway/2836900
// Links
$regex = '/\[([^\[]+)\]\(([^\)]+)\)/';
$replacement = '<a href=\'\2\'>\1</a>';
$item['content'] = preg_replace($regex, $replacement, $content);
// Headings
$regex = '/^(.*)\:\s?$/m';
$replacement = '<h3>\1</h3>';
$item['content'] = preg_replace($regex, $replacement, $item['content']);
// Lists
$regex = '/\n\s*[\*|\-](.*)/';
$item['content'] = preg_replace_callback ($regex, function($regs) {
$item = $regs[1];
return sprintf ('<ul><li>%s</li></ul>', trim ($item));
}, $item['content']);
$this->items[] = $item;
}
}
private function getJsonUri() {
$channel = $this->getInput('channel');
return "https://coreos.com/releases/releases-$channel.json";
}
public function getURI() {
return self::URI;
}
public function getName(){
if(!is_null($this->getInput('channel'))) {
return 'Container Linux Releases: ' . $this->getInput('channel') . ' Channel';
}
return parent::getName();
}
}

View File

@@ -25,7 +25,7 @@ class CopieDoubleBridge extends BridgeAbstract {
} elseif(strpos($element->innertext, '/images/suivant.gif') === false) {
$a = $element->find('a', 0);
$item['uri'] = self::URI . $a->href;
$content = str_replace('src="/', 'src="/' . self::URI, $element->find("td", 0)->innertext);
$content = str_replace('src="/', 'src="/' . self::URI, $element->find('td', 0)->innertext);
$content = str_replace('href="/', 'href="' . self::URI, $content);
$item['content'] = $content;
$this->items[] = $item;

View File

@@ -11,7 +11,7 @@ class CourrierInternationalBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Error.');
$element = $html->find("article");
$element = $html->find('article');
$article_count = 1;
foreach($element as $article) {

View File

@@ -1,74 +0,0 @@
<?php
class CpasbienBridge extends BridgeAbstract {
const MAINTAINER = 'lagaisse';
const NAME = 'Cpasbien Bridge';
const URI = 'http://www.cpasbien.cm';
const CACHE_TIMEOUT = 86400; // 24h
const DESCRIPTION = 'Returns latest torrents from a request query';
const PARAMETERS = array( array(
'q' => array(
'name' => 'Search',
'required' => true,
'title' => 'Type your search'
)
));
public function collectData(){
$request = str_replace(" ", "-", trim($this->getInput('q')));
$html = getSimpleHTMLDOM(self::URI . '/recherche/' . urlencode($request) . '.html')
or returnServerError('No results for this query.');
foreach($html->find('#gauche', 0)->find('div') as $episode) {
if($episode->getAttribute('class') == 'ligne0'
|| $episode->getAttribute('class') == 'ligne1') {
$urlepisode = $episode->find('a', 0)->getAttribute('href');
$htmlepisode = getSimpleHTMLDOMCached($urlepisode, 86400 * 366 * 30);
$item = array();
$item['author'] = $episode->find('a', 0)->text();
$item['title'] = $episode->find('a', 0)->text();
$item['pubdate'] = $this->getCachedDate($urlepisode);
$textefiche = $htmlepisode->find('#textefiche', 0)->find('p', 1);
if(isset($textefiche)) {
$item['content'] = $textefiche->text();
} else {
$p = $htmlepisode->find('#textefiche', 0)->find('p');
if(!empty($p)) {
$item['content'] = $htmlepisode->find('#textefiche', 0)->find('p', 0)->text();
}
}
$item['id'] = $episode->find('a', 0)->getAttribute('href');
$item['uri'] = self::URI . $htmlepisode->find('#telecharger', 0)->getAttribute('href');
$this->items[] = $item;
}
}
}
public function getName(){
if(!is_null($this->getInput('q'))) {
return $this->getInput('q') . ' : ' . self::NAME;
}
return parent::getName();
}
private function getCachedDate($url){
debugMessage('getting pubdate from url ' . $url . '');
// Initialize cache
$cache = Cache::create('FileCache');
$cache->setPath(CACHE_DIR . '/pages');
$params = [$url];
$cache->setParameters($params);
// Get cachefile timestamp
$time = $cache->getTime();
return ($time !== false ? $time : time());
}
}

View File

@@ -23,6 +23,7 @@ class DanbooruBridge extends BridgeAbstract {
const PATHTODATA = 'article';
const IDATTRIBUTE = 'data-id';
const TAGATTRIBUTE = 'alt';
protected function getFullURI(){
return $this->getURI()
@@ -30,16 +31,20 @@ class DanbooruBridge extends BridgeAbstract {
. '&tags=' . urlencode($this->getInput('t'));
}
protected function getTags($element){
return $element->find('img', 0)->getAttribute(static::TAGATTRIBUTE);
}
protected function getItemFromElement($element){
// Fix links
defaultLinkTo($element, $this->getURI());
$item = array();
$item['uri'] = $element->find('a', 0)->href;
$item['postid'] = (int)preg_replace("/[^0-9]/", '', $element->getAttribute(static::IDATTRIBUTE));
$item['postid'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
$item['timestamp'] = time();
$thumbnailUri = $element->find('img', 0)->src;
$item['tags'] = $element->find('img', 0)->getAttribute('alt');
$item['tags'] = $this->getTags($element);
$item['title'] = $this->getName() . ' | ' . $item['postid'];
$item['content'] = '<a href="'
. $item['uri']

View File

@@ -15,8 +15,13 @@ class DansTonChatBridge extends BridgeAbstract {
foreach($html->find('div.item') as $element) {
$item = array();
$item['uri'] = $element->find('a', 0)->href;
$item['title'] = 'DansTonChat ' . $element->find('a', 1)->plaintext;
$item['content'] = $element->find('a', 0)->innertext;
$titleContent = $element->find('h3 a', 0);
if($titleContent) {
$item['title'] = 'DansTonChat ' . html_entity_decode($titleContent->plaintext, ENT_QUOTES);
} else {
$item['title'] = 'DansTonChat';
}
$item['content'] = $element->find('div.item-content a', 0)->innertext;
$this->items[] = $item;
}
}

587
bridges/DealabsBridge.php Normal file
View File

@@ -0,0 +1,587 @@
<?php
class DealabsBridge extends PepperBridgeAbstract {
const NAME = 'Dealabs Bridge';
const URI = 'https://www.dealabs.com/';
const DESCRIPTION = 'Affiche les Deals de Dealabs';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
'Recherche par Mot(s) clé(s)' => array (
'q' => array(
'name' => 'Mot(s) clé(s)',
'type' => 'text',
'required' => true
),
'hide_expired' => array(
'name' => 'Masquer les éléments expirés',
'type' => 'checkbox',
'required' => 'true'
),
'hide_local' => array(
'name' => 'Masquer les deals locaux',
'type' => 'checkbox',
'title' => 'Masquer les deals en magasins physiques',
'required' => 'true'
),
'priceFrom' => array(
'name' => 'Prix minimum',
'type' => 'text',
'title' => 'Prix mnimum en euros',
'required' => 'false',
'defaultValue' => ''
),
'priceTo' => array(
'name' => 'Prix maximum',
'type' => 'text',
'title' => 'Prix maximum en euros',
'required' => 'false',
'defaultValue' => ''
),
),
'Deals par groupe' => array(
'group' => array(
'name' => 'Groupe',
'type' => 'list',
'required' => 'true',
'title' => 'Groupe dont il faut afficher les deals',
'values' => array(
'Accessoires & gadgets' => 'accessoires-gadgets',
'Alimentation & boissons' => 'alimentation-boissons',
'Animaux' => 'animaux',
'Applis & logiciels' => 'applis-logiciels',
'Consoles & jeux vidéo' => 'consoles-jeux-video',
'Culture & divertissement' => 'culture-divertissement',
'Gratuit' => 'gratuit',
'Image, son & vidéo' => 'image-son-video',
'Informatique' => 'informatique',
'Jeux & jouets' => 'jeux-jouets',
'Maison & jardin' => 'maison-jardin',
'Mode & accessoires' => 'mode-accessoires',
'Santé & cosmétiques' => 'hygiene-sante-cosmetiques',
'Services divers' => 'services-divers',
'Sports & plein air' => 'sports-plein-air',
'Téléphonie' => 'telephonie',
'Voyages & sorties' => 'voyages-sorties-restaurants',
)
),
'order' => array(
'name' => 'Trier par',
'type' => 'list',
'required' => 'true',
'title' => 'Ordre de tri des deals',
'values' => array(
'Du deal le plus Hot au moins Hot' => '',
'Du deal le plus récent au plus ancien' => '-nouveaux',
'Du deal le plus commentés au moins commentés' => '-commentes'
)
)
)
);
public $lang = array(
'bridge-uri' => SELF::URI,
'bridge-name' => SELF::NAME,
'context-keyword' => 'Recherche par Mot(s) clé(s)',
'context-group' => 'Deals par groupe',
'uri-group' => '/groupe/',
'request-error' => 'Could not request Dealabs',
'no-results' => 'Il n&#039;y a rien à afficher pour le moment :(',
'relative-date-indicator' => array(
'il y a',
),
'price' => 'Prix',
'shipping' => 'Livraison',
'origin' => 'Origine',
'discount' => 'Réduction',
'title-keyword' => 'Recherche',
'title-group' => 'Groupe',
'local-months' => array(
'janvier',
'février',
'mars',
'avril',
'mai',
'juin',
'juillet',
'août',
'septembre',
'octobre',
'novembre',
'décembre'
),
'local-time-relative' => array(
'il y a ',
'min',
'h',
'jour',
'jours',
'mois',
'ans',
'et '
),
'date-prefixes' => array(
'Actualisé ',
),
'relative-date-alt-prefixes' => array(
'Actualisé ',
),
'relative-date-ignore-suffix' => array(
),
'localdeal' => array(
'Local',
'Pays d\'expédition'
),
);
}
class PepperBridgeAbstract extends BridgeAbstract {
const CACHE_TIMEOUT = 3600;
public function collectData(){
switch($this->queriedContext) {
case $this->i8n('context-keyword'):
return $this->collectDataKeywords();
break;
case $this->i8n('context-group'):
return $this->collectDataGroup();
break;
}
}
/**
* Get the Deal data from the choosen group in the choosed order
*/
public function collectDataGroup()
{
$group = $this->getInput('group');
$order = $this->getInput('order');
$url = $this->i8n('bridge-uri')
. $this->i8n('uri-group') . $group . $order;
$this->collectDeals($url);
}
/**
* Get the Deal data from the choosen keywords and parameters
*/
public function collectDataKeywords()
{
$q = $this->getInput('q');
$hide_expired = $this->getInput('hide_expired');
$hide_local = $this->getInput('hide_local');
$priceFrom = $this->getInput('priceFrom');
$priceTo = $this->getInput('priceFrom');
/* Even if the original website uses POST with the search page, GET works too */
$url = $this->i8n('bridge-uri')
. '/search/advanced?q='
. urlencode($q)
. '&hide_expired='. $hide_expired
. '&hide_local='. $hide_local
. '&priceFrom='. $priceFrom
. '&priceTo='. $priceTo
/* Some default parameters
* search_fields : Search in Titres & Descriptions & Codes
* sort_by : Sort the search by new deals
* time_frame : Search will not be on a limited timeframe
*/
. '&search_fields[]=1&search_fields[]=2&search_fields[]=3&sort_by=new&time_frame=0';
$this->collectDeals($url);
}
/**
* Get the Deal data using the given URL
*/
public function collectDeals($url){
$html = getSimpleHTMLDOM($url)
or returnServerError($this->i8n('request-error'));
$list = $html->find('article[id]');
// Deal Image Link CSS Selector
$selectorImageLink = implode(
' ', /* Notice this is a space! */
array(
'cept-thread-image-link',
'imgFrame',
'imgFrame--noBorder',
'thread-listImgCell',
)
);
// Deal Link CSS Selector
$selectorLink = implode(
' ', /* Notice this is a space! */
array(
'cept-tt',
'thread-link',
'linkPlain',
)
);
// Deal Hotness CSS Selector
$selectorHot = implode(
' ', /* Notice this is a space! */
array(
'flex',
'flex--align-c',
'flex--justify-space-between',
'space--b-2',
)
);
// Deal Description CSS Selector
$selectorDescription = implode(
' ', /* Notice this is a space! */
array(
'cept-description-container',
'overflow--wrap-break'
)
);
// Deal Date CSS Selector
$selectorDate = implode(
' ', /* Notice this is a space! */
array(
'size--all-s',
'flex',
'flex--justify-e',
'flex--grow-1',
)
);
// If there is no results, we don't parse the content because it display some random deals
$noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0);
if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) {
$this->items = array();
} else {
foreach ($list as $deal) {
$item = array();
$item['uri'] = $deal->find('div[class=threadGrid-title]', 0)->find('a', 0)->href;
$item['title'] = $deal->find('a[class*='. $selectorLink .']', 0
)->plaintext;
$item['author'] = $deal->find('span.thread-username', 0)->plaintext;
$item['content'] = '<table><tr><td><a href="'
. $deal->find(
'a[class*='. $selectorImageLink .']', 0)->href
. '"><img src="'
. $this->getImage($deal)
. '"/></td><td><h2><a href="'
. $deal->find('a[class*='. $selectorLink .']', 0)->href
. '">'
. $deal->find('a[class*='. $selectorLink .']', 0)->innertext
. '</a></h2>'
. $this->getPrice($deal)
. $this->getDiscount($deal)
. $this->getShipsFrom($deal)
. $this->getShippingCost($deal)
. $this->GetSource($deal)
. $deal->find('div[class*='. $selectorDescription .']', 0)->innertext
. '</td><td>'
. $deal->find('div[class='. $selectorHot .']', 0)->children(0)->outertext
. '</td></table>';
$dealDateDiv = $deal->find('div[class*='. $selectorDate .']', 0)
->find('span[class=hide--toW3]');
$itemDate = end($dealDateDiv)->plaintext;
// In case of a Local deal, there is no date, but we can use
// this case for other reason (like date not in the last field)
if ($this->contains($itemDate, $this->i8n('localdeal'))) {
$item['timestamp'] = time();
} else if ($this->contains($itemDate, $this->i8n('relative-date-indicator'))) {
$item['timestamp'] = $this->relativeDateToTimestamp($itemDate);
} else {
$item['timestamp'] = $this->parseDate($itemDate);
}
$this->items[] = $item;
}
}
}
/**
* Check if the string $str contains any of the string of the array $arr
* @return boolean true if the string matched anything otherwise false
*/
private function contains($str, array $arr)
{
foreach ($arr as $a) {
if (stripos($str, $a) !== false) {
return true;
}
}
return false;
}
/**
* Get the Price from a Deal if it exists
* @return string String of the deal price
*/
private function getPrice($deal)
{
if ($deal->find(
'span[class*=thread-price]', 0) != null) {
return '<div>'.$this->i8n('price') .' : '
. $deal->find(
'span[class*=thread-price]', 0
)->plaintext
. '</div>';
} else {
return '';
}
}
/**
* Get the Shipping costs from a Deal if it exists
* @return string String of the deal shipping Cost
*/
private function getShippingCost($deal)
{
if ($deal->find('span[class*=cept-shipping-price]', 0) != null) {
if ($deal->find('span[class*=cept-shipping-price]', 0)->children(0) != null) {
return '<div>'. $this->i8n('shipping') .' : '
. $deal->find('span[class*=cept-shipping-price]', 0)->children(0)->innertext
. '</div>';
} else {
return '<div>'. $this->i8n('shipping') .' : '
. $deal->find('span[class*=cept-shipping-price]', 0)->innertext
. '</div>';
}
} else {
return '';
}
}
/**
* Get the source of a Deal if it exists
* @return string String of the deal source
*/
private function GetSource($deal)
{
if ($deal->find('a[class=text--color-greyShade]', 0) != null) {
return '<div>'. $this->i8n('origin') .' : '
. $deal->find('a[class=text--color-greyShade]', 0)->outertext
. '</div>';
} else {
return '';
}
}
/**
* Get the original Price and discout from a Deal if it exists
* @return string String of the deal original price and discount
*/
private function getDiscount($deal)
{
if ($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) {
$discountHtml = $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0);
if ($discountHtml != null) {
$discount = $discountHtml->plaintext;
} else {
$discount = '';
}
return '<div>'. $this->i8n('discount') .' : <span style="text-decoration: line-through;">'
. $deal->find(
'span[class*=mute--text text--lineThrough]', 0
)->plaintext
. '</span>&nbsp;'
. $discount
. '</div>';
} else {
return '';
}
}
/**
* Get the Picture URL from a Deal if it exists
* @return string String of the deal Picture URL
*/
private function getImage($deal)
{
$selectorLazy = implode(
' ', /* Notice this is a space! */
array(
'thread-image',
'width--all-auto',
'height--all-auto',
'imgFrame-img',
'cept-thread-img',
'img--dummy',
'js-lazy-img'
)
);
$selectorPlain = implode(
' ', /* Notice this is a space! */
array(
'thread-image',
'width--all-auto',
'height--all-auto',
'imgFrame-img',
'cept-thread-img'
)
);
if ($deal->find('img[class='. $selectorLazy .']', 0) != null) {
return json_decode(
html_entity_decode(
$deal->find('img[class='. $selectorLazy .']', 0)
->getAttribute('data-lazy-img')))->{'src'};
} else {
return $deal->find('img[class*='. $selectorPlain .']', 0 )->src;
}
}
/**
* Get the originating country from a Deal if it exists
* @return string String of the deal originating country
*/
private function getShipsFrom($deal)
{
$selector = implode(
' ', /* Notice this is a space! */
array(
'meta-ribbon',
'overflow--wrap-off',
'space--l-3',
'text--color-greyShade'
)
);
if ($deal->find('span[class='. $selector .']', 0) != null) {
return '<div>'
. $deal->find('span[class='. $selector .']', 0)->children(2)->plaintext
. '</div>';
} else {
return '';
}
}
/**
* Transforms a local date into a timestamp
* @return int timestamp of the input date
*/
private function parseDate($string)
{
$month_local = $this->i8n('local-months');
$month_en = array(
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
);
// A date can be prfixed with some words, we remove theme
$string = $this->removeDatePrefixes($string);
// We translate the local months name in the english one
$date_str = trim(str_replace($month_local, $month_en, $string));
// If the date does not contain any year, we add the current year
if (!preg_match('/[0-9]{4}/', $string)) {
$date_str .= ' ' . date('Y');
}
// Add the Hour and minutes
$date_str .= ' 00:00';
$date = DateTime::createFromFormat('j F Y H:i', $date_str);
return $date->getTimestamp();
}
/**
* Remove the prefix of a date if it has one
* @return the date without prefiux
*/
private function removeDatePrefixes($string)
{
$string = str_replace($this->i8n('date-prefixes'), array(), $string);
return $string;
}
/**
* Remove the suffix of a relative date if it has one
* @return the relative date without suffixes
*/
private function removeRelativeDateSuffixes($string)
{
if (count($this->i8n('relative-date-ignore-suffix')) > 0) {
$string = preg_replace($this->i8n('relative-date-ignore-suffix'), '', $string);
}
return $string;
}
/**
* Transforms a relative local date into a timestamp
* @return int timestamp of the input date
*/
private function relativeDateToTimestamp($str) {
$date = new DateTime();
// In case of update date, replace it by the regular relative date first word
$str = str_replace($this->i8n('relative-date-alt-prefixes'), $this->i8n('local-time-relative')[0], $str);
$str = $this->removeRelativeDateSuffixes($str);
$search = $this->i8n('local-time-relative');
$replace = array(
'-',
'minute',
'hour',
'day',
'month',
'year',
''
);
$date->modify(str_replace($search, $replace, $str));
return $date->getTimestamp();
}
/**
* Returns the RSS Feed title according to the parameters
* @return string the RSS feed Tiyle
*/
public function getName(){
switch($this->queriedContext) {
case $this->i8n('context-keyword'):
return $this->i8n('bridge-name') . ' - '. $this->i8n('title-keyword') .' : '. $this->getInput('q');
break;
case $this->i8n('context-group'):
$values = $this->getParameters()[$this->i8n('context-group')]['group']['values'];
$group = array_search($this->getInput('group'), $values);
return $this->i8n('bridge-name') . ' - '. $this->i8n('title-group'). ' : '. $group;
break;
default: // Return default value
return static::NAME;
}
}
/**
* This is some "localisation" function that returns the needed content using
* the "$lang" class variable in the local class
* @return various the local content needed
*/
public function i8n($key)
{
if (array_key_exists($key, $this->lang)) {
return $this->lang[$key];
} else {
return null;
}
}
}

View File

@@ -35,11 +35,11 @@ class DemoBridge extends BridgeAbstract {
public function collectData(){
$item = array();
$item['author'] = "Me!";
$item['title'] = "Test";
$item['content'] = "Awesome content !";
$item['id'] = "Lalala";
$item['uri'] = "http://example.com/test";
$item['author'] = 'Me!';
$item['title'] = 'Test';
$item['content'] = 'Awesome content !';
$item['id'] = 'Lalala';
$item['uri'] = 'http://example.com/test';
$this->items[] = $item;
}

166
bridges/DemonoidBridge.php Normal file
View File

@@ -0,0 +1,166 @@
<?php
class DemonoidBridge extends BridgeAbstract {
const MAINTAINER = 'metaMMA';
const NAME = 'Demonoid';
const URI = 'https://www.demonoid.pw/';
const DESCRIPTION = 'Returns results from search';
const PARAMETERS = array(array(
'q' => array(
'name' => 'keywords',
'exampleValue' => 'keyword1 keyword2…',
'required' => true,
),
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'All' => 0,
'Movies' => 1,
'Music' => 2,
'TV' => 3,
'Games' => 4,
'Applications' => 5,
'Pictures' => 8,
'Anime' => 9,
'Comics' => 10,
'Books' => 11,
'Audiobooks' => 17
)
)
), array(
'catOnly' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'All' => 0,
'Movies' => 1,
'Music' => 2,
'TV' => 3,
'Games' => 4,
'Applications' => 5,
'Pictures' => 8,
'Anime' => 9,
'Comics' => 10,
'Books' => 11,
'Audiobooks' => 17
)
)
), array(
'userid' => array(
'name' => 'user id',
'exampleValue' => '00000',
'required' => true,
'type' => 'number'
),
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'All' => 0,
'Movies' => 1,
'Music' => 2,
'TV' => 3,
'Games' => 4,
'Applications' => 5,
'Pictures' => 8,
'Anime' => 9,
'Comics' => 10,
'Books' => 11,
'Audiobooks' => 17
)
)
)
);
public function collectData() {
if(!empty($this->getInput('q'))) {
$html = getSimpleHTMLDOM(
self::URI .
'files/?category=' .
rawurlencode($this->getInput('category')) .
'&subcategory=All&quality=All&seeded=2&external=2&query=' .
urlencode($this->getInput('q')) .
'&uid=0&sort='
) or returnServerError('Could not request Demonoid.');
} elseif(!empty($this->getInput('catOnly'))) {
$html = getSimpleHTMLDOM(
self::URI .
'files/?uid=0&category=' .
rawurlencode($this->getInput('catOnly')) .
'&subcategory=0&language=0&seeded=2&quality=0&query=&sort='
) or returnServerError('Could not request Demonoid.');
} elseif(!empty($this->getInput('userid'))) {
$html = getSimpleHTMLDOM(
self::URI .
'files/?uid=' .
rawurlencode($this->getInput('userid')) .
'&seeded=2'
) or returnServerError('Could not request Demonoid.');
} else {
returnServerError('Invalid parameters !');
}
if(preg_match('~No torrents found~', $html)) {
return;
}
$table = $html->find('td[class=ctable_content_no_pad]', 0);
$cursorCount = 4;
$elementCount = 0;
while($elementCount != 40) {
$elementCount++;
$currentElement = $table->find('tr', $cursorCount);
if(preg_match('~items total~', $currentElement)) {
break;
}
$item = array();
//Do we have a date ?
if(preg_match('~Added.*?(.*)~', $currentElement->plaintext, $dateStr)) {
if(preg_match('~today~', $dateStr[0])) {
date_default_timezone_set('UTC');
$timestamp = mktime(0, 0, 0, gmdate('n'), gmdate('j'), gmdate('Y'));
} else {
preg_match('~(?<=ed on ).*\d+~', $currentElement->plaintext, $fullDateStr);
date_default_timezone_set('UTC');
$dateObj = strptime($fullDateStr[0], '%A, %b %d, %Y');
$timestamp = mktime(0, 0, 0, $dateObj['tm_mon'] + 1, $dateObj['tm_mday'], 1900 + $dateObj['tm_year']);
}
$cursorCount++;
}
$content = $table->find('tr', $cursorCount)->find('a', 1);
$cursorCount++;
$torrentInfo = $table->find('tr', $cursorCount);
$item['timestamp'] = $timestamp;
$item['title'] = $content->plaintext;
$item['id'] = self::URI . $content->href;
$item['uri'] = self::URI . $content->href;
$item['author'] = $torrentInfo->find('a[class=user]', 0)->plaintext;
$item['seeders'] = $torrentInfo->find('font[class=green]', 0)->plaintext;
$item['leechers'] = $torrentInfo->find('font[class=red]', 0)->plaintext;
$item['size'] = $torrentInfo->find('td', 3)->plaintext;
$item['content'] = 'Uploaded by ' . $item['author']
. ' , Size ' . $item['size']
. '<br>seeders: '
. $item['seeders']
. ' | leechers: '
. $item['leechers']
. '<br><a href="'
. $item['id']
. '">info page</a>';
$this->items[] = $item;
$cursorCount++;
}
}
}

112
bridges/DiscogsBridge.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
class DiscogsBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'DiscogsBridge';
const URI = 'https://www.discogs.com/';
const DESCRIPTION = 'Returns releases from discogs';
const PARAMETERS = array(
'Artist Releases' => array(
'artistid' => array(
'name' => 'Artist ID',
'type' => 'number',
)
),
'Label Releases' => array(
'labelid' => array(
'name' => 'Label ID',
'type' => 'number',
)
),
'User Wantlist' => array(
'username_wantlist' => array(
'name' => 'Username',
'type' => 'text',
)
),
'User Folder' => array(
'username_folder' => array(
'name' => 'Username',
'type' => 'text',
),
'folderid' => array(
'name' => 'Folder ID',
'type' => 'number',
)
)
);
public function collectData() {
if(!empty($this->getInput('artistid')) || !empty($this->getInput('labelid'))) {
if(!empty($this->getInput('artistid'))) {
$data = getContents('https://api.discogs.com/artists/'
. $this->getInput('artistid')
. '/releases?sort=year&sort_order=desc')
or returnServerError('Unable to query discogs !');
} elseif(!empty($this->getInput('labelid'))) {
$data = getContents('https://api.discogs.com/labels/'
. $this->getInput('labelid')
. '/releases?sort=year&sort_order=desc')
or returnServerError('Unable to query discogs !');
}
$jsonData = json_decode($data, true);
foreach($jsonData['releases'] as $release) {
$item = array();
$item['author'] = $release['artist'];
$item['title'] = $release['title'];
$item['id'] = $release['id'];
$resId = array_key_exists('main_release', $release) ? $release['main_release'] : $release['id'];
$item['uri'] = self::URI . $this->getInput('artistid') . '/release/' . $resId;
$item['timestamp'] = DateTime::createFromFormat('Y', $release['year'])->getTimestamp();
$item['content'] = $item['author'] . ' - ' . $item['title'];
$this->items[] = $item;
}
} elseif(!empty($this->getInput('username_wantlist')) || !empty($this->getInput('username_folder'))) {
if(!empty($this->getInput('username_wantlist'))) {
$data = getContents('https://api.discogs.com/users/'
. $this->getInput('username_wantlist')
. '/wants?sort=added&sort_order=desc')
or returnServerError('Unable to query discogs !');
$jsonData = json_decode($data, true)['wants'];
} elseif(!empty($this->getInput('username_folder'))) {
$data = getContents('https://api.discogs.com/users/'
. $this->getInput('username_folder')
. '/collection/folders/'
. $this->getInput('folderid')
.'/releases?sort=added&sort_order=desc')
or returnServerError('Unable to query discogs !');
$jsonData = json_decode($data, true)['releases'];
}
foreach($jsonData as $element) {
$infos = $element['basic_information'];
$item = array();
$item['title'] = $infos['title'];
$item['author'] = $infos['artists'][0]['name'];
$item['id'] = $infos['artists'][0]['id'];
$item['uri'] = self::URI . $infos['artists'][0]['id'] . '/release/' . $infos['id'];
$item['timestamp'] = strtotime($element['date_added']);
$item['content'] = $item['author'] . ' - ' . $item['title'];
$this->items[] = $item;
}
}
}
public function getURI() {
return self::URI;
}
public function getName() {
return static::NAME;
}
}

View File

@@ -0,0 +1,91 @@
<?php
class DribbbleBridge extends BridgeAbstract {
const MAINTAINER = 'quentinus95';
const NAME = 'Dribbble popular shots';
const URI = 'https://dribbble.com';
const CACHE_TIMEOUT = 1800;
const DESCRIPTION = 'Returns the newest popular shots from Dribbble.';
public function collectData(){
$html = getSimpleHTMLDOM(self::URI . '/shots')
or returnServerError('Error while downloading the website content');
$json = $this->loadEmbeddedJsonData($html);
foreach($html->find('li[id^="screenshot-"]') as $shot) {
$item = [];
$additional_data = $this->findJsonForShot($shot, $json);
if ($additional_data === null) {
$item['uri'] = self::URI . $shot->find('a', 0)->href;
$item['title'] = $shot->find('.dribbble-over strong', 0)->plaintext;
} else {
$item['timestamp'] = strtotime($additional_data['published_at']);
$item['uri'] = self::URI . $additional_data['path'];
$item['title'] = $additional_data['title'];
}
$item['author'] = trim($shot->find('.attribution-user a', 0)->plaintext);
$description = $shot->find('.comment', 0);
$item['content'] = $description === null ? '' : $description->plaintext;
$preview_path = $shot->find('picture source', 0)->attr['srcset'];
$item['content'] .= $this->getImageTag($preview_path, $item['title']);
$item['enclosures'] = [$this->getFullSizeImagePath($preview_path)];
$this->items[] = $item;
}
}
private function loadEmbeddedJsonData($html){
$json = [];
$scripts = $html->find('script');
foreach($scripts as $script) {
if(strpos($script->innertext, 'newestShots') !== false) {
// fix single quotes
$script->innertext = str_replace('\'', '"', $script->innertext);
// fix JavaScript JSON (why do they not adhere to the standard?)
$script->innertext = preg_replace('/(\w+):/i', '"\1":', $script->innertext);
// find beginning of JSON array
$start = strpos($script->innertext, '[');
// find end of JSON array, compensate for missing character!
$end = strpos($script->innertext, '];') + 1;
// convert JSON to PHP array
$json = json_decode(substr($script->innertext, $start, $end - $start), true);
break;
}
}
return $json;
}
private function findJsonForShot($shot, $json){
foreach($json as $element) {
if(strpos($shot->getAttribute('id'), (string)$element['id']) !== false) {
return $element;
}
}
return null;
}
private function getImageTag($preview_path, $title){
return sprintf(
'<br /> <a href="%s"><img src="%s" alt="%s" /></a>',
$this->getFullSizeImagePath($preview_path),
$preview_path,
$title
);
}
private function getFullSizeImagePath($preview_path){
return str_replace('_1x', '', $preview_path);
}
}

142
bridges/ETTVBridge.php Normal file
View File

@@ -0,0 +1,142 @@
<?php
class ETTVBridge extends BridgeAbstract {
const MAINTAINER = 'GregThib';
const NAME = 'ETTV';
const URI = 'https://www.ettv.tv/';
const DESCRIPTION = 'Returns list of 20 latest torrents for a specific search.';
const CACHE_TIMEOUT = 14400; // 4 hours
const PARAMETERS = array( array(
'query' => array(
'name' => 'Keywords',
'required' => true
),
'cat' => array(
'type' => 'list',
'name' => 'Category',
'values' => array(
'(ALL TYPES)' => '0',
'Anime: Movies' => '73',
'Anime: Dubbed/Subbed' => '74',
'Anime: Others' => '75',
'Books: Ebooks' => '53',
'Books: Magazines' => '54',
'Books: Comics' => '55',
'Books: Audio' => '56',
'Books: Others' => '68',
'Games: Windows' => '57',
'Games: Android' => '58',
'Games: Others' => '71',
'Movies: HD 1080p' => '1',
'Movies: HD 720p' => '2',
'Movies: UltraHD/4K' => '3',
'Movies: XviD' => '42',
'Movies: X264/H264' => '47',
'Movies: 3D' => '49',
'Movies: Dubs/Dual Audio' => '51',
'Movies: CAM/TS' => '65',
'Movies: BluRay Disc/Remux' => '66',
'Movies: DVDR' => '67',
'Movies: HEVC/x265' => '76',
'Music: MP3' => '59',
'Music: FLAC' => '60',
'Music: Music Videos' => '61',
'Music: Others' => '69',
'Software: Windows' => '62',
'Software: Android' => '63',
'Software: Mac' => '64',
'Software: Others' => '70',
'TV: HD/X264/H264' => '41',
'TV: SD/X264/H264' => '5',
'TV: TV Packs' => '7',
'TV: SD/XVID' => '50',
'TV: Sport' => '72',
'TV: HEVC/x265' => '77',
'Unsorted: Unsorted' => '78'
),
'defaultValue' => '(ALL TYPES)'
),
'status' => array(
'type' => 'list',
'name' => 'Status',
'values' => array(
'Active Transfers' => '0',
'Included Dead' => '1',
'Only Dead' => '2'
),
'defaultValue' => 'Included Dead'
),
'lang' => array(
'type' => 'list',
'name' => 'Lang',
'values' => array(
'(ALL)' => '0',
'Arabic' => '17',
'Chinese ' => '10',
'Danish' => '13',
'Dutch' => '11',
'English' => '1',
'Finnish' => '18',
'French' => '2',
'German' => '3',
'Greek' => '15',
'Hindi' => '8',
'Italian' => '4',
'Japanese' => '5',
'Korean' => '9',
'Polish' => '14',
'Russian' => '7',
'Spanish' => '6',
'Turkish' => '16'
),
'defaultValue' => '(ALL)'
)
));
public function collectData(){
// No control on inputs, because all have defaultValue set
$query_str = 'torrents-search.php';
$query_str .= '?search=' . urlencode('+'.str_replace(' ', ' +', $this->getInput('query')));
$query_str .= '&cat=' . $this->getInput('cat');
$query_str .= 'incldead&=' . $this->getInput('status');
$query_str .= '&lang=' . $this->getInput('lang');
$query_str .= '&sort=id&order=desc';
// Get results page
$html = getSimpleHTMLDOM(self::URI . $query_str)
or returnServerError('Could not request ' . $this->getName());
// Loop on each entry
foreach($html->find('table.table tr') as $element) {
if($element->parent->tag == 'thead') continue;
$entry = $element->find('td', 1)->find('a', 0);
// retrieve result page to get more details
$link = rtrim(self::URI, '/') . $entry->href;
$page = getSimpleHTMLDOM($link)
or returnServerError('Could not request page ' . $link);
// get details & download links
$details = $page->find('fieldset.download table', 0); // WHAT?? It should be the second one…
$dllinks = $page->find('div#downloadbox table', 0);
// fill item
$item = array();
$item['author'] = $details->children(6)->children(1)->plaintext;
$item['title'] = $entry->title;
$item['uri'] = $dllinks->children(0)->children(0)->children(0)->href;
$item['timestamp'] = strtotime($details->children(7)->children(1)->plaintext);
$item['content'] = '';
$item['content'] .= '<br/><b>Name: </b>' . $details->children(0)->children(1)->innertext;
$item['content'] .= '<br/><b>Lang: </b>' . $details->children(3)->children(1)->innertext;
$item['content'] .= '<br/><b>Size: </b>' . $details->children(4)->children(1)->innertext;
$item['content'] .= '<br/><b>Hash: </b>' . $details->children(5)->children(1)->innertext;
foreach($dllinks->children(0)->children(1)->find('a') as $dl) {
$item['content'] .= '<br/>' . $dl->outertext;
}
$item['content'] .= '<br/><br/>' . $details->children(1)->children(0)->innertext;
$this->items[] = $item;
}
}
}

View File

@@ -1,7 +1,7 @@
<?php
class EZTVBridge extends BridgeAbstract {
const MAINTAINER = "alexAubin";
const MAINTAINER = 'alexAubin';
const NAME = 'EZTV';
const URI = 'https://eztv.ch/';
const DESCRIPTION = 'Returns list of *recent* torrents for a specific show
@@ -23,15 +23,15 @@ on EZTV. Get showID from URLs in https://eztv.ch/shows/showID/show-full-name.';
$relativeDays = 0;
$relativeHours = 0;
foreach(explode(" ", $relativeReleaseTime) as $relativeTimeElement) {
if(substr($relativeTimeElement, -1) == "d") $relativeDays = substr($relativeTimeElement, 0, -1);
if(substr($relativeTimeElement, -1) == "h") $relativeHours = substr($relativeTimeElement, 0, -1);
foreach(explode(' ', $relativeReleaseTime) as $relativeTimeElement) {
if(substr($relativeTimeElement, -1) == 'd') $relativeDays = substr($relativeTimeElement, 0, -1);
if(substr($relativeTimeElement, -1) == 'h') $relativeHours = substr($relativeTimeElement, 0, -1);
}
return mktime(date('h') - $relativeHours, 0, 0, date('m'), date('d') - $relativeDays, date('Y'));
}
// Loop on show ids
$showList = explode(",", $this->getInput('i'));
$showList = explode(',', $this->getInput('i'));
foreach($showList as $showID) {
// Get show page

147
bridges/ElloBridge.php Normal file
View File

@@ -0,0 +1,147 @@
<?php
class ElloBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'Ello Bridge';
const URI = 'https://ello.co/';
const CACHE_TIMEOUT = 4800; //2hours
const DESCRIPTION = 'Returns the newest posts for Ello';
const PARAMETERS = array(
'By User' => array(
'u' => array(
'name' => 'Username',
'required' => true,
'title' => 'Username'
)
),
'Search' => array(
's' => array(
'name' => 'Search',
'required' => true,
'title' => 'Search'
)
)
);
public function collectData() {
$header = array(
'Authorization: Bearer ' . $this->getAPIKey()
);
if(!empty($this->getInput('u'))) {
$postData = getContents(self::URI . 'api/v2/users/~' . urlencode($this->getInput('u')) . '/posts', $header) or
returnServerError('Unable to query Ello API.');
} else {
$postData = getContents(self::URI . 'api/v2/posts?terms=' . urlencode($this->getInput('s')), $header) or
returnServerError('Unable to query Ello API.');
}
$postData = json_decode($postData);
$count = 0;
foreach($postData->posts as $post) {
$item = array();
$item['author'] = $this->getUsername($post, $postData);
$item['timestamp'] = strtotime($post->created_at);
$item['title'] = strip_tags($this->findText($post->summary));
$item['content'] = $this->getPostContent($post->body);
$item['enclosures'] = $this->getEnclosures($post, $postData);
$item['uri'] = self::URI . $item['author'] . '/post/' . $post->token;
$content = $post->body;
$this->items[] = $item;
$count += 1;
}
}
public function findText($path) {
foreach($path as $summaryElement) {
if($summaryElement->kind == 'text') {
return $summaryElement->data;
}
}
return '';
}
public function getPostContent($path) {
$content = '';
foreach($path as $summaryElement) {
if($summaryElement->kind == 'text') {
$content .= $summaryElement->data;
} elseif ($summaryElement->kind == 'image') {
$alt = '';
if(property_exists($summaryElement->data, 'alt')) {
$alt = $summaryElement->data->alt;
}
$content .= '<img src="' . $summaryElement->data->url . '" alt="' . $alt . '" />';
}
}
return $content;
}
public function getEnclosures($post, $postData) {
$assets = [];
foreach($post->links->assets as $asset) {
foreach($postData->linked->assets as $assetLink) {
if($asset == $assetLink->id) {
$assets[] = $assetLink->attachment->original->url;
break;
}
}
}
return $assets;
}
public function getUsername($post, $postData) {
foreach($postData->linked->users as $user) {
if($user->id == $post->links->author->id) {
return $user->username;
}
}
}
public function getAPIKey() {
$cache = Cache::create('FileCache');
$cache->setPath(CACHE_DIR);
$cache->setParameters(['key']);
$key = $cache->loadData();
if($key == null) {
$keyInfo = getContents(self::URI . 'api/webapp-token') or
returnServerError('Unable to get token.');
$key = json_decode($keyInfo)->token->access_token;
$cache->saveData($key);
}
return $key;
}
public function getName(){
if(!is_null($this->getInput('u'))) {
return $this->getInput('u') . ' - Ello Bridge';
}
return parent::getName();
}
}

View File

@@ -95,7 +95,7 @@ EOD;
. $pageID
. '&cursor={"card_id"%3A"videos"%2C"has_next_page"%3Atrue}&surface=mobile_page_home&unit_count=8';
$fileContent = file_get_contents($requestString);
$fileContent = getContents($requestString);
$articleIndex = 0;
$maxArticle = 3;
@@ -103,19 +103,19 @@ EOD;
$html = $this->buildContent($fileContent);
$author = $this->getInput('u');
foreach($html->find("article") as $content) {
foreach($html->find('article') as $content) {
$item = array();
$item['uri'] = "http://touch.facebook.com"
. $content->find("div[class='_52jc _5qc4 _24u0 _36xo']", 0)->find("a", 0)->getAttribute("href");
$item['uri'] = 'http://touch.facebook.com'
. $content->find("div[class='_52jc _5qc4 _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href');
if($content->find("header", 0) !== null) {
$content->find("header", 0)->innertext = "";
if($content->find('header', 0) !== null) {
$content->find('header', 0)->innertext = '';
}
if($content->find("footer", 0) !== null) {
$content->find("footer", 0)->innertext = "";
if($content->find('footer', 0) !== null) {
$content->find('footer', 0)->innertext = '';
}
//Remove html nodes, keep only img, links, basic formatting
@@ -168,7 +168,7 @@ EOD;
$regex = implode(
'',
array(
"/timeline_unit",
'/timeline_unit',
"\\\\\\\\u00253A1",
"\\\\\\\\u00253A([0-9]*)",
"\\\\\\\\u00253A([0-9]*)",
@@ -182,29 +182,29 @@ EOD;
return implode(
'',
array(
"https://touch.facebook.com/pages_reaction_units/more/?page_id=",
'https://touch.facebook.com/pages_reaction_units/more/?page_id=',
$pageID,
"&cursor=%7B%22timeline_cursor%22%3A%22timeline_unit%3A1%3A",
'&cursor=%7B%22timeline_cursor%22%3A%22timeline_unit%3A1%3A',
$result[1],
"%3A",
'%3A',
$result[2],
"%3A",
'%3A',
$result[3],
"%3A",
'%3A',
$result[4],
"%22%2C%22timeline_section_cursor%22%3A%7B%7D%2C%22",
"has_next_page%22%3Atrue%7D&surface=mobile_page_home&unit_count=3"
'%22%2C%22timeline_section_cursor%22%3A%7B%7D%2C%22',
'has_next_page%22%3Atrue%7D&surface=mobile_page_home&unit_count=3'
)
);
}
//Builds the HTML from the encoded JS that Facebook provides.
private function buildContent($pageContent){
$regex = "/\\\"html\\\":\\\"(.*?)\\\",\\\"replace/";
// The html ends with:
// /div>","replaceifexists
$regex = '/\\"html\\":(\".+\/div>"),"replace/';
preg_match($regex, $pageContent, $result);
return str_get_html(html_entity_decode(json_decode('"' . $result[1] . '"')));
return str_get_html(html_entity_decode(json_decode($result[1])));
}
@@ -214,7 +214,7 @@ EOD;
$ctx = stream_context_create(array(
'http' => array(
'user_agent' => "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0",
'user_agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
)
)
@@ -222,12 +222,12 @@ EOD;
$a = file_get_contents($pageURL, 0, $ctx);
//First request to get the cookie
$cookies = "";
$cookies = '';
foreach($http_response_header as $hdr) {
if(strpos($hdr, "Set-Cookie") !== false) {
$cLine = explode(":", $hdr)[1];
$cLine = explode(";", $cLine)[0];
$cookies .= ";" . $cLine;
if(strpos($hdr, 'Set-Cookie') !== false) {
$cLine = explode(':', $hdr)[1];
$cLine = explode(';', $cLine)[0];
$cookies .= ';' . $cLine;
}
}
@@ -239,7 +239,7 @@ EOD;
$context = stream_context_create(array(
'http' => array(
'user_agent' => "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0",
'user_agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
'header' => 'Cookie: ' . $cookies
)
)
@@ -247,12 +247,12 @@ EOD;
$pageContent = file_get_contents($page, 0, $context);
if(strpos($pageContent, "signup-button") != false) {
if(strpos($pageContent, 'signup-button') != false) {
return -1;
}
//Get the page ID if we don't have a captcha
$regex = "/page_id=([0-9]*)&/";
$regex = '/page_id=([0-9]*)&/';
preg_match($regex, $pageContent, $matches);
if(count($matches) > 0) {
@@ -260,7 +260,7 @@ EOD;
}
//Get the page ID if we do have a captcha
$regex = "/\"pageID\":\"([0-9]*)\"/";
$regex = '/"pageID":"([0-9]*)"/';
preg_match($regex, $pageContent, $matches);
return $matches[1];

54
bridges/FDroidBridge.php Normal file
View File

@@ -0,0 +1,54 @@
<?php
class FDroidBridge extends BridgeAbstract {
const MAINTAINER = 'Mitsukarenai';
const NAME = 'F-Droid Bridge';
const URI = 'https://f-droid.org/';
const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours
const DESCRIPTION = 'Returns latest added/updated apps on the open-source Android apps repository F-Droid';
const PARAMETERS = array( array(
'u' => array(
'name' => 'Widget selection',
'type' => 'list',
'required' => true,
'values' => array(
'Latest added apps' => 'added',
'Latest updated apps' => 'updated'
)
)
));
public function collectData(){
$url = self::URI;
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request F-Droid.');
// targetting the corresponding widget based on user selection
// "updated" is the 4th widget on the page, "added" is the 5th
switch($this->getInput('u')) {
case 'updated':
$html_widget = $html->find('div.sidebar-widget', 4);
break;
default:
$html_widget = $html->find('div.sidebar-widget', 5);
break;
}
// and now extracting app info from the selected widget (and yeah turns out icons are of heterogeneous sizes)
foreach($html_widget->find('a') as $element) {
$item = array();
$item['uri'] = self::URI . $element->href;
$item['title'] = $element->find('h4', 0)->plaintext;
$item['icon'] = $element->find('img', 0)->src;
$item['summary'] = $element->find('span.package-summary', 0)->plaintext;
$item['content'] = '
<a href="'.$item['uri'].'">
<img alt="" style="max-height:128px" src="'.$item['icon'].'">
</a><br>'.$item['summary'];
$this->items[] = $item;
}
}
}

View File

@@ -1,34 +1,257 @@
<?php
class FacebookBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const MAINTAINER = 'teromene, logmanoriginal';
const NAME = 'Facebook';
const URI = 'https://www.facebook.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
please insert the parameter as follow : myExamplePage/132621766841117';
const PARAMETERS = array( array(
'u' => array(
'name' => 'Username',
'required' => true
),
'media_type' => array(
'name' => 'Media type',
'type' => 'list',
'required' => false,
'values' => array(
'All' => 'all',
'Video' => 'video',
'No Video' => 'novideo'
const PARAMETERS = array(
'User' => array(
'u' => array(
'name' => 'Username',
'required' => true
),
'defaultValue' => 'all'
'media_type' => array(
'name' => 'Media type',
'type' => 'list',
'required' => false,
'values' => array(
'All' => 'all',
'Video' => 'video',
'No Video' => 'novideo'
),
'defaultValue' => 'all'
),
'skip_reviews' => array(
'name' => 'Skip reviews',
'type' => 'checkbox',
'required' => false,
'defaultValue' => false,
'title' => 'Feed includes reviews when checked'
)
),
'Group' => array(
'g' => array(
'name' => 'Group',
'type' => 'text',
'required' => true,
'exampleValue' => 'https://www.facebook.com/groups/743149642484225',
'title' => 'Insert group name or facebook group URL'
)
)
));
);
private $authorName = '';
private $groupName = '';
public function collectData(){
public function getURI() {
$uri = self::URI;
switch($this->queriedContext) {
case 'Group':
$uri .= 'groups/' . $this->sanitizeGroup(filter_var($this->getInput('g'), FILTER_SANITIZE_URL));
break;
}
return $uri .= '?_fb_noscript=1';
}
public function collectData() {
switch($this->queriedContext) {
case 'Group':
$this->collectGroupData();
break;
case 'User':
$this->collectUserData();
break;
default:
returnClientError('Unknown context: "' . $this->queriedContext . '"!');
}
}
#region Group
private function collectGroupData() {
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n");
$html = getSimpleHTMLDOM($this->getURI(), $header)
or returnServerError('Failed loading facebook page: ' . $this->getURI());
if(!$this->isPublicGroup($html)) {
returnClientError('This group is not public! RSS-Bridge only supports public groups!');
}
defaultLinkTo($html, substr(self::URI, 0, strlen(self::URI) - 1));
$this->groupName = $this->extractGroupName($html);
$posts = $html->find('div.userContentWrapper')
or returnServerError('Failed finding posts!');
foreach($posts as $post) {
$item = array();
$item['uri'] = $this->extractGroupURI($post);
$item['title'] = $this->extractGroupTitle($post);
$item['author'] = $this->extractGroupAuthor($post);
$item['content'] = $this->extractGroupContent($post);
$item['timestamp'] = $this->extractGroupTimestamp($post);
$item['enclosures'] = $this->extractGroupEnclosures($post);
$this->items[] = $item;
}
}
private function sanitizeGroup($group) {
if(filter_var(
$group,
FILTER_VALIDATE_URL,
FILTER_FLAG_HOST_REQUIRED | FILTER_FLAG_PATH_REQUIRED)) {
// User provided a URL
$urlparts = parse_url($group);
if($urlparts['host'] !== parse_url(self::URI)['host']
&& 'www.' . $urlparts['host'] !== parse_url(self::URI)['host']) {
returnClientError('The host you provided is invalid! Received "'
. $urlparts['host']
. '", expected "'
. parse_url(self::URI)['host']
. '"!');
}
return explode('/', $urlparts['path'])[2];
} elseif(strpos($group, '/') !== false) {
returnClientError('The group you provided is invalid: ' . $group);
} else {
return $group;
}
}
private function isPublicGroup($html) {
// Facebook redirects to the groups about page for non-public groups
$about = $html->find('#pagelet_group_about', 0);
return !($about);
}
private function extractGroupName($html) {
$ogtitle = $html->find('meta[property="og:title"]', 0)
or returnServerError('Unable to find group title!');
return htmlspecialchars_decode($ogtitle->content, ENT_QUOTES);
}
private function extractGroupURI($post) {
$elements = $post->find('a')
or returnServerError('Unable to find URI!');
foreach($elements as $anchor) {
// Find the one that is a permalink
if(strpos($anchor->href, 'permalink') !== false) {
return $anchor->href;
}
}
return null;
}
private function extractGroupContent($post) {
$content = $post->find('div.userContent', 0)
or returnServerError('Unable to find user content!');
return $content->innertext . $content->next_sibling()->innertext;
}
private function extractGroupTimestamp($post) {
$element = $post->find('abbr[data-utime]', 0)
or returnServerError('Unable to find timestamp!');
return $element->getAttribute('data-utime');
}
private function extractGroupAuthor($post) {
$element = $post->find('img', 0)
or returnServerError('Unable to find author information!');
return $element->{'aria-label'};
}
private function extractGroupEnclosures($post) {
$elements = $post->find('div.userContent', 0)->next_sibling()->find('img');
$enclosures = array();
foreach($elements as $enclosure) {
$enclosures[] = $enclosure->src;
}
return empty($enclosures) ? null : $enclosures;
}
private function extractGroupTitle($post) {
$element = $post->find('h5', 0)
or returnServerError('Unable to find title!');
if(strpos($element->plaintext, 'shared') === false) {
$content = strip_tags($this->extractGroupContent($post));
return $this->extractGroupAuthor($post)
. ' posted: '
. substr(
$content,
0,
strpos(wordwrap($content, 64), "\n")
)
. '...';
}
return $element->plaintext;
}
#endregion
private function collectUserData(){
//Extract a string using start and end delimiters
function extractFromDelimiters($string, $start, $end){
@@ -46,7 +269,7 @@ class FacebookBridge extends BridgeAbstract {
if(is_array($matches) && count($matches) > 1) {
$link = $matches[1];
if(strpos($link, '/') === 0)
$link = self::URI . $link . '"';
$link = self::URI . $link;
if(strpos($link, 'facebook.com/l.php?u=') !== false)
$link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&'));
return ' href="' . $link . '"';
@@ -95,18 +318,16 @@ class FacebookBridge extends BridgeAbstract {
if (isset($_SESSION['captcha_fields'], $_SESSION['captcha_action'])) {
$captcha_action = $_SESSION['captcha_action'];
$captcha_fields = $_SESSION['captcha_fields'];
$captcha_fields['captcha_response'] = preg_replace("/[^a-zA-Z0-9]+/", "", $_POST['captcha_response']);
$http_options = array(
'http' => array(
'method' => 'POST',
'user_agent' => ini_get('user_agent'),
'header' => array("Content-type:
application/x-www-form-urlencoded\r\nReferer: $captcha_action\r\nCookie: noscript=1\r\n"),
'content' => http_build_query($captcha_fields)
),
$captcha_fields['captcha_response'] = preg_replace('/[^a-zA-Z0-9]+/', '', $_POST['captcha_response']);
$header = array("Content-type:
application/x-www-form-urlencoded\r\nReferer: $captcha_action\r\nCookie: noscript=1\r\n");
$opts = array(
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => http_build_query($captcha_fields)
);
$context = stream_context_create($http_options);
$html = getContents($captcha_action, false, $context);
$html = getContents($captcha_action, $header, $opts);
if($html === false) {
returnServerError('Failed to submit captcha response back to Facebook');
@@ -120,24 +341,46 @@ class FacebookBridge extends BridgeAbstract {
//Retrieve page contents
if(is_null($html)) {
$http_options = array(
'http' => array(
'method' => 'GET',
'user_agent' => ini_get('user_agent'),
'header' => 'Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n"
)
);
$context = stream_context_create($http_options);
if(!strpos($this->getInput('u'), "/")) {
$html = getSimpleHTMLDOM(self::URI . urlencode($this->getInput('u')) . '?_fb_noscript=1',
false,
$context)
or returnServerError('No results for this query.');
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n");
// Check if the user provided a fully qualified URL
if (filter_var($this->getInput('u'), FILTER_VALIDATE_URL)) {
$urlparts = parse_url($this->getInput('u'));
if($urlparts['host'] !== parse_url(self::URI)['host']) {
returnClientError('The host you provided is invalid! Received "'
. $urlparts['host']
. '", expected "'
. parse_url(self::URI)['host']
. '"!');
}
if(!array_key_exists('path', $urlparts)
|| $urlparts['path'] === '/') {
returnClientError('The URL you provided doesn\'t contain the user name!');
}
$user = explode('/', $urlparts['path'])[1];
$html = getSimpleHTMLDOM(self::URI . urlencode($user) . '?_fb_noscript=1', $header)
or returnServerError('No results for this query.');
} else {
$html = getSimpleHTMLDOM(self::URI . 'pages/' . $this->getInput('u') . '?_fb_noscript=1',
false,
$context)
or returnServerError('No results for this query.');
// First character cannot be a forward slash
if(strpos($this->getInput('u'), '/') === 0) {
returnClientError('Remove leading slash "/" from the username!');
}
if(!strpos($this->getInput('u'), '/')) {
$html = getSimpleHTMLDOM(self::URI . urlencode($this->getInput('u')) . '?_fb_noscript=1', $header)
or returnServerError('No results for this query.');
} else {
$html = getSimpleHTMLDOM(self::URI . 'pages/' . $this->getInput('u') . '?_fb_noscript=1', $header)
or returnServerError('No results for this query.');
}
}
}
@@ -155,7 +398,7 @@ class FacebookBridge extends BridgeAbstract {
//Show captcha filling form to the viewer, proxying the captcha image
$img = base64_encode(getContents($captcha->find('img', 0)->src));
header('HTTP/1.1 500 ' . Http::getMessageForCode(500));
http_response_code(500);
header('Content-Type: text/html');
$message = <<<EOD
<form method="post" action="?{$_SERVER['QUERY_STRING']}">
@@ -171,6 +414,12 @@ EOD;
}
//No captcha? We can carry on retrieving page contents :)
//First, we check wether the page is public or not
$loginForm = $html->find('._585r', 0);
if($loginForm != null) {
returnServerError('You must be logged in to view this page. This is not supported by RSS-Bridge.');
}
$element = $html
->find('#pagelet_timeline_main_column')[0]
->children(0)
@@ -196,6 +445,12 @@ EOD;
$posts = array($cell);
}
// Optionally skip reviews
if($this->getInput('skip_reviews')
&& !is_null($cell->find('#review_composer_container', 0))) {
continue;
}
foreach($posts as $post) {
// Check media type
switch($this->getInput('media_type')) {
@@ -266,7 +521,7 @@ EOD;
);
//Retrieve date of the post
$date = $post->find("abbr")[0];
$date = $post->find('abbr')[0];
if(isset($date) && $date->hasAttribute('data-utime')) {
$date = $date->getAttribute('data-utime');
} else {
@@ -281,9 +536,11 @@ EOD;
if(strlen($title) > 64)
$title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
$uri = self::URI . $post->find('abbr')[0]->parent()->getAttribute('href');
//Build and add final item
$item['uri'] = self::URI . $post->find('abbr')[0]->parent()->getAttribute('href');
$item['content'] = $content;
$item['uri'] = htmlspecialchars_decode($uri);
$item['content'] = htmlspecialchars_decode($content);
$item['title'] = $title;
$item['author'] = $author;
$item['timestamp'] = $date;
@@ -295,9 +552,22 @@ EOD;
}
public function getName(){
if(!empty($this->authorName)) {
return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName
. ' - Facebook Bridge';
switch($this->queriedContext) {
case 'User':
if(!empty($this->authorName)) {
return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName
. ' - Facebook Bridge';
}
break;
case 'Group':
if(!empty($this->groupName)) {
return $this->groupName . ' - Facebook Bridge';
}
break;
}
return parent::getName();

View File

@@ -8,17 +8,22 @@ class FierPandaBridge extends BridgeAbstract {
const DESCRIPTION = 'Returns latest articles from Fier Panda.';
public function collectData(){
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request Fier Panda.');
foreach($html->find('div.container-content article') as $element) {
defaultLinkTo($html, static::URI);
foreach($html->find('article') as $article) {
$item = array();
$item['uri'] = $this->getURI() . $element->find('a', 0)->href;
$item['title'] = trim($element->find('h1 a', 0)->innertext);
// Remove the link at the end of the article
$element->find('p a', 0)->outertext = '';
$item['content'] = $element->find('p', 0)->innertext;
$item['uri'] = $article->find('a', 0)->href;
$item['title'] = $article->find('a', 0)->title;
$this->items[] = $item;
}
}
}

View File

@@ -26,11 +26,34 @@ class FilterBridge extends FeedExpander {
),
'defaultValue' => 'permit',
),
'title_from_content' => array(
'name' => 'Generate title from content',
'type' => 'checkbox',
'required' => false,
)
));
protected function parseItem($newItem){
$item = parent::parseItem($newItem);
if($this->getInput('title_from_content') && array_key_exists('content', $item)) {
$content = str_get_html($item['content']);
$pos = strpos($item['content'], ' ', 50);
$item['title'] = substr(
$content->plaintext,
0,
$pos
);
if(strlen($content->plaintext) >= $pos) {
$item['title'] .= '...';
}
}
switch(true) {
case $this->getFilterType() === 'permit':
if (preg_match($this->getFilter(), $item['title'])) {

View File

@@ -30,30 +30,76 @@ class FlickrBridge extends BridgeAbstract {
'title' => 'Insert username (as shown in the address bar)',
'exampleValue' => 'flickr'
)
),
)
);
public function collectData(){
switch($this->queriedContext) {
case 'Explore':
$key = 'photos';
$filter = 'photo-lite-models';
$html = getSimpleHTMLDOM(self::URI . 'explore')
or returnServerError('Could not request Flickr.');
break;
case 'By keyword':
$key = 'photos';
$filter = 'photo-lite-models';
$html = getSimpleHTMLDOM(self::URI . 'search/?q=' . urlencode($this->getInput('q')) . '&s=rec')
or returnServerError('No results for this query.');
break;
case 'By username':
$key = 'photoPageList';
$filter = 'photo-models';
$html = getSimpleHTMLDOM(self::URI . 'photos/' . urlencode($this->getInput('u')))
or returnServerError('Requested username can\'t be found.');
break;
default:
returnClientError('Invalid context: ' . $this->queriedContext);
}
$model_json = $this->extractJsonModel($html);
$photo_models = $this->getPhotoModels($model_json, $filter);
foreach($photo_models as $model) {
$item = array();
/* Author name depends on scope. On a keyword search the
* author is part of the picture data. On a username search
* the author is part of the owner data.
*/
if(array_key_exists('username', $model)) {
$item['author'] = $model['username'];
} elseif (array_key_exists('owner', reset($model_json)[0])) {
$item['author'] = reset($model_json)[0]['owner']['username'];
}
$item['title'] = (array_key_exists('title', $model) ? $model['title'] : 'Untitled');
$item['uri'] = self::URI . 'photo.gne?id=' . $model['id'];
$description = (array_key_exists('description', $model) ? $model['description'] : '');
$item['content'] = '<a href="'
. $item['uri']
. '"><img src="'
. $this->extractContentImage($model)
. '" style="max-width: 640px; max-height: 480px;"/></a><br><p>'
. $description
. '</p>';
$item['enclosures'] = $this->extractEnclosures($model);
$this->items[] = $item;
}
}
private function extractJsonModel($html) {
// Find SCRIPT containing JSON data
$model = $html->find('.modelExport', 0);
$model_text = $model->innertext;
@@ -62,59 +108,79 @@ class FlickrBridge extends BridgeAbstract {
$start = strpos($model_text, 'modelExport:') + strlen('modelExport:');
$end = strpos($model_text, 'auth:') - strlen('auth:');
// Dissect JSON data and remove trailing comma
// Extract JSON data, remove trailing comma
$model_text = trim(substr($model_text, $start, $end - $start));
$model_text = substr($model_text, 0, strlen($model_text) - 1);
$model_json = json_decode($model_text, true);
return json_decode($model_text, true);
foreach($html->find('.photo-list-photo-view') as $element) {
// Get the styles
$style = explode(';', $element->style);
// Get the background-image style
$backgroundImage = explode(':', end($style));
// URI type : url(//cX.staticflickr.com/X/XXXXX/XXXXXXXXX.jpg)
$imageURI = trim(str_replace(['url(', ')'], '', end($backgroundImage)));
// Get the image ID
$imageURIs = explode('_', basename($imageURI));
$imageID = reset($imageURIs);
// Use JSON data to build items
foreach(reset($model_json)[0][$key]['_data'] as $element) {
if($element['id'] === $imageID) {
$item = array();
/* Author name depends on scope. On a keyword search the
* author is part of the picture data. On a username search
* the author is part of the owner data.
*/
if(array_key_exists('username', $element)) {
$item['author'] = $element['username'];
} elseif (array_key_exists('owner', reset($model_json)[0])) {
$item['author'] = reset($model_json)[0]['owner']['username'];
}
$item['title'] = (array_key_exists('title', $element) ? $element['title'] : 'Untitled');
$item['uri'] = self::URI . 'photo.gne?id=' . $imageID;
$description = (array_key_exists('description', $element) ? $element['description'] : '');
$item['content'] = '<a href="'
. $item['uri']
. '"><img src="'
. $imageURI
. '" /></a><br><p>'
. $description
. '</p>';
$this->items[] = $item;
break;
}
}
}
}
private function getPhotoModels($json, $filter) {
// The JSON model contains a "legend" array, where each element contains
// the path to an element in the "main" object
$photo_models = array();
foreach($json['legend'] as $legend) {
$photo_model = $json['main'];
foreach($legend as $element) { // Traverse tree
$photo_model = $photo_model[$element];
}
// We are only interested in content
if($photo_model['_flickrModelRegistry'] === $filter) {
$photo_models[] = $photo_model;
}
}
return $photo_models;
}
private function extractEnclosures($model) {
$areas = array();
foreach($model['sizes'] as $size) {
$areas[$size['width'] * $size['height']] = $size['url'];
}
return array($this->fixURL(max($areas)));
}
private function extractContentImage($model) {
$areas = array();
$limit = 320 * 240;
foreach($model['sizes'] as $size) {
$image_area = $size['width'] * $size['height'];
if($image_area >= $limit) {
$areas[$image_area] = $size['url'];
}
}
return $this->fixURL(min($areas));
}
private function fixURL($url) {
// For some reason the image URLs don't include the protocol (https)
if(strpos($url, '//') === 0) {
$url = 'https:' . $url;
}
return $url;
}
}

View File

@@ -15,47 +15,47 @@ class FootitoBridge extends BridgeAbstract {
$content = trim($element->innertext);
$content = str_replace(
"<img",
'<img',
"<img style='float : left;'",
$content );
$content = str_replace(
"class=\"logo\"",
'class="logo"',
"style='float : left;'",
$content );
$content = str_replace(
"class=\"contenu\"",
'class="contenu"',
"style='margin-left : 60px;'",
$content );
$content = str_replace(
"class=\"responsive-comment\"",
'class="responsive-comment"',
"style='border-top : 1px #DDD solid; background-color : white; padding : 10px;'",
$content );
$content = str_replace(
"class=\"jaime\"",
'class="jaime"',
"style='display : none;'",
$content );
$content = str_replace(
"class=\"auteur-event responsive\"",
'class="auteur-event responsive"',
"style='display : none;'",
$content );
$content = str_replace(
"class=\"report-abuse-button\"",
'class="report-abuse-button"',
"style='display : none;'",
$content );
$content = str_replace(
"class=\"reaction clearfix\"",
'class="reaction clearfix"',
"style='margin : 10px 0px; padding : 5px; border-bottom : 1px #DDD solid;'",
$content );
$content = str_replace(
"class=\"infos\"",
'class="infos"',
"style='font-size : 0.7em;'",
$content );

41
bridges/ForGifsBridge.php Executable file
View File

@@ -0,0 +1,41 @@
<?php
class ForGifsBridge extends FeedExpander {
const MAINTAINER = 'logmanoriginal';
const NAME = 'forgifs Bridge';
const URI = 'https://forgifs.com';
const DESCRIPTION = 'Returns the forgifs feed with actual gifs instead of images';
public function collectData() {
$this->collectExpandableDatas('https://forgifs.com/gallery/srss/7');
}
protected function parseItem($feedItem) {
$item = parent::parseItem($feedItem);
$content = str_get_html($item['content']);
$img = $content->find('img', 0);
$poster = $img->src;
// The actual gif is the same path but its id must be decremented by one.
// Example:
// http://forgifs.com/gallery/d/279419-2/Reporter-videobombed-shoulder-checks.gif
// http://forgifs.com/gallery/d/279418-2/Reporter-videobombed-shoulder-checks.gif
// Notice how this changes ----------^
// Now let's extract that number and do some math
// Notice: Technically we could also load the content page but that would
// require unnecessary traffic. As long as it works...
$num = substr($img->src, 29, 6);
$num -= 1;
$img->src = substr_replace($img->src, $num, 29, strlen($num));
$img->width = 'auto';
$img->height = 'auto';
$item['content'] = $content;
return $item;
}
}

View File

@@ -30,7 +30,7 @@ class FourchanBridge extends BridgeAbstract {
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError("Could not request 4chan, thread not found");
or returnServerError('Could not request 4chan, thread not found');
foreach($html->find('div.postContainer') as $element) {
$item = array();

View File

@@ -10,6 +10,7 @@ class GelbooruBridge extends DanbooruBridge {
const PATHTODATA = '.thumb';
const IDATTRIBUTE = 'id';
const TAGATTRIBUTE = 'title';
const PIDBYPAGE = 63;
@@ -19,4 +20,16 @@ class GelbooruBridge extends DanbooruBridge {
. ($this->getInput('p') ? ($this->getInput('p') - 1) * static::PIDBYPAGE : '')
. '&tags=' . urlencode($this->getInput('t'));
}
protected function getTags($element){
$tags = parent::getTags($element);
$tags = explode(' ', $tags);
// Remove statistics from the tags list (identified by colon)
foreach($tags as $key => $tag) {
if(strpos($tag, ':') !== false) unset($tags[$key]);
}
return implode(' ', $tags);
}
}

View File

@@ -0,0 +1,164 @@
<?php
class GitHubGistBridge extends BridgeAbstract {
const NAME = 'GitHubGist comment bridge';
const URI = 'https://gist.github.com';
const DESCRIPTION = 'Generates feeds for Gist comments';
const MAINTAINER = 'logmanoriginal';
const CACHE_TIMEOUT = 3600;
const PARAMETERS = array(array(
'id' => array(
'name' => 'Gist',
'type' => 'text',
'required' => true,
'title' => 'Insert Gist ID or URI',
'exampleValue' => '2646763, https://gist.github.com/2646763'
)
));
private $filename;
public function getURI() {
$id = $this->getInput('id') ?: '';
$urlpath = parse_url($id, PHP_URL_PATH);
if($urlpath) {
$components = explode('/', $urlpath);
$id = end($components);
}
return static::URI . '/' . $id;
}
public function getName() {
return $this->filename ? $this->filename . ' - ' . static::NAME : static::NAME;
}
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI(),
null,
null,
true,
true,
DEFAULT_TARGET_CHARSET,
false, // Do NOT remove line breaks
DEFAULT_BR_TEXT,
DEFAULT_SPAN_TEXT)
or returnServerError('Could not request ' . $this->getURI());
$html = defaultLinkTo($html, static::URI);
$fileinfo = $html->find('[class="file-info"]', 0)
or returnServerError('Could not find file info!');
$this->filename = $fileinfo->plaintext;
$comments = $html->find('div[class="timeline-comment-wrapper"]');
if(is_null($comments)) { // no comments yet
return;
}
foreach($comments as $comment) {
$uri = $comment->find('a[href^=#gistcomment]', 0)
or returnServerError('Could not find comment anchor!');
$title = $comment->find('div[class="unminimized-comment"] h3[class="timeline-comment-header-text"]', 0)
or returnServerError('Could not find comment header text!');
$datetime = $comment->find('[datetime]', 0)
or returnServerError('Could not find comment datetime!');
$author = $comment->find('a.author', 0)
or returnServerError('Could not find author name!');
$message = $comment->find('[class="comment-body"]', 0)
or returnServerError('Could not find comment body!');
$item = array();
$item['uri'] = $this->getURI() . $uri->href;
$item['title'] = str_replace('commented', 'commented on', $title->plaintext);
$item['timestamp'] = strtotime($datetime->datetime);
$item['author'] = '<a href="' . $author->href . '">' . $author->plaintext . '</a>';
$item['content'] = $this->fixContent($message);
// $item['enclosures'] = array();
// $item['categories'] = array();
$this->items[] = $item;
}
}
/** Removes all unnecessary tags and adds formatting */
private function fixContent($content){
// Restore code (inside <pre />) highlighting
foreach($content->find('pre') as $pre) {
$pre->style = <<<EOD
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 3px;
word-wrap: normal;
box-sizing: border-box;
margin-bottom: 16px;
EOD;
$code = $pre->find('code', 0);
if($code) {
$code->style = <<<EOD
white-space: pre;
word-break: normal;
EOD;
}
}
// find <code /> not inside <pre /> (`inline-code`)
foreach($content->find('code') as $code) {
if($code->parent()->tag === 'pre') {
continue;
}
$code->style = <<<EOD
background-color: rgba(27,31,35,0.05);
padding: 0.2em 0.4em;
border-radius: 3px;
EOD;
}
// restore text spacing
foreach($content->find('p') as $p) {
$p->style = 'margin-bottom: 16px;';
}
// Remove unnecessary tags
$content = strip_tags(
$content->innertext,
'<p><a><img><ol><ul><li><table><tr><th><td><string><pre><code><br><hr><h>'
);
return $content;
}
}

View File

@@ -106,7 +106,7 @@ class GithubIssueBridge extends BridgeAbstract {
$content = $comment->parent()->innertext;
} else {
$title .= ' / ' . trim($comment->firstChild()->plaintext);
$content = "<pre>" . $comment->find('.comment-body', 0)->innertext . "</pre>";
$content = '<pre>' . $comment->find('.comment-body', 0)->innertext . '</pre>';
}
$item = array();

View File

@@ -3,7 +3,7 @@ class GoComicsBridge extends BridgeAbstract {
const MAINTAINER = 'sky';
const NAME = 'GoComics Unofficial RSS';
const URI = 'http://www.gocomics.com/';
const URI = 'https://www.gocomics.com/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'The Unofficial GoComics RSS';
const PARAMETERS = array( array(
@@ -18,25 +18,27 @@ class GoComicsBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request GoComics: ' . $this->getURI());
foreach($html->find('div.item-comic-container') as $element) {
//Get info from first page
$author = preg_replace('/By /', '', $html->find('.media-subheading', 0)->plaintext);
$img = $element->find('img', 0);
$link = $element->find('a.item-comic-link', 0);
$comic = $img->src;
$title = $link->title;
$url = $html->find('input.js-copy-link', 0)->value;
$date = substr($title, -10);
if (empty($title))
$title = 'GoComics ' . $this->getInput('comicname') . ' on ' . $date;
$date = strtotime($date);
$link = self::URI . $html->find('.gc-deck--cta-0', 0)->find('a', 0)->href;
for($i = 0; $i < 5; $i++) {
$item = array();
$item['id'] = $url;
$item['uri'] = $url;
$item['title'] = $title;
$item['author'] = preg_replace('/by /', '', $element->find('a.link-blended small', 0)->plaintext);
$item['timestamp'] = $date;
$item['content'] = '<img src="' . $comic . '" alt="' . $title . '" />';
$page = getSimpleHTMLDOM($link)
or returnServerError('Could not request GoComics: ' . $link);
$imagelink = $page->find('.img-fluid', 1)->src;
$date = explode('/', $link);
$item['id'] = $imagelink;
$item['uri'] = $link;
$item['author'] = $author;
$item['title'] = 'GoComics ' . $this->getInput('comicname');
$item['timestamp'] = DateTime::createFromFormat('Ymd', $date[5] . $date[6] . $date[7])->getTimestamp();
$item['content'] = '<img src="' . $imagelink . '" />';
$link = self::URI . $page->find('.js-previous-comic', 0)->href;
$this->items[] = $item;
}
}

View File

@@ -1,12 +1,12 @@
<?php
class GooglePlusPostBridge extends BridgeAbstract{
protected $_title;
protected $_url;
private $title;
private $url;
const MAINTAINER = 'Grummfy';
const MAINTAINER = 'Grummfy, logmanoriginal';
const NAME = 'Google Plus Post Bridge';
const URI = 'https://plus.google.com/';
const URI = 'https://plus.google.com';
const CACHE_TIMEOUT = 600; //10min
const DESCRIPTION = 'Returns user public post (without API).';
@@ -14,10 +14,16 @@ class GooglePlusPostBridge extends BridgeAbstract{
'username' => array(
'name' => 'username or Id',
'required' => true
),
'include_media' => array(
'name' => 'Include media',
'type' => 'checkbox',
'title' => 'Enable to include media in the feed content'
)
));
public function collectData(){
$username = $this->getInput('username');
// Usernames start with a + if it's not an ID
@@ -25,22 +31,20 @@ class GooglePlusPostBridge extends BridgeAbstract{
$username = '+' . $username;
}
// get content parsed
$html = getSimpleHTMLDOMCached(self::URI . urlencode($username) . '/posts')
$html = getSimpleHTMLDOM(static::URI . '/' . urlencode($username) . '/posts')
or returnServerError('No results for this query.');
// get title, url, ... there is a lot of intresting stuff in meta
$this->_title = $html->find('meta[property=og:title]', 0)->getAttribute('content');
$this->_url = $html->find('meta[property=og:url]', 0)->getAttribute('content');
$html = defaultLinkTo($html, static::URI);
$this->title = $html->find('meta[property=og:title]', 0)->getAttribute('content');
$this->url = $html->find('meta[property=og:url]', 0)->getAttribute('content');
// I don't even know where to start with this discusting html...
foreach($html->find('div[jsname=WsjYwc]') as $post) {
$item = array();
$item['author'] = $item['fullname'] = $post->find('div div div div a', 0)->innertext;
$item['id'] = $post->find('div div div', 0)->getAttribute('id');
$item['avatar'] = $post->find('div img', 0)->src;
$item['uri'] = self::URI . $post->find('div div div a', 1)->href;
$item['author'] = $post->find('div div div div a', 0)->innertext;
$item['uri'] = $post->find('div div div a', 1)->href;
$timestamp = $post->find('a.qXj2He span', 0);
@@ -51,61 +55,149 @@ class GooglePlusPostBridge extends BridgeAbstract{
$timestamp->getAttribute('aria-label')));
}
// hashtag to treat : https://plus.google.com/explore/tag
// $hashtags = array();
// foreach($post->find('a.d-s') as $hashtag){
// $hashtags[trim($hashtag->plaintext)] = self::URI . $hashtag->href;
// }
$message = $post->find('div[jsname=EjRJtf]', 0);
$item['content'] = '';
// Empty messages are not supported right now
if(!$message) {
continue;
}
// avatar display
$item['content'] .= '<div style="float:left; margin: 0 0.5em 0.5em 0;"><a href="'
. self::URI
. urlencode($this->getInput('username'));
$item['content'] .= '"><img align="top" alt="'
$item['content'] = '<div style="float: left; padding: 0 10px 10px 0;"><a href="'
. $this->url
. '"><img align="top" alt="'
. $item['author']
. '" src="'
. $item['avatar']
. '" /></a></div>';
. $post->find('div img', 0)->src
. '" /></a></div><div>'
. trim(strip_tags($message, '<a><p><div><img>'))
. '</div>';
$content = $post->find('div[jsname=EjRJtf]', 0);
// extract plaintext
$item['content_simple'] = $content->plaintext;
$item['title'] = substr($item['content_simple'], 0, 72) . '...';
// XXX ugly but I don't have any idea how to do a better stuff,
// str_replace on link doesn't work as expected and ask too many checks
foreach($content->find('a') as $link) {
$hasHttp = strpos($link->href, 'http');
$hasDoubleSlash = strpos($link->href, '//');
if((!$hasHttp && !$hasDoubleSlash)
|| (false !== $hasHttp && strpos($link->href, 'http') != 0)
|| (false === $hasHttp && false !== $hasDoubleSlash && $hasDoubleSlash != 0)) {
// skipp bad link, for some hashtag or other stuff
if(strpos($link->href, '/') == 0) {
$link->href = substr($link->href, 1);
}
$link->href = self::URI . $link->href;
}
// Make title at least 50 characters long, but don't add '...' if it is shorter!
if(strlen($message->plaintext) > 50) {
$end = strpos($message->plaintext, ' ', 50);
}
$content = $content->innertext;
$item['content'] .= '<div style="margin-top: -1.5em">' . $content . '</div>';
$item['content'] = trim(strip_tags($item['content'], '<a><p><div><img>'));
if(strlen(substr($message->plaintext, 0, $end)) === strlen($message->plaintext)) {
$item['title'] = $message->plaintext;
} else {
$item['title'] = substr($message->plaintext, 0, $end) . '...';
}
$media = $post->find('[jsname="MTOxpb"]', 0);
if($media) {
$item['enclosures'] = array();
foreach($media->find('img') as $img) {
$item['enclosures'][] = $this->fixImage($img)->src;
}
if($this->getInput('include_media') === true && count($item['enclosures'] > 0)) {
$item['content'] .= '<div style="clear: both;"><a href="'
. $item['enclosures'][0]
. '"><img src="'
. $item['enclosures'][0]
. '" /></a></div>';
}
}
// Add custom parameters (only useful for JSON or Plaintext)
$item['fullname'] = $item['author'];
$item['avatar'] = $post->find('div img', 0)->src;
$item['id'] = $post->find('div div div', 0)->getAttribute('id');
$item['content_simple'] = $message->plaintext;
$this->items[] = $item;
}
}
public function getName(){
return $this->_title ?: 'Google Plus Post Bridge';
return $this->title ?: 'Google Plus Post Bridge';
}
public function getURI(){
return $this->_url ?: parent::getURI();
return $this->url ?: parent::getURI();
}
private function fixImage($img) {
// There are certain images like .gif which link to a static picture and
// get replaced dynamically via JS in the browser. If we want the "real"
// image we need to account for that.
$urlparts = parse_url($img->src);
if(array_key_exists('host', $urlparts)) {
// For some reason some URIs don't contain the scheme, assume https
if(!array_key_exists('scheme', $urlparts)) {
$urlparts['scheme'] = 'https';
}
$pathelements = explode('/', $urlparts['path']);
switch($urlparts['host']) {
case 'lh3.googleusercontent.com':
if(pathinfo(end($pathelements), PATHINFO_EXTENSION)) {
// The second to last element of the path specifies the
// image format. The URL is still valid if we remove it.
unset($pathelements[count($pathelements) - 2]);
} elseif(strrpos(end($pathelements), '=') !== false) {
// Some images go throug a proxy. For those images they
// add size information after an equal sign.
// Example: '=w530-h298-n'. Again this can safely be
// removed to get the original image.
$pathelements[count($pathelements) - 1] = substr(
end($pathelements),
0,
strrpos(end($pathelements), '=')
);
}
break;
}
$urlparts['path'] = implode('/', $pathelements);
}
$img->src = $this->build_url($urlparts);
return $img;
}
/**
* From: https://gist.github.com/Ellrion/f51ba0d40ae1d62eeae44fd1adf7b704
* slightly adjusted to work with PHP < 7.0
* @param array $parts
* @return string
*/
private function build_url(array $parts)
{
$scheme = isset($parts['scheme']) ? ($parts['scheme'] . '://') : '';
$host = isset($parts['host']) ? $parts['host'] : '';
$port = isset($parts['port']) ? (':' . $parts['port']) : '';
$user = isset($parts['user']) ? $parts['user'] : '';
$pass = isset($parts['pass']) ? (':' . $parts['pass']) : '';
$pass = ($user || $pass) ? ($pass . '@') : '';
$path = isset($parts['path']) ? $parts['path'] : '';
$query = isset($parts['query']) ? ('?' . $parts['query']) : '';
$fragment = isset($parts['fragment']) ? ('#' . $parts['fragment']) : '';
return implode('', [$scheme, $user, $pass, $host, $port, $path, $query, $fragment]);
}
}

View File

@@ -17,7 +17,7 @@ class GoogleSearchBridge extends BridgeAbstract {
const PARAMETERS = array(array(
'q' => array(
'name' => "keyword",
'name' => 'keyword',
'required' => true
)
));

View File

@@ -0,0 +1,61 @@
<?php
class GrandComicsDatabaseBridge extends BridgeAbstract {
const MAINTAINER = 'corenting';
const NAME = 'Grand Comics Database Bridge';
const URI = 'https://www.comics.org/';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Returns the latest comics added to a series timeline';
const PARAMETERS = array( array(
'series' => array(
'name' => 'Series id (from the timeline URL)',
'required' => true,
'exampleValue' => '63051',
),
));
public function collectData(){
$url = self::URI . 'series/' . $this->getInput('series') . '/details/timeline/';
$html = getSimpleHTMLDOM($url)
or returnServerError('Error while downloading the website content');
$table = $html->find('table', 0);
$list = array_reverse($table->find('[class^=row_even]'));
$seriesName = $html->find('span[id=series_name]', 0)->innertext;
// Get row headers
$rowHeaders = $table->find('th');
foreach($list as $article) {
// Skip empty rows
$emptyRow = $article->find('td.empty_month');
if (count($emptyRow) != 0) {
continue;
}
$rows = $article->find('td');
$key_date = $rows[0]->innertext;
// Get URL too
$uri = 'https://www.comics.org' . $article->find('a')[0]->href;
// Build content
$content = '';
for($i = 0; $i < count($rowHeaders); $i++) {
$headerItem = $rowHeaders[$i]->innertext;
$rowItem = $rows[$i]->innertext;
$content = $content . $headerItem . ': ' . $rowItem . '<br/>';
}
// Build final item
$item = array();
$item['title'] = $seriesName . ' - ' . $key_date;
$item['timestamp'] = strtotime($key_date);
$item['content'] = str_get_html($content);
$item['uri'] = $uri;
$this->items[] = $item;
}
}
}

1397
bridges/HotUKDealsBridge.php Normal file

File diff suppressed because it is too large Load Diff

310
bridges/IPBBridge.php Normal file
View File

@@ -0,0 +1,310 @@
<?php
class IPBBridge extends FeedExpander {
const NAME = 'IPB Bridge';
const URI = 'https://www.invisionpower.com';
const DESCRIPTION = 'Returns feeds for forums powered by IPB';
const MAINTAINER = 'logmanoriginal';
const PARAMETERS = array(
array(
'uri' => array(
'name' => 'URI',
'type' => 'text',
'required' => true,
'title' => 'Insert forum, subforum or topic URI',
'exampleValue' => 'https://invisioncommunity.com/forums/forum/499-feedback-and-ideas/'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'required' => false,
'title' => 'Specifies the number of items to return on each request (-1: all)',
'defaultValue' => 10
)
)
);
const CACHE_TIMEOUT = 3600;
// Constants for internal use
const FORUM_TYPE_LIST_FILTER = '.cForumTopicTable';
const FORUM_TYPE_TABLE_FILTER = '#forum_table';
const TOPIC_TYPE_ARTICLE = 'article';
const TOPIC_TYPE_DIV = 'div.post_block';
public function getURI(){
return $this->getInput('uri') ?: parent::getURI();
}
public function collectData(){
// The URI cannot be the mainpage (or anything related)
switch(parse_url($this->getInput('uri'), PHP_URL_PATH)) {
case null:
case '/index.php':
returnClientError('Provided URI is invalid!');
break;
default:
break;
}
// Sanitize the URI (because else it won't work)
$uri = rtrim($this->getInput('uri'), '/'); // No trailing slashes!
// Forums might provide feeds, though that's optional *facepalm*
// Let's check if there is a valid feed available
$headers = get_headers($uri . '.xml');
if($headers[0] === 'HTTP/1.1 200 OK') { // Heureka! It's a valid feed!
return $this->collectExpandableDatas($uri);
}
// No valid feed, so do it the hard way
$html = getSimpleHTMLDOM($uri)
or returnServerError('Could not request ' . $this->getInput('uri') . '!');
$limit = $this->getInput('limit');
// Determine if this is a topic or a forum
switch(true) {
case $this->isTopic($html):
$this->collectTopic($html, $limit);
break;
case $this->isForum($html);
$this->collectForum($html);
break;
default:
returnClientError('Unknown type!');
break;
}
}
private function isForum($html){
return !is_null($html->find('div[data-controller*=forums.front.forum.forumPage]', 0))
|| !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0));
}
private function isTopic($html){
return !is_null($html->find('div[data-controller*=core.front.core.commentFeed]', 0))
|| !is_null($html->find(static::TOPIC_TYPE_DIV, 0));
}
private function collectForum($html){
// There are multiple forum designs in use (depends on version?)
// 1 - Uses an ordered list (based on https://invisioncommunity.com/forums)
// 2 - Uses a table (based on https://onehallyu.com)
switch(true) {
case !is_null($html->find(static::FORUM_TYPE_LIST_FILTER, 0)):
$this->collectForumList($html);
break;
case !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)):
$this->collectForumTable($html);
break;
default:
returnClientError('Unknown forum format!');
break;
}
}
private function collectForumList($html){
foreach($html->find(static::FORUM_TYPE_LIST_FILTER, 0)->children() as $row) {
// Columns: Title, Statistics, Last modified
$item = array();
$item['uri'] = $row->find('a', 0)->href;
$item['title'] = $row->find('a', 0)->title;
$item['author'] = $row->find('a', 1)->innertext;
$item['timestamp'] = strtotime($row->find('time', 0)->getAttribute('datetime'));
$this->items[] = $item;
}
}
private function collectForumTable($html){
foreach($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)->children() as $row) {
// Columns: Icon, Content, Preview, Statistics, Last modified
$item = array();
// Skip header row
if(!is_null($row->find('th', 0))) continue;
$item['uri'] = $row->find('a', 0)->href;
$item['title'] = $row->find('.title', 0)->plaintext;
$item['timestamp'] = strtotime($row->find('[itemprop=dateCreated]', 0)->plaintext);
$this->items[] = $item;
}
}
private function collectTopic($html, $limit){
// There are multiple topic designs in use (depends on version?)
// 1 - Uses articles (based on https://invisioncommunity.com/forums)
// 2 - Uses divs (based on https://onehallyu.com)
switch(true) {
case !is_null($html->find(static::TOPIC_TYPE_ARTICLE, 0)):
$this->collectTopicHistory($html, $limit, 'collectTopicArticle');
break;
case !is_null($html->find(static::TOPIC_TYPE_DIV, 0)):
$this->collectTopicHistory($html, $limit, 'collectTopicDiv');
break;
default:
returnClientError('Unknown topic format!');
break;
}
}
private function collectTopicHistory($html, $limit, $callback){
// Make sure the callback is valid!
if(!method_exists($this, $callback))
returnServerError('Unknown function (\'' . $callback . '\')!');
$next = null; // Holds the URI of the next page
while(true) {
$next = $this->$callback($html, is_null($next));
if(is_null($next) || ($limit > 0 && count($this->items) >= $limit)) {
break;
}
$html = getSimpleHTMLDOMCached($next);
}
// We might have more items than specified, remove excess
$this->items = array_slice($this->items, 0, $limit);
}
private function collectTopicArticle($html, $firstrun = true){
$title = $html->find('h1.ipsType_pageTitle', 0)->plaintext;
// Are we on last page?
if($firstrun && !is_null($html->find('.ipsPagination', 0))) {
$last = $html->find('.ipsPagination_last a', 0)->{'data-page'};
$active = $html->find('.ipsPagination_active a', 0)->{'data-page'};
if($active !== $last) {
// Load last page into memory (cached)
$html = getSimpleHTMLDOMCached($html->find('.ipsPagination_last a', 0)->href);
}
}
foreach(array_reverse($html->find(static::TOPIC_TYPE_ARTICLE)) as $article) {
$item = array();
$item['uri'] = $article->find('time', 0)->parent()->href;
$item['author'] = $article->find('aside a', 0)->plaintext;
$item['title'] = $item['author'] . ' - ' . $title;
$item['timestamp'] = strtotime($article->find('time', 0)->getAttribute('datetime'));
$content = $article->find('[data-role=commentContent]', 0);
$content = $this->scaleImages($content);
$item['content'] = $this->fixContent($content);
$item['enclosures'] = $this->findImages($article->find('[data-role=commentContent]', 0)) ?: null;
$this->items[] = $item;
}
// Return whatever page comes next (previous, as we add in inverse order)
// Do we have a previous page? (inactive means no)
if(!is_null($html->find('li[class=ipsPagination_prev ipsPagination_inactive]', 0))) {
return null; // No, or no more
} elseif(!is_null($html->find('li[class=ipsPagination_prev]', 0))) {
return $html->find('.ipsPagination_prev a', 0)->href;
}
return null;
}
private function collectTopicDiv($html, $firstrun = true){
$title = $html->find('h1.ipsType_pagetitle', 0)->plaintext;
// Are we on last page?
if($firstrun && !is_null($html->find('.pagination', 0))) {
$active = $html->find('li[class=page active]', 0)->plaintext;
// There are two ways the 'last' page is displayed:
// - With a distict 'last' button (only if there are enough pages)
// - With a button for each page (use last button)
if(!is_null($html->find('li.last', 0))) {
$last = $html->find('li.last a', 0);
} else {
$last = $html->find('li[class=page] a', -1);
}
if($active !== $last->plaintext) {
// Load last page into memory (cached)
$html = getSimpleHTMLDOMCached($last->href);
}
}
foreach(array_reverse($html->find(static::TOPIC_TYPE_DIV)) as $article) {
$item = array();
$item['uri'] = $article->find('a[rel=bookmark]', 0)->href;
$item['author'] = $article->find('.author', 0)->plaintext;
$item['title'] = $item['author'] . ' - ' . $title;
$item['timestamp'] = strtotime($article->find('.published', 0)->getAttribute('title'));
$content = $article->find('[itemprop=commentText]', 0);
$content = $this->scaleImages($content);
$item['content'] = $this->fixContent($content);
$item['enclosures'] = $this->findImages($article->find('.post_body', 0)) ?: null;
$this->items[] = $item;
}
// Return whatever page comes next (previous, as we add in inverse order)
// Do we have a previous page?
if(!is_null($html->find('li.prev', 0))) {
return $html->find('li.prev a', 0)->href;
}
return null;
}
/** Returns all images from the provide HTML DOM */
private function findImages($html){
$images = array();
foreach($html->find('img') as $img) {
$images[] = $img->src;
}
return $images;
}
/** Sets the maximum width and height for all images */
private function scaleImages($html, $width = 400, $height = 400){
foreach($html->find('img') as $img) {
$img->style = "max-width: {$width}px; max-height: {$height}px;";
}
return $html;
}
/** Removes all unnecessary tags and adds formatting */
private function fixContent($html){
// Restore quote highlighting
foreach($html->find('blockquote') as $quote) {
$quote->style = <<<EOD
padding: 0px 15px;
border-width: 1px 1px 1px 2px;
border-style: solid;
border-color: #ededed #e8e8e8 #dbdbdb #666666;
background: #fbfbfb;
EOD;
}
// Remove unnecessary tags
$content = strip_tags(
$html->innertext,
'<p><a><img><ol><ul><li><table><tr><th><td><strong><blockquote><br><hr><h>'
);
return $content;
}
}

View File

@@ -6,75 +6,129 @@ class InstagramBridge extends BridgeAbstract {
const URI = 'https://instagram.com/';
const DESCRIPTION = 'Returns the newest images';
const PARAMETERS = array( array(
'u' => array(
'name' => 'username',
'required' => true
const PARAMETERS = array(
array(
'u' => array(
'name' => 'username',
'required' => true
)
),
'media_type' => array(
'name' => 'Media type',
'type' => 'list',
'required' => false,
'values' => array(
'Both' => 'all',
'Video' => 'video',
'Picture' => 'picture'
),
'defaultValue' => 'all'
array(
'h' => array(
'name' => 'hashtag',
'required' => true
)
),
'global' => array(
'media_type' => array(
'name' => 'Media type',
'type' => 'list',
'required' => false,
'values' => array(
'All' => 'all',
'Story' => 'story',
'Video' => 'video',
'Picture' => 'picture',
),
'defaultValue' => 'all'
)
)
));
);
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request Instagram.');
$innertext = null;
foreach($html->find('script') as $script) {
if('' === $script->innertext) {
continue;
}
$pos = strpos(trim($script->innertext), 'window._sharedData');
if(0 !== $pos) {
continue;
}
$innertext = $script->innertext;
break;
if(!is_null($this->getInput('h')) && $this->getInput('media_type') == 'story') {
returnClientError('Stories are not supported for hashtags!');
}
$json = trim(substr($innertext, $pos + 18), ' =;');
$data = json_decode($json);
$data = $this->getInstagramJSON($this->getURI());
$userMedia = $data->entry_data->ProfilePage[0]->user->media->nodes;
if(!is_null($this->getInput('u'))) {
$userMedia = $data->entry_data->ProfilePage[0]->graphql->user->edge_owner_to_timeline_media->edges;
} else {
$userMedia = $data->entry_data->TagPage[0]->graphql->hashtag->edge_hashtag_to_media->edges;
}
foreach($userMedia as $media) {
// Check media type
switch($this->getInput('media_type')) {
case 'all': break;
case 'video':
if($media->is_video === false) continue 2;
break;
case 'picture':
if($media->is_video === true) continue 2;
break;
default: break;
$media = $media->node;
if(!is_null($this->getInput('u'))) {
switch($this->getInput('media_type')) {
case 'all': break;
case 'video':
if($media->__typename != 'GraphVideo') continue 2;
break;
case 'picture':
if($media->__typename != 'GraphImage') continue 2;
break;
case 'story':
if($media->__typename != 'GraphSidecar') continue 2;
break;
default: break;
}
} else {
if($this->getInput('media_type') == 'video' && !$media->is_video) continue;
}
$item = array();
$item['uri'] = self::URI . 'p/' . $media->code . '/';
$item['content'] = '<img src="' . htmlentities($media->display_src) . '" />';
if (isset($media->caption)) {
$item['title'] = $media->caption;
$item['uri'] = self::URI . 'p/' . $media->shortcode . '/';
if (isset($media->edge_media_to_caption->edges[0]->node->text)) {
$item['title'] = $media->edge_media_to_caption->edges[0]->node->text;
} else {
$item['title'] = basename($media->display_src);
$item['title'] = basename($media->display_url);
}
$item['timestamp'] = $media->date;
if(!is_null($this->getInput('u')) && $media->__typename == 'GraphSidecar') {
$data = $this->getInstagramStory($item['uri']);
$item['content'] = $data[0];
$item['enclosures'] = $data[1];
} else {
$item['content'] = '<img src="' . htmlentities($media->display_url) . '" alt="'. $item['title'] . '" />';
$item['enclosures'] = array($media->display_url);
}
$item['timestamp'] = $media->taken_at_timestamp;
$this->items[] = $item;
}
}
protected function getInstagramStory($uri) {
$data = $this->getInstagramJSON($uri);
$mediaInfo = $data->entry_data->PostPage[0]->graphql->shortcode_media;
//Process the first element, that isn't in the node graph
$caption = $mediaInfo->edge_media_to_caption->edges[0]->node->text;
$enclosures = [$mediaInfo->display_url];
$content = '<img src="' . htmlentities($mediaInfo->display_url) . '" alt="'. $caption . '" />';
foreach($mediaInfo->edge_sidecar_to_children->edges as $media) {
$content .= '<img src="' . htmlentities($media->node->display_url) . '" alt="'. $caption . '" />';
$enclosures[] = $media->node->display_url;
}
return [$content, $enclosures];
}
protected function getInstagramJSON($uri) {
$html = getContents($uri)
or returnServerError('Could not request Instagram.');
$scriptRegex = '/window\._sharedData = (.*);<\/script>/';
preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0);
return json_decode($matches[1][0]);
}
public function getName(){
if(!is_null($this->getInput('u'))) {
return $this->getInput('u') . ' - Instagram Bridge';
@@ -86,6 +140,8 @@ class InstagramBridge extends BridgeAbstract {
public function getURI(){
if(!is_null($this->getInput('u'))) {
return self::URI . urlencode($this->getInput('u'));
} elseif(!is_null($this->getInput('h'))) {
return self::URI . 'explore/tags/' . urlencode($this->getInput('h'));
}
return parent::getURI();

View File

@@ -0,0 +1,370 @@
<?php
/**
* This class implements a bridge for http://www.instructables.com, supporting
* general feeds and feeds by category. Instructables doesn't support HTTPS as
* of now (23.06.2018), so all connections are insecure!
*
* Remarks:
* - For some reason it is very important to have the category URI end with a
* slash, otherwise the site defaults to the main category (i.e. Technology)!
* If you need to update the categories list, enable the 'listCategories'
* function (see comments below) and run the bridge with format=Html (see page
* source)
*/
class InstructablesBridge extends BridgeAbstract {
const NAME = 'Instructables Bridge';
const URI = 'http://www.instructables.com';
const DESCRIPTION = 'Returns general feeds and feeds by category';
const MAINTAINER = 'logmanoriginal';
const PARAMETERS = array(
'Category' => array(
'category' => array(
'name' => 'Category',
'type' => 'list',
'required' => true,
'values' => array(
'Play' => array(
'All' => '/play/',
'KNEX' => '/play/knex/',
'Offbeat' => '/play/offbeat/',
'Lego' => '/play/lego/',
'Airsoft' => '/play/airsoft/',
'Card Games' => '/play/card-games/',
'Guitars' => '/play/guitars/',
'Instruments' => '/play/instruments/',
'Magic Tricks' => '/play/magic-tricks/',
'Minecraft' => '/play/minecraft/',
'Music' => '/play/music/',
'Nerf' => '/play/nerf/',
'Nintendo' => '/play/nintendo/',
'Office Supplies' => '/play/office-supplies/',
'Paintball' => '/play/paintball/',
'Paper Airplanes' => '/play/paper-airplanes/',
'Party Tricks' => '/play/party-tricks/',
'PlayStation' => '/play/playstation/',
'Pranks and Humor' => '/play/pranks-and-humor/',
'Puzzles' => '/play/puzzles/',
'Siege Engines' => '/play/siege-engines/',
'Sports' => '/play/sports/',
'Table Top' => '/play/table-top/',
'Toys' => '/play/toys/',
'Video Games' => '/play/video-games/',
'Wii' => '/play/wii/',
'Xbox' => '/play/xbox/',
'Yo-Yo' => '/play/yo-yo/',
),
'Craft' => array(
'All' => '/craft/',
'Art' => '/craft/art/',
'Sewing' => '/craft/sewing/',
'Paper' => '/craft/paper/',
'Jewelry' => '/craft/jewelry/',
'Fashion' => '/craft/fashion/',
'Books & Journals' => '/craft/books-and-journals/',
'Cards' => '/craft/cards/',
'Clay' => '/craft/clay/',
'Duct Tape' => '/craft/duct-tape/',
'Embroidery' => '/craft/embroidery/',
'Felt' => '/craft/felt/',
'Fiber Arts' => '/craft/fiber-arts/',
'Gifts & Wrapping' => '/craft/gifts-and-wrapping/',
'Knitting & Crocheting' => '/craft/knitting-and-crocheting/',
'Leather' => '/craft/leather/',
'Mason Jars' => '/craft/mason-jars/',
'No-Sew' => '/craft/no-sew/',
'Parties & Weddings' => '/craft/parties-and-weddings/',
'Print Making' => '/craft/print-making/',
'Soap' => '/craft/soap/',
'Wallets' => '/craft/wallets/',
),
'Technology' => array(
'All' => '/technology/',
'Electronics' => '/technology/electronics/',
'Arduino' => '/technology/arduino/',
'Photography' => '/technology/photography/',
'Leds' => '/technology/leds/',
'Science' => '/technology/science/',
'Reuse' => '/technology/reuse/',
'Apple' => '/technology/apple/',
'Computers' => '/technology/computers/',
'3D Printing' => '/technology/3D-Printing/',
'Robots' => '/technology/robots/',
'Art' => '/technology/art/',
'Assistive Tech' => '/technology/assistive-technology/',
'Audio' => '/technology/audio/',
'Clocks' => '/technology/clocks/',
'CNC' => '/technology/cnc/',
'Digital Graphics' => '/technology/digital-graphics/',
'Gadgets' => '/technology/gadgets/',
'Kits' => '/technology/kits/',
'Laptops' => '/technology/laptops/',
'Lasers' => '/technology/lasers/',
'Linux' => '/technology/linux/',
'Microcontrollers' => '/technology/microcontrollers/',
'Microsoft' => '/technology/microsoft/',
'Mobile' => '/technology/mobile/',
'Raspberry Pi' => '/technology/raspberry-pi/',
'Remote Control' => '/technology/remote-control/',
'Sensors' => '/technology/sensors/',
'Software' => '/technology/software/',
'Soldering' => '/technology/soldering/',
'Speakers' => '/technology/speakers/',
'Steampunk' => '/technology/steampunk/',
'Tools' => '/technology/tools/',
'USB' => '/technology/usb/',
'Wearables' => '/technology/wearables/',
'Websites' => '/technology/websites/',
'Wireless' => '/technology/wireless/',
),
'Workshop' => array(
'All' => '/workshop/',
'Woodworking' => '/workshop/woodworking/',
'Tools' => '/workshop/tools/',
'Gardening' => '/workshop/gardening/',
'Cars' => '/workshop/cars/',
'Metalworking' => '/workshop/metalworking/',
'Cardboard' => '/workshop/cardboard/',
'Electric Vehicles' => '/workshop/electric-vehicles/',
'Energy' => '/workshop/energy/',
'Furniture' => '/workshop/furniture/',
'Home Improvement' => '/workshop/home-improvement/',
'Home Theater' => '/workshop/home-theater/',
'Hydroponics' => '/workshop/hydroponics/',
'Laser Cutting' => '/workshop/laser-cutting/',
'Lighting' => '/workshop/lighting/',
'Molds & Casting' => '/workshop/molds-and-casting/',
'Motorcycles' => '/workshop/motorcycles/',
'Organizing' => '/workshop/organizing/',
'Pallets' => '/workshop/pallets/',
'Repair' => '/workshop/repair/',
'Shelves' => '/workshop/shelves/',
'Solar' => '/workshop/solar/',
'Workbenches' => '/workshop/workbenches/',
),
'Home' => array(
'All' => '/home/',
'Halloween' => '/home/halloween/',
'Decorating' => '/home/decorating/',
'Organizing' => '/home/organizing/',
'Pets' => '/home/pets/',
'Life Hacks' => '/home/life-hacks/',
'Beauty' => '/home/beauty/',
'Christmas' => '/home/christmas/',
'Cleaning' => '/home/cleaning/',
'Education' => '/home/education/',
'Finances' => '/home/finances/',
'Gardening' => '/home/gardening/',
'Green' => '/home/green/',
'Health' => '/home/health/',
'Hiding Places' => '/home/hiding-places/',
'Holidays' => '/home/holidays/',
'Homesteading' => '/home/homesteading/',
'Kids' => '/home/kids/',
'Kitchen' => '/home/kitchen/',
'Life Skills' => '/home/life-skills/',
'Parenting' => '/home/parenting/',
'Pest Control' => '/home/pest-control/',
'Relationships' => '/home/relationships/',
'Reuse' => '/home/reuse/',
'Travel' => '/home/travel/',
),
'Outside' => array(
'All' => '/outside/',
'Bikes' => '/outside/bikes/',
'Survival' => '/outside/survival/',
'Backyard' => '/outside/backyard/',
'Beach' => '/outside/beach/',
'Birding' => '/outside/birding/',
'Boats' => '/outside/boats/',
'Camping' => '/outside/camping/',
'Climbing' => '/outside/climbing/',
'Fire' => '/outside/fire/',
'Fishing' => '/outside/fishing/',
'Hunting' => '/outside/hunting/',
'Kites' => '/outside/kites/',
'Knives' => '/outside/knives/',
'Knots' => '/outside/knots/',
'Paracord' => '/outside/paracord/',
'Rockets' => '/outside/rockets/',
'Skateboarding' => '/outside/skateboarding/',
'Snow' => '/outside/snow/',
'Water' => '/outside/water/',
),
'Food' => array(
'All' => '/food/',
'Dessert' => '/food/dessert/',
'Snacks & Appetizers' => '/food/snacks-and-appetizers/',
'Bacon' => '/food/bacon/',
'BBQ & Grilling' => '/food/bbq-and-grilling/',
'Beverages' => '/food/beverages/',
'Bread' => '/food/bread/',
'Breakfast' => '/food/breakfast/',
'Cake' => '/food/cake/',
'Candy' => '/food/candy/',
'Canning & Preserves' => '/food/canning-and-preserves/',
'Cocktails & Mocktails' => '/food/cocktails-and-mocktails/',
'Coffee' => '/food/coffee/',
'Cookies' => '/food/cookies/',
'Cupcakes' => '/food/cupcakes/',
'Homebrew' => '/food/homebrew/',
'Main Course' => '/food/main-course/',
'Pasta' => '/food/pasta/',
'Pie' => '/food/pie/',
'Pizza' => '/food/pizza/',
'Salad' => '/food/salad/',
'Sandwiches' => '/food/sandwiches/',
'Soups & Stews' => '/food/soups-and-stews/',
'Vegetarian & Vegan' => '/food/vegetarian-and-vegan/',
),
'Costumes' => array(
'All' => '/costumes/',
'Props' => '/costumes/props-and-accessories/',
'Animals' => '/costumes/animals/',
'Comics' => '/costumes/comics/',
'Fantasy' => '/costumes/fantasy/',
'For Kids' => '/costumes/for-kids/',
'For Pets' => '/costumes/for-pets/',
'Funny' => '/costumes/funny/',
'Games' => '/costumes/games/',
'Historic & Futuristic' => '/costumes/historic-and-futuristic/',
'Makeup' => '/costumes/makeup/',
'Masks' => '/costumes/masks/',
'Scary' => '/costumes/scary/',
'TV & Movies' => '/costumes/tv-and-movies/',
'Weapons & Armor' => '/costumes/weapons-and-armor/',
)
),
'title' => 'Select your category (required)',
'defaultValue' => 'Technology'
),
'filter' => array(
'name' => 'Filter',
'type' => 'list',
'required' => true,
'values' => array(
'Featured' => ' ',
'Recent' => 'recent/',
'Popular' => 'popular/',
'Views' => 'views/',
'Contest Winners' => 'winners/'
),
'title' => 'Select a filter',
'defaultValue' => 'Featured'
)
)
);
private $uri;
public function collectData() {
// Enable the following line to get the category list (dev mode)
// $this->listCategories();
$this->uri = static::URI;
switch($this->queriedContext) {
case 'Category': $this->uri .= $this->getInput('category') . $this->getInput('filter');
}
$html = getSimpleHTMLDOM($this->uri)
or returnServerError('Error loading category ' . $this->uri);
foreach($html->find('ul.explore-covers-list li') as $cover) {
$item = array();
$item['uri'] = static::URI . $cover->find('a.cover-image', 0)->href;
$item['title'] = $cover->find('.title', 0)->innertext;
$item['author'] = $this->getCategoryAuthor($cover);
$item['content'] = '<a href='
. $item['uri']
. '><img src='
. $cover->find('a.cover-image img', 0)->src
. '></a>';
$image = str_replace('.RECTANGLE1', '.LARGE', $cover->find('a.cover-image img', 0)->src);
$item['enclosures'] = [$image];
$this->items[] = $item;
}
}
public function getName() {
if(!is_null($this->getInput('category'))
&& !is_null($this->getInput('filter'))) {
foreach(self::PARAMETERS[$this->queriedContext]['category']['values'] as $key => $value) {
$subcategory = array_search($this->getInput('category'), $value);
if($subcategory !== false)
break;
}
$filter = array_search(
$this->getInput('filter'),
self::PARAMETERS[$this->queriedContext]['filter']['values']
);
return $subcategory . ' (' . $filter . ') - ' . static::NAME;
}
return parent::getName();
}
public function getURI() {
if(!is_null($this->getInput('category'))
&& !is_null($this->getInput('filter'))) {
return $this->uri;
}
return parent::getURI();
}
/**
* Returns a list of categories for development purposes (used to build the
* parameters list)
*/
private function listCategories(){
// Use arbitrary category to receive full list
$html = getSimpleHTMLDOM(self::URI . '/technology/');
foreach($html->find('.channel a') as $channel) {
$name = html_entity_decode(trim($channel->innertext));
// Remove unwanted entities
$name = str_replace("'", '', $name);
$name = str_replace('&#39;', '', $name);
$uri = $channel->href;
$category = explode('/', $uri)[1];
if(!isset($categories)
|| !array_key_exists($category, $categories)
|| !in_array($uri, $categories[$category]))
$categories[$category][$name] = $uri;
}
// Build PHP array manually
foreach($categories as $key => $value) {
$name = ucfirst($key);
echo "'{$name}' => array(\n";
echo "\t'All' => '/{$key}/',\n";
foreach($value as $name => $uri) {
echo "\t'{$name}' => '{$uri}',\n";
}
echo "),\n";
}
die;
}
/**
* Returns the author as anchor for a given cover.
*/
private function getCategoryAuthor($cover) {
return '<a href='
. static::URI . $cover->find('span.author a', 0)->href
. '>'
. $cover->find('span.author a', 0)->innertext
. '</a>';
}
}

View File

@@ -1,465 +0,0 @@
<?php
class IsoHuntBridge extends BridgeAbstract {
const MAINTAINER = 'logmanoriginal';
const NAME = 'isoHunt Bridge';
const URI = 'https://isohunt.to/';
const CACHE_TIMEOUT = 300; //5min
const DESCRIPTION = 'Returns the latest results by category or search result';
const PARAMETERS = array(
/*
* Get feeds for one of the "latest" categories
* Notice: The categories "News" and "Top Searches" are received from the main page
* Elements are sorted by name ascending!
*/
'By "Latest" category' => array(
'latest_category' => array(
'name' => 'Latest category',
'type' => 'list',
'required' => true,
'title' => 'Select your category',
'defaultValue' => 'news',
'values' => array(
'Hot Torrents' => 'hot_torrents',
'News' => 'news',
'Releases' => 'releases',
'Torrents' => 'torrents'
)
)
),
/*
* Get feeds for one of the "torrent" categories
* Make sure to add new categories also to get_torrent_category_index($)!
* Elements are sorted by name ascending!
*/
'By "Torrent" category' => array(
'torrent_category' => array(
'name' => 'Torrent category',
'type' => 'list',
'required' => true,
'title' => 'Select your category',
'defaultValue' => 'anime',
'values' => array(
'Adult' => 'adult',
'Anime' => 'anime',
'Books' => 'books',
'Games' => 'games',
'Movies' => 'movies',
'Music' => 'music',
'Other' => 'other',
'Series & TV' => 'series_tv',
'Software' => 'software'
)
),
'torrent_popularity' => array(
'name' => 'Sort by popularity',
'type' => 'checkbox',
'title' => 'Activate to receive results by popularity'
)
),
/*
* Get feeds for a specific search request
*/
'Search torrent by name' => array(
'search_name' => array(
'name' => 'Name',
'required' => true,
'title' => 'Insert your search query',
'exampleValue' => 'Bridge'
),
'search_category' => array(
'name' => 'Category',
'type' => 'list',
'title' => 'Select your category',
'defaultValue' => 'all',
'values' => array(
'Adult' => 'adult',
'All' => 'all',
'Anime' => 'anime',
'Books' => 'books',
'Games' => 'games',
'Movies' => 'movies',
'Music' => 'music',
'Other' => 'other',
'Series & TV' => 'series_tv',
'Software' => 'software'
)
)
)
);
public function getURI(){
$uri = self::URI;
switch($this->queriedContext) {
case 'By "Latest" category':
switch($this->getInput('latest_category')) {
case 'hot_torrents':
$uri .= 'statistic/hot/torrents';
break;
case 'news':
break;
case 'releases':
$uri .= 'releases.php';
break;
case 'torrents':
$uri .= 'latest.php';
break;
}
break;
case 'By "Torrent" category':
$uri .= $this->buildCategoryUri(
$this->getInput('torrent_category'),
$this->getInput('torrent_popularity')
);
break;
case 'Search torrent by name':
$category = $this->getInput('search_category');
$uri .= $this->buildCategoryUri($category);
if($category !== 'movies')
$uri .= '&ihq=' . urlencode($this->getInput('search_name'));
break;
default: parent::getURI();
}
return $uri;
}
public function getName(){
switch($this->queriedContext) {
case 'By "Latest" category':
$categoryName = array_search(
$this->getInput('latest_category'),
self::PARAMETERS['By "Latest" category']['latest_category']['values']
);
$name = 'Latest ' . $categoryName . ' - ' . self::NAME;
break;
case 'By "Torrent" category':
$categoryName = array_search(
$this->getInput('torrent_category'),
self::PARAMETERS['By "Torrent" category']['torrent_category']['values']
);
$name = 'Category: ' . $categoryName . ' - ' . self::NAME;
break;
case 'Search torrent by name':
$categoryName = array_search(
$this->getInput('search_category'),
self::PARAMETERS['Search torrent by name']['search_category']['values']
);
$name = 'Search: "'
. $this->getInput('search_name')
. '" in category: '
. $categoryName . ' - '
. self::NAME;
break;
default: return parent::getName();
}
return $name;
}
public function collectData(){
$html = $this->loadHtml($this->getURI());
switch($this->queriedContext) {
case 'By "Latest" category':
switch($this->getInput('latest_category')) {
case 'hot_torrents':
$this->getLatestHotTorrents($html);
break;
case 'news':
$this->getLatestNews($html);
break;
case 'releases':
case 'torrents':
$this->getLatestTorrents($html);
break;
}
break;
case 'By "Torrent" category':
if($this->getInput('torrent_category') === 'movies') {
// This one is special (content wise)
$this->getMovieTorrents($html);
} else {
$this->getLatestTorrents($html);
}
break;
case 'Search torrent by name':
if($this->getInput('search_category') === 'movies') {
// This one is special (content wise)
$this->getMovieTorrents($html);
} else {
$this->getLatestTorrents($html);
}
break;
}
}
#region Helper functions for "Movie Torrents"
private function getMovieTorrents($html){
$container = $html->find('div#w0', 0);
if(!$container)
returnServerError('Unable to find torrent container!');
$torrents = $container->find('article');
if(!$torrents)
returnServerError('Unable to find torrents!');
foreach($torrents as $torrent) {
$anchor = $torrent->find('a', 0);
if(!$anchor)
returnServerError('Unable to find anchor!');
$date = $torrent->find('small', 0);
if(!$date)
returnServerError('Unable to find date!');
$item = array();
$item['uri'] = $this->fixRelativeUri($anchor->href);
$item['title'] = $anchor->title;
// $item['author'] =
$item['timestamp'] = strtotime($date->plaintext);
$item['content'] = $this->fixRelativeUri($torrent->innertext);
$this->items[] = $item;
}
}
#endregion
#region Helper functions for "Latest Hot Torrents"
private function getLatestHotTorrents($html){
$container = $html->find('div#serps', 0);
if(!$container)
returnServerError('Unable to find torrent container!');
$torrents = $container->find('tr');
if(!$torrents)
returnServerError('Unable to find torrents!');
// Remove first element (header row)
$torrents = array_slice($torrents, 1);
foreach($torrents as $torrent) {
$cell = $torrent->find('td', 0);
if(!$cell)
returnServerError('Unable to find cell!');
$element = $cell->find('a', 0);
if(!$element)
returnServerError('Unable to find element!');
$item = array();
$item['uri'] = $element->href;
$item['title'] = $element->plaintext;
// $item['author'] =
// $item['timestamp'] =
// $item['content'] =
$this->items[] = $item;
}
}
#endregion
#region Helper functions for "Latest News"
private function getLatestNews($html){
$container = $html->find('div#postcontainer', 0);
if(!$container)
returnServerError('Unable to find post container!');
$posts = $container->find('div.index-post');
if(!$posts)
returnServerError('Unable to find posts!');
foreach($posts as $post) {
$item = array();
$item['uri'] = $this->latestNewsExtractUri($post);
$item['title'] = $this->latestNewsExtractTitle($post);
$item['author'] = $this->latestNewsExtractAuthor($post);
$item['timestamp'] = $this->latestNewsExtractTimestamp($post);
$item['content'] = $this->latestNewsExtractContent($post);
$this->items[] = $item;
}
}
private function latestNewsExtractAuthor($post){
$author = $post->find('small', 0);
if(!$author)
returnServerError('Unable to find author!');
// The author is hidden within a string like: 'Posted by {author} on {date}'
preg_match('/Posted\sby\s(.*)\son/i', $author->innertext, $matches);
return $matches[1];
}
private function latestNewsExtractTimestamp($post){
$date = $post->find('small', 0);
if(!$date)
returnServerError('Unable to find date!');
// The date is hidden within a string like: 'Posted by {author} on {date}'
preg_match('/Posted\sby\s.*\son\s(.*)/i', $date->innertext, $matches);
$timestamp = strtotime($matches[1]);
// Make sure date is not in the future (dates are given like 'Nov. 20' without year)
if($timestamp > time()) {
$timestamp = strtotime('-1 year', $timestamp);
}
return $timestamp;
}
private function latestNewsExtractTitle($post){
$title = $post->find('a', 0);
if(!$title)
returnServerError('Unable to find title!');
return $title->plaintext;
}
private function latestNewsExtractUri($post){
$uri = $post->find('a', 0);
if(!$uri)
returnServerError('Unable to find uri!');
return $uri->href;
}
private function latestNewsExtractContent($post){
$content = $post->find('div', 0);
if(!$content)
returnServerError('Unable to find content!');
// Remove <h2>...</h2> (title)
foreach($content->find('h2') as $element) {
$element->outertext = '';
}
// Remove <small>...</small> (author)
foreach($content->find('small') as $element) {
$element->outertext = '';
}
return $content->innertext;
}
#endregion
#region Helper functions for "Latest Torrents", "Latest Releases" and "Torrent Category"
private function getLatestTorrents($html){
$container = $html->find('div#serps', 0);
if(!$container)
returnServerError('Unable to find torrent container!');
$torrents = $container->find('tr[data-key]');
if(!$torrents)
returnServerError('Unable to find torrents!');
foreach($torrents as $torrent) {
$item = array();
$item['uri'] = $this->latestTorrentsExtractUri($torrent);
$item['title'] = $this->latestTorrentsExtractTitle($torrent);
$item['author'] = $this->latestTorrentsExtractAuthor($torrent);
$item['timestamp'] = $this->latestTorrentsExtractTimestamp($torrent);
$item['content'] = ''; // There is no valuable content
$this->items[] = $item;
}
}
private function latestTorrentsExtractTitle($torrent){
$cell = $torrent->find('td.title-row', 0);
if(!$cell)
returnServerError('Unable to find title cell!');
$title = $cell->find('span', 0);
if(!$title)
returnServerError('Unable to find title!');
return $title->plaintext;
}
private function latestTorrentsExtractUri($torrent){
$cell = $torrent->find('td.title-row', 0);
if(!$cell)
returnServerError('Unable to find title cell!');
$uri = $cell->find('a', 0);
if(!$uri)
returnServerError('Unable to find uri!');
return $this->fixRelativeUri($uri->href);
}
private function latestTorrentsExtractAuthor($torrent){
$cell = $torrent->find('td.user-row', 0);
if(!$cell)
return; // No author
$user = $cell->find('a', 0);
if(!$user)
returnServerError('Unable to find user!');
return $user->plaintext;
}
private function latestTorrentsExtractTimestamp($torrent){
$cell = $torrent->find('td.date-row', 0);
if(!$cell)
returnServerError('Unable to find date cell!');
return strtotime('-' . $cell->plaintext, time());
}
#endregion
#region Generic helper functions
private function loadHtml($uri){
$html = getSimpleHTMLDOM($uri);
if(!$html)
returnServerError('Unable to load ' . $uri . '!');
return $html;
}
private function fixRelativeUri($uri){
return preg_replace('/\//i', self::URI, $uri, 1);
}
private function buildCategoryUri($category, $order_popularity = false){
switch($category) {
case 'anime': $index = 1; break;
case 'software' : $index = 2; break;
case 'games' : $index = 3; break;
case 'adult' : $index = 4; break;
case 'movies' : $index = 5; break;
case 'music' : $index = 6; break;
case 'other' : $index = 7; break;
case 'series_tv' : $index = 8; break;
case 'books': $index = 9; break;
case 'all':
default: $index = 0; break;
}
return 'torrents/?iht=' . $index . '&ihs=' . ($order_popularity ? 1 : 0) . '&age=0';
}
#endregion
}

353
bridges/JustETFBridge.php Normal file
View File

@@ -0,0 +1,353 @@
<?php
class JustETFBridge extends BridgeAbstract {
const NAME = 'justETF Bridge';
const URI = 'https://www.justetf.com';
const DESCRIPTION = 'Currently only supports the news feed';
const MAINTAINER = 'logmanoriginal';
const PARAMETERS = array(
'News' => array(
'full' => array(
'name' => 'Full Article',
'type' => 'checkbox',
'title' => 'Enable to load full articles'
)
),
'Profile' => array(
'isin' => array(
'name' => 'ISIN',
'type' => 'text',
'required' => true,
'pattern' => '[a-zA-Z]{2}[a-zA-Z0-9]{10}',
'title' => 'ISIN, consisting of 2-letter country code, 9-character identifier, check character'
),
'strategy' => array(
'name' => 'Include Strategy',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'description' => array(
'name' => 'Include Description',
'type' => 'checkbox',
'defaultValue' => 'checked'
)
),
'global' => array(
'lang' => array(
'name' => 'Language',
'required' => true,
'type' => 'list',
'values' => array(
'Englisch' => 'en',
'Deutsch' => 'de',
'Italiano' => 'it'
),
'defaultValue' => 'Englisch'
)
)
);
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Failed loading contents from ' . $this->getURI());
defaultLinkTo($html, static::URI);
switch($this->queriedContext) {
case 'News':
$this->collectNews($html);
break;
case 'Profile':
$this->collectProfile($html);
break;
}
}
public function getURI() {
$uri = static::URI;
if($this->getInput('lang')) {
$uri .= '/' . $this->getInput('lang');
}
switch($this->queriedContext) {
case 'News':
$uri .= '/news';
break;
case 'Profile':
$uri .= '/etf-profile.html?' . http_build_query(array(
'isin' => strtoupper($this->getInput('isin'))
));
break;
}
return $uri;
}
public function getName() {
$name = static::NAME;
$name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : '';
switch($this->queriedContext) {
case 'News': break;
case 'Profile':
if($this->getInput('isin')) {
$name .= ' ISIN ' . strtoupper($this->getInput('isin'));
}
}
if($this->getInput('lang')) {
$name .= ' (' . strtoupper($this->getInput('lang')) . ')';
}
return $name;
}
#region Common
/**
* Fixes dates depending on the choosen language:
*
* de : dd.mm.yy
* en : dd.mm.yy
* it : dd/mm/yy
*
* Basically strtotime doesn't convert dates correctly due to formats
* being hard to interpret. So we use the DateTime object, manually
* fixing dates and times (set to 00:00:00.000).
*
* We don't know the timezone, so just assume +00:00 (or whatever
* DateTime chooses)
*/
private function fixDate($date) {
switch($this->getInput('lang')) {
case 'en':
case 'de':
$df = date_create_from_format('d.m.y', $date);
break;
case 'it':
$df = date_create_from_format('d/m/y', $date);
break;
}
date_time_set($df, 0, 0);
// debugMessage(date_format($df, 'U'));
return date_format($df, 'U');
}
private function extractImages($article) {
// Notice: We can have zero or more images (though it should mostly be 1)
$elements = $article->find('img');
$images = array();
foreach($elements as $img) {
// Skip the logo (mostly provided part of a hidden div)
if(substr($img->src, strrpos($img->src, '/') + 1) === 'logo.png')
continue;
$images[] = $img->src;
}
return $images;
}
#endregion
#region News
private function collectNews($html) {
$articles = $html->find('div.newsTopArticle')
or returnServerError('No articles found! Layout might have changed!');
foreach($articles as $article) {
$item = array();
// Common data
$item['uri'] = $this->extractNewsUri($article);
$item['timestamp'] = $this->extractNewsDate($article);
$item['title'] = $this->extractNewsTitle($article);
if($this->getInput('full')) {
$uri = $this->extractNewsUri($article);
$html = getSimpleHTMLDOMCached($uri)
or returnServerError('Failed loading full article from ' . $uri);
$fullArticle = $html->find('div.article', 0)
or returnServerError('No content found! Layout might have changed!');
defaultLinkTo($fullArticle, static::URI);
$item['author'] = $this->extractFullArticleAuthor($fullArticle);
$item['content'] = $this->extractFullArticleContent($fullArticle);
$item['enclosures'] = $this->extractImages($fullArticle);
} else {
$item['content'] = $this->extractNewsDescription($article);
$item['enclosures'] = $this->extractImages($article);
}
$this->items[] = $item;
}
}
private function extractNewsUri($article) {
$element = $article->find('a', 0)
or returnServerError('Anchor not found!');
return $element->href;
}
private function extractNewsDate($article) {
$element = $article->find('div.subheadline', 0)
or returnServerError('Date not found!');
// debugMessage($element->plaintext);
$date = trim(explode('|', $element->plaintext)[0]);
return $this->fixDate($date);
}
private function extractNewsDescription($article) {
$element = $article->find('span.newsText', 0)
or returnServerError('Description not found!');
$element->find('a', 0)->onclick = '';
// debugMessage($element->innertext);
return $element->innertext;
}
private function extractNewsTitle($article) {
$element = $article->find('h3', 0)
or returnServerError('Title not found!');
return $element->plaintext;
}
private function extractFullArticleContent($article) {
$element = $article->find('div.article_body', 0)
or returnServerError('Article body not found!');
// Remove teaser image
$element->find('img.teaser-img', 0)->outertext = '';
// Remove self advertisements
foreach($element->find('.call-action') as $adv) {
$adv->outertext = '';
}
// Remove tips
foreach($element->find('.panel-edu') as $tip) {
$tip->outertext = '';
}
// Remove inline scripts (used for i.e. interactive graphs) as they are
// rendered as a long series of strings
foreach($element->find('script') as $script) {
$script->outertext = '[Content removed! Visit site to see full contents!]';
}
return $element->innertext;
}
private function extractFullArticleAuthor($article) {
$element = $article->find('span[itemprop=name]', 0)
or returnServerError('Author not found!');
return $element->plaintext;
}
#endregion
#region Profile
private function collectProfile($html) {
$item = array();
$item['uri'] = $this->getURI();
$item['timestamp'] = $this->extractProfileDate($html);
$item['title'] = $this->extractProfiletitle($html);
$item['author'] = $this->extractProfileAuthor($html);
$item['content'] = $this->extractProfileContent($html);
$this->items[] = $item;
}
private function extractProfileDate($html) {
$element = $html->find('div.infobox div.vallabel', 0)
or returnServerError('Date not found!');
// debugMessage($element->plaintext);
$date = trim(explode("\r\n", $element->plaintext)[1]);
return $this->fixDate($date);
}
private function extractProfileTitle($html) {
$element = $html->find('span.h1', 0)
or returnServerError('Title not found!');
return $element->plaintext;
}
private function extractProfileContent($html) {
// There are a few thins we are interested:
// - Investment Strategy
// - Description
// - Quote
$strategy = $html->find('div.tab-container div.col-sm-6 p', 0)
or returnServerError('Investment Strategy not found!');
// Description requires a bit of cleanup due to lack of propper identification
$description = $html->find('div.headline', 5)
or returnServerError('Description container not found!');
$description = $description->parent();
foreach($description->find('div') as $div) {
$div->outertext = '';
}
$quote = $html->find('div.infobox div.val', 0)
or returnServerError('Quote not found!');
$quote_html = '<strong>Quote</strong><br><p>' . $quote . '</p>';
$strategy_html = '';
$description_html = '';
if($this->getInput('strategy') === true) {
$strategy_html = '<strong>Strategy</strong><br><p>' . $strategy . '</p><br>';
}
if($this->getInput('description') === true) {
$description_html = '<strong>Description</strong><br><p>' . $description . '</p><br>';
}
return $strategy_html . $description_html . $quote_html;
}
private function extractProfileAuthor($html) {
// Use ISIN + WKN as author
// Notice: "identfier" is not a typo [sic]!
$element = $html->find('span.identfier', 0)
or returnServerError('Author not found!');
return $element->plaintext;
}
#endregion
}

View File

@@ -45,9 +45,7 @@ class KernelBugTrackerBridge extends BridgeAbstract {
// We use the print preview page for simplicity
$html = getSimpleHTMLDOMCached($this->getURI() . '&format=multiple',
86400,
false,
null,
0,
null,
true,
true,

View File

@@ -58,7 +58,7 @@ class KununuBridge extends BridgeAbstract {
break;
}
return self::URI . $site . '/' . $company . '/' . $section;
return self::URI . $site . '/' . $company . '/' . $section . '?sort=update_time_desc';
}
return parent::getURI();
@@ -135,8 +135,8 @@ class KununuBridge extends BridgeAbstract {
* Encodes unmlauts in the given text
*/
private function encodeUmlauts($text){
$umlauts = Array("/ä/","/ö/","/ü/","/Ä/","/Ö/","/Ü/","/ß/");
$replace = Array("ae","oe","ue","Ae","Oe","Ue","ss");
$umlauts = Array('/ä/','/ö/','/ü/','/Ä/','/Ö/','/Ü/','/ß/');
$replace = Array('ae','oe','ue','Ae','Oe','Ue','ss');
return preg_replace($umlauts, $replace, $text);
}
@@ -190,11 +190,11 @@ class KununuBridge extends BridgeAbstract {
* Returns the URI from a given article
*/
private function extractArticleUri($article){
$anchor = $article->find('ku-company-review-more', 0);
$anchor = $article->find('h1.review-title a', 0);
if(is_null($anchor))
returnServerError('Cannot find article URI!');
return self::URI . $anchor->{'review-url'};
return self::URI . $anchor->href;
}
/**

View File

@@ -188,9 +188,9 @@ EOD;
$item = array();
$item['uri'] = self::URI.'#'.microtime(true);
$item['uri'] = self::URI.'#'.count($items);
$item['timestamp'] = $this->editionTimeStamp;//+$URICounter;
$item['timestamp'] = $this->editionTimeStamp;
$item['author'] = 'LWN';

296
bridges/LeBonCoinBridge.php Executable file → Normal file
View File

@@ -1,11 +1,10 @@
<?php
class LeBonCoinBridge extends BridgeAbstract {
const MAINTAINER = '16mhz';
const MAINTAINER = 'jacknumber';
const NAME = 'LeBonCoin';
const URI = 'https://www.leboncoin.fr/';
const DESCRIPTION = 'Returns most recent results from LeBonCoin for a
region, and optionally a category and a keyword .';
const DESCRIPTION = 'Returns most recent results from LeBonCoin';
const PARAMETERS = array(
array(
@@ -14,125 +13,138 @@ region, and optionally a category and a keyword .';
'name' => 'Région',
'type' => 'list',
'values' => array(
'Toute la France' => 'ile_de_france/occasions',
'Alsace' => 'alsace',
'Aquitaine' => 'aquitaine',
'Auvergne' => 'auvergne',
'Basse Normandie' => 'basse_normandie',
'Bourgogne' => 'bourgogne',
'Bretagne' => 'bretagne',
'Centre' => 'centre',
'Champagne Ardenne' => 'champagne_ardenne',
'Corse' => 'corse',
'Franche Comté' => 'franche_comte',
'Haute Normandie' => 'haute_normandie',
'Ile de France' => 'ile_de_france',
'Languedoc Roussillon' => 'languedoc_roussillon',
'Limousin' => 'limousin',
'Lorraine' => 'lorraine',
'Midi Pyrénées' => 'midi_pyrenees',
'Nord Pas De Calais' => 'nord_pas_de_calais',
'Pays de la Loire' => 'pays_de_la_loire',
'Picardie' => 'picardie',
'Poitou Charentes' => 'poitou_charentes',
'Provence Alpes Côte d\'Azur' => 'provence_alpes_cote_d_azur',
'Rhône-Alpes' => 'rhone_alpes',
'Guadeloupe' => 'guadeloupe',
'Martinique' => 'martinique',
'Guyane' => 'guyane',
'Réunion' => 'reunion'
'Toute la France' => '',
'Alsace' => '1',
'Aquitaine' => '2',
'Auvergne' => '3',
'Basse Normandie' => '4',
'Bourgogne' => '5',
'Bretagne' => '6',
'Centre' => '7',
'Champagne Ardenne' => '8',
'Corse' => '9',
'Franche Comté' => '10',
'Haute Normandie' => '11',
'Ile de France' => '12',
'Languedoc Roussillon' => '13',
'Limousin' => '14',
'Lorraine' => '15',
'Midi Pyrénées' => '16',
'Nord Pas De Calais' => '17',
'Pays de la Loire' => '18',
'Picardie' => '19',
'Poitou Charentes' => '20',
'Provence Alpes Côte d\'Azur' => '21',
'Rhône-Alpes' => '22',
'Guadeloupe' => '23',
'Martinique' => '24',
'Guyane' => '25',
'Réunion' => '26'
)
),
'cities' => array('name' => 'Ville'),
'c' => array(
'name' => 'Catégorie',
'type' => 'list',
'values' => array(
'TOUS' => '',
'EMPLOI' => '_emploi_',
'Toutes catégories' => '',
'EMPLOI' => array(
'Emploi et recrutement' => '71',
'Offres d\'emploi et jobs' => '33'
),
'VEHICULES' => array(
'Tous' => '_vehicules_',
'Voitures' => 'voitures',
'Motos' => 'motos',
'Caravaning' => 'caravaning',
'Utilitaires' => 'utilitaires',
'Équipement Auto' => 'equipement_auto',
'Équipement Moto' => 'equipement_moto',
'Équipement Caravaning' => 'equipement_caravaning',
'Nautisme' => 'nautisme',
'Équipement Nautisme' => 'equipement_nautisme'
'Tous' => '1',
'Voitures' => '2',
'Motos' => '3',
'Caravaning' => '4',
'Utilitaires' => '5',
'Equipement Auto' => '6',
'Equipement Moto' => '44',
'Equipement Caravaning' => '50',
'Nautisme' => '7',
'Equipement Nautisme' => '51'
),
'IMMOBILIER' => array(
'Tous' => '_immobilier_',
'Ventes immobilières' => 'ventes_immobilieres',
'Locations' => 'locations',
'Colocations' => 'colocations',
'Bureaux & Commerces' => 'bureaux_commerces'
'Tous' => '8',
'Ventes immobilières' => '9',
'Locations' => '10',
'Colocations' => '11',
'Bureaux & Commerces' => '13'
),
'VACANCES' => array(
'Tous' => '_vacances_',
'Location gîtes' => 'locations_gites',
'Chambres d\'hôtes' => 'chambres_d_hotes',
'Campings' => 'campings',
'Hôtels' => 'hotels',
'Hébergements insolites' => 'hebergements_insolites'
'Tous' => '66',
'Locations & Gîtes' => '12',
'Chambres d\'hôtes' => '67',
'Campings' => '68',
'Hôtels' => '69',
'Hébergements insolites' => '70'
),
'MULTIMEDIA' => array(
'Tous' => '_multimedia_',
'Informatique' => 'informatique',
'Consoles & Jeux vidéo' => 'consoles_jeux_video',
'Image & Son' => 'image_son',
'Téléphonie' => 'telephonie'
'Tous' => '14',
'Informatique' => '15',
'Consoles & Jeux vidéo' => '43',
'Image & Son' => '16',
'Téléphonie' => '17'
),
'LOISIRS' => array(
'Tous' => '_loisirs_',
'DVD / Films' => 'dvd_films',
'CD / Musique' => 'cd_musique',
'Livres' => 'livres',
'Animaux' => 'animaux',
'Vélos' => 'velos',
'Sports & Hobbies' => 'sports_hobbies',
'Instruments de musique' => 'instruments_de_musique',
'Collection' => 'collection',
'Jeux & Jouets' => 'jeux_jouets',
'Vins & Gastronomie' => 'vins_gastronomie'
'Tous' => '24',
'DVD / Films' => '25',
'CD / Musique' => '26',
'Livres' => '27',
'Animaux' => '28',
'Vélos' => '55',
'Sports & Hobbies' => '29',
'Instruments de musique' => '30',
'Collection' => '40',
'Jeux & Jouets' => '41',
'Vins & Gastronomie' => '48'
),
'MATÉRIEL PROFESSIONNEL' => array(
'Tous' => '_materiel_professionnel_',
'Matériel Agricole' => 'mateiel_agricole',
'Transport - Manutention' => 'transport_manutention',
'BTP - Chantier - Gros-œuvre' => 'btp_chantier_gros_oeuvre',
'Outillage - Matériaux 2nd-œuvre' => 'outillage_materiaux_2nd_oeuvre',
'Équipements Industriels' => 'equipement_industriels',
'Restauration - Hôtellerie' => 'restauration_hotellerie',
'Fournitures de Bureau' => 'fournitures_de_bureau',
'Commerces & Marchés' => 'commerces_marches',
'Matériel médical' => 'materiel_medical'
'MATERIEL PROFESSIONNEL' => array(
'Tous' => '56',
'Matériel Agricole' => '57',
'Transport - Manutention' => '58',
'BTP - Chantier Gros-oeuvre' => '59',
'Outillage - Matériaux 2nd-oeuvre' => '60',
'Équipements Industriels' => '32',
'Restauration - Hôtellerie' => '61',
'Fournitures de Bureau' => '62',
'Commerces & Marchés' => '63',
'Matériel Médical' => '64'
),
'SERVICES' => array(
'Tous' => '_services_',
'Prestations de services' => 'prestations_de_services',
'Billetterie' => 'billetterie',
'Évènements' => 'evenements',
'Cours particuliers' => 'cours_particuliers',
'Covoiturage' => 'covoiturage'
'Tous' => '31',
'Prestations de services' => '34',
'Billetterie' => '35',
'Evénements' => '49',
'Cours particuliers' => '36',
'Covoiturage' => '65'
),
'MAISON' => array(
'Tous' => '_maison_',
'Ameublement' => 'ameublement',
'Électroménager' => 'electromenager',
'Arts de la table' => 'arts_de_la_table',
'Décoration' => 'decoration',
'Linge de maison' => 'linge_de_maison',
'Bricolage' => 'bricolage',
'Jardinage' => 'jardinage',
'Vêtements' => 'vetements',
'Chaussures' => 'chaussures',
'Accessoires & Bagagerie' => 'accessoires_bagagerie',
'Montres & Bijoux' => 'montres_bijoux',
'Équipement bébé' => 'equipement_bebe',
'Vêtements bébé' => 'vetements_bebe'
'Tous' => '18',
'Ameublement' => '19',
'Electroménager' => '20',
'Arts de la table' => '45',
'Décoration' => '39',
'Linge de maison' => '46',
'Bricolage' => '21',
'Jardinage' => '52',
'Vêtements' => '22',
'Chaussures' => '53',
'Accessoires & Bagagerie' => '47',
'Montres & Bijoux' => '42',
'Equipement bébé' => '23',
'Vêtements bébé' => '54',
),
'AUTRES' => 'autres'
'AUTRES' => '37'
)
),
'o' => array(
'name' => 'Vendeur',
'type' => 'list',
'values' => array(
'Tous' => '',
'Particuliers' => 'private',
'Professionnels' => 'pro',
)
)
)
@@ -140,50 +152,72 @@ region, and optionally a category and a keyword .';
public function collectData(){
$category = $this->getInput('c');
if(empty($category)) {
$category = 'annonces';
$params = array(
'text' => $this->getInput('k'),
'region' => $this->getInput('r'),
'cities' => $this->getInput('cities'),
'category' => $this->getInput('c'),
'owner_type' => $this->getInput('o'),
);
$url = self::URI . 'recherche/?' . http_build_query($params);
$html = getContents($url)
or returnServerError('Could not request LeBonCoin. Tried: ' . $url);
if(!preg_match('/^<script>window.FLUX_STATE[^\r\n]*/m', $html, $matches)) {
returnServerError('Could not parse JSON in page content.');
}
$html = getSimpleHTMLDOM(self::URI
. $category
. '/offres/'
. $this->getInput('r')
. '/?f=a&th=1&q='
. urlencode($this->getInput('k')))
or returnServerError('Could not request LeBonCoin.');
$clean_match = str_replace(
array('</script>', '<script>window.FLUX_STATE = '),
array('', ''),
$matches[0]
);
$json = json_decode($clean_match);
$list = $html->find('.tabsContent', 0);
if($list === null) {
if($json->adSearch->data->total === 0) {
return;
}
$tags = $list->find('li');
foreach($json->adSearch->data->ads as $element) {
foreach($tags as $element) {
$item['title'] = $element->subject;
$item['content'] = $element->body;
$item['date'] = $element->index_date;
$item['timestamp'] = strtotime($element->index_date);
$item['uri'] = $element->url;
$item['ad_type'] = $element->ad_type;
$item['author'] = $element->owner->name;
$element = $element->find('a', 0);
if(isset($element->location->city)) {
$item = array();
$item['uri'] = $element->href;
$title = html_entity_decode($element->getAttribute('title'));
$content_image = $element->find('div.item_image', 0)->find('.lazyload', 0);
$item['city'] = $element->location->city;
$item['content'] .= ' -- ' . $element->location->city;
if($content_image !== null) {
$content = '<img src="' . $content_image->getAttribute('data-imgsrc') . '" alt="thumbnail">';
} else {
$content = "";
}
$date = $element->find('aside.item_absolute', 0)->find('p.item_sup', 0);
$detailsList = $element->find('section.item_infos', 0);
if(isset($element->location->zipcode)) {
$item['zipcode'] = $element->location->zipcode;
}
for($i = 0; $i <= 1; $i++) $content .= $detailsList->find('p.item_supp', $i)->plaintext;
$price = $detailsList->find('h3.item_price', 0);
$content .= $price === null ? '' : $price->plaintext;
if(isset($element->price)) {
$item['price'] = $element->price[0];
$item['content'] .= ' -- ' . current($element->price) . '€';
}
if(isset($element->images->urls)) {
$item['thumbnail'] = $element->images->thumb_url;
$item['enclosures'] = array();
foreach($element->images->urls as $image) {
$item['enclosures'][] = $image;
}
}
$item['title'] = $title;
$item['content'] = $content . $date;
$this->items[] = $item;
}
}

View File

@@ -42,10 +42,10 @@ class LegifranceJOBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI)
or $this->returnServer('Unable to download ' . self::URI);
$this->author = trim($html->find('h2.title', 0)->plaintext);
$this->author = trim($html->find('h2.titleJO', 0)->plaintext);
$uri = $html->find('h2.titleELI', 0)->plaintext;
$this->uri = trim(substr($uri, strpos($uri, 'https')));
$this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/')));
$this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/'), -5));
foreach($html->find('h3') as $section) {
$subsections = $section->nextSibling()->find('h4');

View File

@@ -22,16 +22,16 @@ class LesJoiesDuCodeBridge extends BridgeAbstract {
// retrieve .gif instead of static .jpg
$images = $temp->find('p img');
foreach($images as $image) {
$img_src = str_replace(".jpg", ".gif", $image->src);
$img_src = str_replace('.jpg', '.gif', $image->src);
$image->src = $img_src;
}
$content = $temp->innertext;
$auteur = $temp->find('i', 0);
$pos = strpos($auteur->innertext, "by");
$pos = strpos($auteur->innertext, 'by');
if($pos > 0) {
$auteur = trim(str_replace("*/", "", substr($auteur->innertext, ($pos + 2))));
$auteur = trim(str_replace('*/', '', substr($auteur->innertext, ($pos + 2))));
$item['author'] = $auteur;
}

View File

@@ -100,7 +100,7 @@ class MangareaderBridge extends BridgeAbstract {
case 'Get popular mangas':
// Find manga name within "Popular mangas for ..."
$pagetitle = $xpath->query(".//*[@id='bodyalt']/h1")->item(0)->nodeValue;
$this->request = substr($pagetitle, 0, strrpos($pagetitle, " -"));
$this->request = substr($pagetitle, 0, strrpos($pagetitle, ' -'));
$this->getPopularMangas($xpath);
break;
case 'Get manga updates':
@@ -120,7 +120,7 @@ class MangareaderBridge extends BridgeAbstract {
// Return some dummy-data if no content available
if(empty($this->items)) {
$item = array();
$item['content'] = "<p>No updates available</p>";
$item['content'] = '<p>No updates available</p>';
$this->items[] = $item;
}
@@ -143,18 +143,18 @@ class MangareaderBridge extends BridgeAbstract {
$item['title'] = htmlspecialchars($manga->nodeValue);
// Add each chapter to the feed
$item['content'] = "";
$item['content'] = '';
foreach ($chapters as $chapter) {
if($item['content'] <> "") {
$item['content'] .= "<br>";
if($item['content'] <> '') {
$item['content'] .= '<br>';
}
$item['content'] .= "<a href='"
. self::URI
. htmlspecialchars($chapter->getAttribute('href'))
. "'>"
. htmlspecialchars($chapter->nodeValue)
. "</a>";
. '</a>';
}
$this->items[] = $item;
@@ -211,13 +211,13 @@ EOD;
foreach ($chapters as $chapter) {
$item = array();
$item['title'] = htmlspecialchars($xpath->query("td[1]", $chapter)
$item['title'] = htmlspecialchars($xpath->query('td[1]', $chapter)
->item(0)
->nodeValue);
$item['uri'] = self::URI . $xpath->query("td[1]/a", $chapter)
$item['uri'] = self::URI . $xpath->query('td[1]/a', $chapter)
->item(0)
->getAttribute('href');
$item['timestamp'] = strtotime($xpath->query("td[2]", $chapter)
$item['timestamp'] = strtotime($xpath->query('td[2]', $chapter)
->item(0)
->nodeValue);
array_unshift($this->items, $item);
@@ -227,12 +227,12 @@ EOD;
public function getURI(){
switch($this->queriedContext) {
case 'Get latest updates':
$path = "latest";
$path = 'latest';
break;
case 'Get popular mangas':
$path = "popular";
if($this->getInput('category') !== "all") {
$path .= "/" . $this->getInput('category');
$path = 'popular';
if($this->getInput('category') !== 'all') {
$path .= '/' . $this->getInput('category');
}
break;
case 'Get manga updates':

View File

@@ -4,7 +4,7 @@ class MixCloudBridge extends BridgeAbstract {
const MAINTAINER = 'Alexis CHEMEL';
const NAME = 'MixCloud';
const URI = 'https://mixcloud.com/';
const URI = 'https://www.mixcloud.com';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns latest musics on user stream';
@@ -24,8 +24,9 @@ class MixCloudBridge extends BridgeAbstract {
}
public function collectData(){
ini_set('user_agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0');
$html = getSimpleHTMLDOM(self::URI . $this->getInput('u'))
$html = getSimpleHTMLDOM(self::URI . '/' . $this->getInput('u'))
or returnServerError('Could not request MixCloud.');
foreach($html->find('section.card') as $element) {

144
bridges/MydealsBridge.php Normal file
View File

@@ -0,0 +1,144 @@
<?php
require_once(__DIR__ . '/DealabsBridge.php');
class MydealsBridge extends PepperBridgeAbstract {
const NAME = 'Mydeals bridge';
const URI = 'https://www.mydealz.de/';
const DESCRIPTION = 'Zeigt die Deals von mydeals.de';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
'Suche nach Stichworten' => array (
'q' => array(
'name' => 'Stichworten',
'type' => 'text',
'required' => true
),
'hide_expired' => array(
'name' => 'Abgelaufenes ausblenden',
'type' => 'checkbox',
'required' => 'true'
),
'hide_local' => array(
'name' => 'Lokales ausblenden',
'type' => 'checkbox',
'title' => 'Deals im physischen Geschäft ausblenden',
'required' => 'true'
),
'priceFrom' => array(
'name' => 'Minimaler Preis',
'type' => 'text',
'title' => 'Minmaler Preis in Euros',
'required' => 'false',
'defaultValue' => ''
),
'priceTo' => array(
'name' => 'Maximaler Preis',
'type' => 'text',
'title' => 'maximaler Preis in Euro',
'required' => 'false',
'defaultValue' => ''
),
),
'Deals pro Gruppen' => array(
'group' => array(
'name' => 'Gruppen',
'type' => 'list',
'required' => 'true',
'title' => 'Gruppe, deren Deals angezeigt werden müssen',
'values' => array(
'Elektronik' => 'elektronik',
'Handy & Smartphone' => 'smartphone',
'Gaming' => 'gaming',
'Software' => 'apps-software',
'Fashion Frauen' => 'fashion-frauen',
'Fashion Männer' => 'fashion-accessoires',
'Beauty & Gesundheit' => 'beauty',
'Family & Kids' => 'family-kids',
'Essen & Trinken' => 'food',
'Freizeit & Reisen' => 'reisen',
'Haushalt & Garten' => 'home-living',
'Entertainment' => 'entertainment',
'Verträge & Finanzen' => 'vertraege-finanzen',
'Coupons' => 'coupons',
)
),
'order' => array(
'name' => 'sortieren nach',
'type' => 'list',
'required' => 'true',
'title' => 'Sortierung der deals',
'values' => array(
'Vom heißesten zum kältesten Deal' => '',
'Vom jüngsten Deal zum ältesten' => '-new',
'Vom am meisten kommentierten Deal zum am wenigsten kommentierten Deal' => '-discussed'
)
)
)
);
public $lang = array(
'bridge-uri' => SELF::URI,
'bridge-name' => SELF::NAME,
'context-keyword' => 'Suche nach Stichworten',
'context-group' => 'Deals pro Gruppen',
'uri-group' => '/gruppe/',
'request-error' => 'Could not request mydeals',
'no-results' => 'Ups, wir konnten keine Deals zu',
'relative-date-indicator' => array(
'vor',
'seit'
),
'price' => 'Preis',
'shipping' => 'Versand',
'origin' => 'Ursprung',
'discount' => 'Rabatte',
'title-keyword' => 'Suche',
'title-group' => 'Gruppe',
'local-months' => array(
'Jan',
'Feb',
'Mär',
'Apr',
'Mai',
'Jun',
'Jul',
'Aug',
'Sep',
'Okt',
'Nov',
'Dez',
'.'
),
'local-time-relative' => array(
'eingestellt vor ',
'm',
'h,',
'day',
'days',
'month',
'year',
'and '
),
'date-prefixes' => array(
'eingestellt am ',
'lokal ',
'aktualisiert ',
),
'relative-date-alt-prefixes' => array(
'aktualisiert vor ',
'kommentiert vor ',
'heiß seit '
),
'relative-date-ignore-suffix' => array(
'/von.*$/'
),
'localdeal' => array(
'Lokal ',
'Läuft bis '
)
);
}

View File

@@ -12,7 +12,7 @@ class NasaApodBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI . 'archivepix.html')
or returnServerError('Error while downloading the website content');
$list = explode("<br>", $html->find('b', 0)->innertext);
$list = explode('<br>', $html->find('b', 0)->innertext);
for($i = 0; $i < 3; $i++) {
$line = $list[$i];
@@ -32,7 +32,7 @@ class NasaApodBridge extends BridgeAbstract {
$explanation = $picture_html->find('p', 2)->innertext;
//Extract date from the picture page
$date = explode(" ", $picture_html->find('p', 1)->innertext);
$date = explode(' ', $picture_html->find('p', 1)->innertext);
$item['timestamp'] = strtotime($date[4] . $date[3] . $date[2]);
//Other informations

View File

@@ -0,0 +1,57 @@
<?php
class NotAlwaysBridge extends BridgeAbstract {
const MAINTAINER = 'mozes';
const NAME = 'Not Always family Bridge';
const URI = 'https://notalwaysright.com/';
const DESCRIPTION = 'Returns the latest stories';
const CACHE_TIMEOUT = 1800; // 30 minutes
const PARAMETERS = array( array(
'filter' => array(
'type' => 'list',
'name' => 'Filter',
'values' => array(
'All' => 'all',
'Right' => 'right',
'Working' => 'working',
'Romantic' => 'romantic',
'Related' => 'related',
'Learning' => 'learning',
'Friendly' => 'friendly',
'Hopeless' => 'hopeless',
'Unfiltered' => 'unfiltered'
),
'required' => true
)
));
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request NotAlways.');
foreach($html->find('.post') as $post) {
#print_r($post);
$item = array();
$item['uri'] = $post->find('h1', 0)->find('a', 0)->href;
$item['content'] = $post;
$item['title'] = $post->find('h1', 0)->find('a', 0)->innertext;
$this->items[] = $item;
}
}
public function getName(){
if(!is_null($this->getInput('filter'))) {
return $this->getInput('filter') . ' - NotAlways Bridge';
}
return parent::getName();
}
public function getURI(){
if(!is_null($this->getInput('filter'))) {
return self::URI . $this->getInput('filter') . '/';
}
return parent::getURI();
}
}

23
bridges/PcGamerBridge.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
class PcGamerBridge extends BridgeAbstract
{
const NAME = 'PC Gamer';
const URI = 'https://www.pcgamer.com/';
const DESCRIPTION = 'PC Gamer Most Read Stories';
const MAINTAINER = 'mdemoss';
public function collectData()
{
$html = getSimpleHTMLDOMCached($this->getURI(), 300);
$stories = $html->find('div#popularcontent li.most-popular-item');
foreach ($stories as $element) {
$item['uri'] = $element->find('a', 0)->href;
$articleHtml = getSimpleHTMLDOMCached($item['uri']);
$item['title'] = $element->find('h4 a', 0)->plaintext;
$item['timestamp'] = strtotime($articleHtml->find('meta[name=pub_date]', 0)->content);
$item['content'] = $articleHtml->find('meta[name=description]', 0)->content;
$item['author'] = $articleHtml->find('a[itemprop=author]', 0)->plaintext;
$this->items[] = $item;
}
}
}

View File

@@ -15,12 +15,6 @@ class PinterestBridge extends FeedExpander {
'b' => array(
'name' => 'board',
'required' => true
),
'r' => array(
'name' => 'Use custom RSS',
'type' => 'checkbox',
'required' => false,
'title' => 'Uncheck to return data via custom filters (more data)'
)
),
'From search' => array(
@@ -34,12 +28,8 @@ class PinterestBridge extends FeedExpander {
public function collectData(){
switch($this->queriedContext) {
case 'By username and board':
if($this->getInput('r')) {
$html = getSimpleHTMLDOMCached($this->getURI());
$this->getUserResults($html);
} else {
$this->collectExpandableDatas($this->getURI() . '.rss');
}
$this->collectExpandableDatas($this->getURI() . '.rss');
$this->fixLowRes();
break;
case 'From search':
default:
@@ -48,49 +38,17 @@ class PinterestBridge extends FeedExpander {
}
}
private function getUserResults($html){
$json = json_decode($html->find('#jsInit1', 0)->innertext, true);
$results = $json['tree']['children'][0]['children'][0]['children'][0]['options']['props']['data']['board_feed'];
$username = $json['resourceDataCache'][0]['data']['owner']['username'];
$fullname = $json['resourceDataCache'][0]['data']['owner']['full_name'];
$avatar = $json['resourceDataCache'][0]['data']['owner']['image_small_url'];
private function fixLowRes() {
foreach($results as $result) {
$item = array();
$newitems = [];
$pattern = '/https\:\/\/i\.pinimg\.com\/[a-zA-Z0-9]*x\//';
foreach($this->items as $item) {
$item['uri'] = $result['link'];
// Some use regular titles, others provide 'advanced' infos, a few
// provide even less info. Thus we attempt multiple options.
$item['title'] = trim($result['title']);
if($item['title'] === "")
$item['title'] = trim($result['rich_summary']['display_name']);
if($item['title'] === "")
$item['title'] = trim($result['description']);
$item['timestamp'] = strtotime($result['created_at']);
$item['username'] = $username;
$item['fullname'] = $fullname;
$item['avatar'] = $avatar;
$item['author'] = $item['username'] . ' (' . $item['fullname'] . ')';
$item['content'] = '<img align="left" style="margin: 2px 4px;" src="'
. htmlentities($item['avatar'])
. '" /><p><strong>'
. $item['username']
. '</strong><br>'
. $item['fullname']
. '</p><br><img src="'
. $result['images']['736x']['url']
. '" alt="" /><br><p>'
. $result['description']
. '</p>';
$item['enclosures'] = array($result['images']['orig']['url']);
$this->items[] = $item;
$item['content'] = preg_replace($pattern, 'https://i.pinimg.com/originals/', $item['content']);
$newitems[] = $item;
}
$this->items = $newitems;
}
private function getSearchResults($html){
@@ -106,10 +64,10 @@ class PinterestBridge extends FeedExpander {
// provide even less info. Thus we attempt multiple options.
$item['title'] = trim($result['title']);
if($item['title'] === "")
if($item['title'] === '')
$item['title'] = trim($result['rich_summary']['display_name']);
if($item['title'] === "")
if($item['title'] === '')
$item['title'] = trim($result['grid_description']);
$item['timestamp'] = strtotime($result['created_at']);

73
bridges/PixivBridge.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
class PixivBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'Pixiv Bridge';
const URI = 'https://www.pixiv.net/';
const DESCRIPTION = 'Returns the tag search from pixiv.net';
const PARAMETERS = array( array(
'tag' => array(
'name' => 'Tag to search',
'exampleValue' => 'example',
'required' => true
),
));
public function collectData(){
$html = getContents(static::URI.'search.php?word=' . urlencode($this->getInput('tag')))
or returnClientError('Unable to query pixiv.net');
$regex = '/<input type="hidden"id="js-mount-point-search-result-list"data-items="([^"]*)/';
$timeRegex = '/img\/([0-9]{4})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\//';
preg_match_all($regex, $html, $matches, PREG_SET_ORDER, 0);
if(!$matches) return;
$content = json_decode(html_entity_decode($matches[0][1]), true);
$count = 0;
foreach($content as $result) {
if($count == 10) break;
$count++;
$item = array();
$item['id'] = $result['illustId'];
$item['uri'] = 'https://www.pixiv.net/member_illust.php?mode=medium&illust_id=' . $result['illustId'];
$item['title'] = $result['illustTitle'];
$item['author'] = $result['userName'];
preg_match_all($timeRegex, $result['url'], $dt, PREG_SET_ORDER, 0);
$elementDate = DateTime::createFromFormat('YmdHis',
$dt[0][1] . $dt[0][2] . $dt[0][3] . $dt[0][4] . $dt[0][5] . $dt[0][6]);
$item['timestamp'] = $elementDate->getTimestamp();
$item['content'] = "<img src='" . $this->cacheImage($result['url'], $item['id']) . "' />";
$this->items[] = $item;
}
}
public function cacheImage($url, $illustId) {
$url = str_replace('_master1200', '', $url);
$url = str_replace('c/240x240/img-master/', 'img-original/', $url);
$path = CACHE_DIR . '/pixiv_img';
if(!is_dir($path))
mkdir($path, 0755, true);
if(!is_file($path . '/' . $illustId . '.jpeg')) {
$headers = array('Referer: https://www.pixiv.net/member_illust.php?mode=medium&illust_id=' . $illustId);
$illust = getContents($url, $headers);
if(strpos($illust, '404 Not Found') !== false) {
$illust = getContents(str_replace('jpg', 'png', $url), $headers);
}
file_put_contents($path . '/' . $illustId . '.jpeg', $illust);
}
return 'cache/pixiv_img/' . $illustId . '.jpeg';
}
}

View File

@@ -1,38 +0,0 @@
<?php
class PlanetLibreBridge extends BridgeAbstract {
const MAINTAINER = 'pit-fgfjiudghdf';
const NAME = 'PlanetLibre';
const URI = 'http://www.planet-libre.org';
const DESCRIPTION = 'Returns the 5 newest posts from PlanetLibre (full text)';
private function extractContent($url){
$html2 = getSimpleHTMLDOM($url);
$text = $html2->find('div[class="post-text"]', 0)->innertext;
return $text;
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request PlanetLibre.');
$limit = 0;
foreach($html->find('div.post') as $element) {
if($limit < 5) {
$item = array();
$item['title'] = $element->find('h1', 0)->plaintext;
$item['uri'] = $element->find('a', 0)->href;
$item['timestamp'] = strtotime(
str_replace(
'/',
'-',
$element->find('div[class="post-date"]', 0)->plaintext
)
);
$item['content'] = $this->extractContent($item['uri']);
$this->items[] = $item;
$limit++;
}
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
class RadioMelodieBridge extends BridgeAbstract {
const NAME = 'Radio Melodie Actu';
const URI = 'https://www.radiomelodie.com/';
const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie';
const MAINTAINER = 'sysadminstory';
public function collectData(){
$html = getSimpleHTMLDOM(self::URI . 'actu')
or returnServerError('Could not request Radio Melodie.');
$list = $html->find('div[class=actuitem]');
foreach($list as $element) {
$item = array();
// Get picture URL
$pictureHTML = $element->find('div[class=picture]');
preg_match(
'/background-image:url\((.*)\);/',
$pictureHTML[0]->getAttribute('style'),
$pictures);
$pictureURL = $pictures[1];
$item['enclosures'] = array($pictureURL);
$item['uri'] = self::URI . $element->parent()->href;
$item['title'] = $element->find('h3', 0)->plaintext;
$item['content'] = $element->find('p', 0)->plaintext . '<br/><img src="'.$pictureURL.'"/>';
$this->items[] = $item;
}
}
}

View File

@@ -8,9 +8,9 @@ class RainbowSixSiegeBridge extends BridgeAbstract {
const DESCRIPTION = 'Latest articles from the Rainbow Six Siege blog';
public function collectData(){
$dlUrl = "https://prod-tridionservice.ubisoft.com/live/v1/News/Latest?templateId=tcm%3A152-7677";
$dlUrl .= "8-32&pageIndex=0&pageSize=10&language=en-US&detailPageId=tcm%3A152-194572-64";
$dlUrl .= "&keywordList=175426&siteId=undefined&useSeoFriendlyUrl=true";
$dlUrl = 'https://prod-tridionservice.ubisoft.com/live/v1/News/Latest?templateId=tcm%3A152-7677';
$dlUrl .= '8-32&pageIndex=0&pageSize=10&language=en-US&detailPageId=tcm%3A152-194572-64';
$dlUrl .= '&keywordList=175426&siteId=undefined&useSeoFriendlyUrl=true';
$jsonString = getContents($dlUrl) or returnServerError('Error while downloading the website content');
$json = json_decode($jsonString, true);

View File

@@ -25,7 +25,7 @@ class ReadComicsBridge extends BridgeAbstract {
return $timestamp;
}
$keywordsList = explode(";", $this->getInput('q'));
$keywordsList = explode(';', $this->getInput('q'));
foreach($keywordsList as $keywords) {
$html = $this->getSimpleHTMLDOM(self::URI . 'comic/' . rawurlencode($keywords))
or $this->returnServerError('Could not request readcomics.tv.');

View File

@@ -19,7 +19,7 @@ class ReporterreBridge extends BridgeAbstract {
// Replace all relative urls with absolute ones
$text = preg_replace(
'/(href|src)(\=[\"\'])(?!http)([^"\']+)/ims',
"$1$2" . self::URI . "$3",
'$1$2' . self::URI . '$3',
$text
);

View File

@@ -9,9 +9,9 @@ class Rue89Bridge extends FeedExpander {
protected function parseItem($item){
$item = parent::parseItem($item);
$url = "http://api.rue89.nouvelobs.com/export/mobile2/node/"
. str_replace(" ", "", substr($item['uri'], -8))
. "/full";
$url = 'http://api.rue89.nouvelobs.com/export/mobile2/node/'
. str_replace(' ', '', substr($item['uri'], -8))
. '/full';
$datas = json_decode(getContents($url), true);
$item['content'] = $datas['node']['body'];

View File

@@ -32,7 +32,7 @@ class SexactuBridge extends BridgeAbstract {
$item = array();
$item['author'] = self::AUTHOR;
$item['title'] = $title->plaintext;
$urlAttribute = "data-href";
$urlAttribute = 'data-href';
$uri = $title->$urlAttribute;
if($uri === false)
continue;

View File

@@ -73,7 +73,7 @@ class ShanaprojectBridge extends BridgeAbstract {
// Getting the picture is a little bit tricky as it is part of the style.
// Luckily the style is part of the parent div :)
if(preg_match("/url\(\/\/([^\)]+)\)/i", $anime->parent->style, $matches))
if(preg_match('/url\(\/\/([^\)]+)\)/i', $anime->parent->style, $matches))
return $matches[1];
returnServerError('Could not extract background image!');

View File

@@ -21,7 +21,7 @@ class Shimmie2Bridge extends DanbooruBridge {
protected function getItemFromElement($element){
$item = array();
$item['uri'] = $this->getURI() . $element->href;
$item['id'] = (int)preg_replace("/[^0-9]/", '', $element->getAttribute(static::IDATTRIBUTE));
$item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
$item['timestamp'] = time();
$thumbnailUri = $this->getURI() . $element->find('img', 0)->src;
$item['tags'] = $element->getAttribute('data-tags');

825
bridges/SkimfeedBridge.php Normal file
View File

@@ -0,0 +1,825 @@
<?php
class SkimfeedBridge extends BridgeAbstract {
const CONTEXT_NEWS_BOX = 'News box';
const CONTEXT_HOT_TOPICS = 'Hot topics';
const CONTEXT_TECH_NEWS = 'Tech news';
const CONTEXT_CUSTOM = 'Custom feed';
const NAME = 'Skimfeed Bridge';
const URI = 'https://skimfeed.com';
const DESCRIPTION = 'Returns feeds from Skimfeed, also supports custom feeds!';
const MAINTAINER = 'logmanoriginal';
const CACHE_TIMEOUT = 3600;
const PARAMETERS = array(
self::CONTEXT_NEWS_BOX => array( // auto-generated (see below)
'box_channel' => array(
'name' => 'Channel',
'type' => 'list',
'required' => true,
'title' => 'Select your channel',
'values' => array(
'Hacker News' => '/news/hacker-news.html',
'QZ' => '/news/qz.html',
'The Verge' => '/news/the-verge.html',
'Slashdot' => '/news/slashdot.html',
'Lifehacker' => '/news/lifehacker.html',
'Gizmag' => '/news/gizmag.html',
'Fast Company' => '/news/fast-company.html',
'Engadget' => '/news/engadget.html',
'Wired' => '/news/wired.html',
'MakeUseOf' => '/news/makeuseof.html',
'Techcrunch' => '/news/techcrunch.html',
'Apple Insider' => '/news/apple-insider.html',
'ArsTechnica' => '/news/arstechnica.html',
'Tech in Asia' => '/news/tech-in-asia.html',
'FastCoExist' => '/news/fastcoexist.html',
'Digital Trends' => '/news/digital-trends.html',
'AnandTech' => '/news/anandtech.html',
'How to Geek' => '/news/how-to-geek.html',
'Geek' => '/news/geek.html',
'BBC Technology' => '/news/bbc-technology.html',
'Extreme Tech' => '/news/extreme-tech.html',
'Packet Storm Sec' => '/news/packet-storm-sec.html',
'MedGadget' => '/news/medgadget.html',
'Design' => '/news/design.html',
'The Next Web' => '/news/the-next-web.html',
'Bit-Tech' => '/news/bit-tech.html',
'Next Big Future' => '/news/next-big-future.html',
'A VC' => '/news/a-vc.html',
'Copyblogger' => '/news/copyblogger.html',
'Smashing Mag' => '/news/smashing-mag.html',
'Continuations' => '/news/continuations.html',
'Cult of Mac' => '/news/cult-of-mac.html',
'SecuriTeam' => '/news/securiteam.html',
'The Tech Block' => '/news/the-tech-block.html',
'BetaBeat' => '/news/betabeat.html',
'PC Mag' => '/news/pc-mag.html',
'Venture Beat' => '/news/venture-beat.html',
'ReadWriteWeb' => '/news/readwriteweb.html',
'High Scalability' => '/news/high-scalability.html',
)
)
),
self::CONTEXT_HOT_TOPICS => array(),
self::CONTEXT_TECH_NEWS => array( // auto-generated (see below)
'tech_channel' => array(
'name' => 'Tech channel',
'type' => 'list',
'required' => true,
'title' => 'Select your tech channel',
'values' => array(
'Agg' => array(
'Reddit' => '/news/reddit.html',
'Tech Insider' => '/news/tech-insider.html',
'Digg' => '/news/digg.html',
'Meta Filter' => '/news/meta-filter.html',
'Fark' => '/news/fark.html',
'Mashable' => '/news/mashable.html',
'Ad Week' => '/news/ad-week.html',
'The Chive' => '/news/the-chive.html',
'BoingBoing' => '/news/boingboing.html',
'Vice' => '/news/vice.html',
'ClientsFromHell' => '/news/clientsfromhell.html',
'How Stuff Works' => '/news/how-stuff-works.html',
'Buzzfeed' => '/news/buzzfeed.html',
'BoingBoing' => '/news/boingboing.html',
'Cracked' => '/news/cracked.html',
'Weird News' => '/news/weird-news.html',
'ITOTD' => '/news/itotd.html',
'Metafilter' => '/news/metafilter.html',
'TheOnion' => '/news/theonion.html',
),
'Cars' => array(
'Reddit Cars' => '/news/reddit-cars.html',
'NYT Auto' => '/news/nyt-auto.html',
'Truth About Cars' => '/news/truth-about-cars.html',
'AutoBlog' => '/news/autoblog.html',
'AutoSpies' => '/news/autospies.html',
'Autoweek' => '/news/autoweek.html',
'The Garage' => '/news/the-garage.html',
'Car and Driver' => '/news/car-and-driver.html',
'EGM Car Tech' => '/news/egm-car-tech.html',
'Top Gear' => '/news/top-gear.html',
'eGarage' => '/news/egarage.html',
),
'Comics' => array(
'Penny Arcade' => '/news/penny-arcade.html',
'XKCD' => '/news/xkcd.html',
'Channelate' => '/news/channelate.html',
'Savage Chicken' => '/news/savage-chicken.html',
'Dinosaur Comics' => '/news/dinosaur-comics.html',
'Explosm' => '/news/explosm.html',
'PoorlyDLines' => '/news/poorlydlines.html',
'Moonbeard' => '/news/moonbeard.html',
'Nedroid' => '/news/nedroid.html',
),
'Design' => array(
'FastCoCreate' => '/news/fastcocreate.html',
'Dezeen' => '/news/dezeen.html',
'Design Boom' => '/news/design-boom.html',
'Mmminimal' => '/news/mmminimal.html',
'We Heart' => '/news/we-heart.html',
'CreativeBloq' => '/news/creativebloq.html',
'TheDSGNblog' => '/news/thedsgnblog.html',
'Grainedit' => '/news/grainedit.html',
),
'Football' => array(
'Mail Football' => '/news/mail-football.html',
'Yahoo Football' => '/news/yahoo-football.html',
'FourFourTwo' => '/news/fourfourtwo.html',
'Goal' => '/news/goal.html',
'BBC Football' => '/news/bbc-football.html',
'TalkSport' => '/news/talksport.html',
'101 Great Goals' => '/news/101-great-goals.html',
'Who Scored' => '/news/who-scored.html',
'Football365 Champ' => '/news/football365-champ.html',
'Football365 Premier' => '/news/football365-premier.html',
'BleacherReport' => '/news/bleacherreport.html',
),
'Gaming' => array(
'Polygon' => '/news/polygon.html',
'Gamespot' => '/news/gamespot.html',
'RockPaperShotgun' => '/news/rockpapershotgun.html',
'VG247' => '/news/vg247.html',
'IGN' => '/news/ign.html',
'Reddit Games' => '/news/reddit-games.html',
'TouchArcade' => '/news/toucharcade.html',
'GamesRadar' => '/news/gamesradar.html',
'Siliconera' => '/news/siliconera.html',
'Reddit GameDeals' => '/news/reddit-gamedeals.html',
'Joystiq' => '/news/joystiq.html',
'GameInformer' => '/news/gameinformer.html',
'PSN Blog' => '/news/psn-blog.html',
'Reddit GamerNews' => '/news/reddit-gamernews.html',
'Steam' => '/news/steam.html',
'DualShockers' => '/news/dualshockers.html',
'ShackNews' => '/news/shacknews.html',
'CheapAssGamer' => '/news/cheapassgamer.html',
'Eurogamer' => '/news/eurogamer.html',
'Major Nelson' => '/news/major-nelson.html',
'Reddit Truegaming' => '/news/reddit-truegaming.html',
'GameTrailers' => '/news/gametrailers.html',
'GamaSutra' => '/news/gamasutra.html',
'USGamer' => '/news/usgamer.html',
'Shoryuken' => '/news/shoryuken.html',
'Destructoid' => '/news/destructoid.html',
'ArsGaming' => '/news/arsgaming.html',
'XBOX Blog' => '/news/xbox-blog.html',
'GiantBomb' => '/news/giantbomb.html',
'VideoGamer' => '/news/videogamer.html',
'Pocket Tactics' => '/news/pocket-tactics.html',
'WiredGaming' => '/news/wiredgaming.html',
'AllGamesBeta' => '/news/allgamesbeta.html',
'OnGamers' => '/news/ongamers.html',
'Reddit GameBundles' => '/news/reddit-gamebundles.html',
'Kotaku' => '/news/kotaku.html',
'PCGamer' => '/news/pcgamer.html',
),
'Investing' => array(
'Seeking Alpha' => '/news/seeking-alpha.html',
'BBC Business' => '/news/bbc-business.html',
'Harvard Biz' => '/news/harvard-biz.html',
'Market Watch' => '/news/market-watch.html',
'Investor Place' => '/news/investor-place.html',
'Money Week' => '/news/money-week.html',
'Moneybeat' => '/news/moneybeat.html',
'Dealbook' => '/news/dealbook.html',
'Economist Business' => '/news/economist-business.html',
'Economist' => '/news/economist.html',
'Economist CN' => '/news/economist-cn.html',
),
'Long' => array(
'The Atlantic' => '/news/the-atlantic.html',
'Reddit Long' => '/news/reddit-long.html',
'Paris Review' => '/news/paris-review.html',
'New Yorker' => '/news/new-yorker.html',
'LongForm' => '/news/longform.html',
'LongReads' => '/news/longreads.html',
'The Browser' => '/news/the-browser.html',
'The Feature' => '/news/the-feature.html',
),
'MMA' => array(
'MMA Weekly' => '/news/mma-weekly.html',
'MMAFighting' => '/news/mmafighting.html',
'Reddit MMA' => '/news/reddit-mma.html',
'Sherdog Articles' => '/news/sherdog-articles.html',
'FightLand Vice' => '/news/fightland-vice.html',
'Sherdog Forum' => '/news/sherdog-forum.html',
'MMA Junkie' => '/news/mma-junkie.html',
'Sherdog MMA Video' => '/news/sherdog-mma-video.html',
'BloodyElbow' => '/news/bloodyelbow.html',
'CageWriter' => '/news/cagewriter.html',
'Sherdog News' => '/news/sherdog-news.html',
'MMAForum' => '/news/mmaforum.html',
'MMA Junkie Radio' => '/news/mma-junkie-radio.html',
'UFC News' => '/news/ufc-news.html',
'FightLinker' => '/news/fightlinker.html',
'Bodybuilding MMA' => '/news/bodybuilding-mma.html',
'BleacherReport MMA' => '/news/bleacherreport-mma.html',
'FiveOuncesofPain' => '/news/fiveouncesofpain.html',
'Sherdog Pictures' => '/news/sherdog-pictures.html',
'CagePotato' => '/news/cagepotato.html',
'Sherdog Radio' => '/news/sherdog-radio.html',
'ProMMARadio' => '/news/prommaradio.html',
),
'Mobile' => array(
'Macrumors' => '/news/macrumors.html',
'Android Police' => '/news/android-police.html',
'GSM Arena' => '/news/gsm-arena.html',
'DigiTrend Mobile' => '/news/digitrend-mobile.html',
'Mobile Nation' => '/news/mobile-nation.html',
'TechRadar' => '/news/techradar.html',
'ZDNET Mobile' => '/news/zdnet-mobile.html',
'MacWorld' => '/news/macworld.html',
'Android Dev Blog' => '/news/android-dev-blog.html',
),
'News' => array(
'Daily Mail' => '/news/daily-mail.html',
'Business Insider' => '/news/business-insider.html',
'The Guardian' => '/news/the-guardian.html',
'Fox' => '/news/fox.html',
'BBC World' => '/news/bbc-world.html',
'MSNBC' => '/news/msnbc.html',
'ABC News' => '/news/abc-news.html',
'Al Jazeera' => '/news/al-jazeera.html',
'Business Insider India' => '/news/business-insider-india.html',
'Observer' => '/news/observer.html',
'NYT Tech' => '/news/nyt-tech.html',
'NYT World' => '/news/nyt-world.html',
'CNN' => '/news/cnn.html',
'Japan Times' => '/news/japan-times.html',
'WorldCrunch' => '/news/worldcrunch.html',
'Pro publica' => '/news/pro-publica.html',
'OZY' => '/news/ozy.html',
'Times of India' => '/news/times-of-india.html',
'The Australian' => '/news/the-australian.html',
'Harpers' => '/news/harpers.html',
'Moscow Times' => '/news/moscow-times.html',
'The Times' => '/news/the-times.html',
'Reuters Tech' => '/news/reuters-tech.html',
),
'Politics' => array(
'FreeRepublic' => '/news/freerepublic.html',
'Salon' => '/news/salon.html',
'DrudgeReport' => '/news/drudgereport.html',
'TheHill' => '/news/thehill.html',
'TheBlaze' => '/news/theblaze.html',
'InfoWars' => '/news/infowars.html',
'New Republic' => '/news/new-republic.html',
'WashTimes' => '/news/washtimes.html',
'RealCleanPol' => '/news/realcleanpol.html',
'Fact Check' => '/news/fact-check.html',
'DailyKos' => '/news/dailykos.html',
'NewsMax' => '/news/newsmax.html',
'Politico' => '/news/politico.html',
'Michelle Malkin' => '/news/michelle-malkin.html',
),
'Reddit' => array(
'R Movies' => '/news/r-movies.html',
'R News' => '/news/r-news.html',
'Futurology' => '/news/futurology.html',
'R All' => '/news/r-all.html',
'R Music' => '/news/r-music.html',
'R Askscience' => '/news/r-askscience.html',
'R Technology' => '/news/r-technology.html',
'R Bestof' => '/news/r-bestof.html',
'R Askreddit' => '/news/r-askreddit.html',
'R Worldnews' => '/news/r-worldnews.html',
'R Explainlikeimfive' => '/news/r-explainlikeimfive.html',
'R Iama' => '/news/r-iama.html',
),
'Science' => array(
'PhysOrg' => '/news/physorg.html',
'Hack-a-day' => '/news/hack-a-day.html',
'Reddit Science' => '/news/reddit-science.html',
'Stats Blog' => '/news/stats-blog.html',
'Flowing Data' => '/news/flowing-data.html',
'Eureka Alert' => '/news/eureka-alert.html',
'Robotics BizRev' => '/news/robotics-bizrev.html',
'Planet big Data' => '/news/planet-big-data.html',
'Makezine' => '/news/makezine.html',
'MIT Tech' => '/news/mit-tech.html',
'R Bloggers' => '/news/r-bloggers.html',
'DataIsBeautiful' => '/news/dataisbeautiful.html',
'Ted Videos' => '/news/ted-videos.html',
'Advanced Science' => '/news/advanced-science.html',
'Robotiq' => '/news/robotiq.html',
'Science Daily' => '/news/science-daily.html',
'IEEE Robotics' => '/news/ieee-robotics.html',
'PSFK' => '/news/psfk.html',
'Discover Magazine' => '/news/discover-magazine.html',
'DataTau' => '/news/datatau.html',
'RoboHub' => '/news/robohub.html',
'Discovery' => '/news/discovery.html',
'Smart Data' => '/news/smart-data.html',
'Whats Big Data' => '/news/whats-big-data.html',
),
'Tech' => array(
'Hacker News' => '/news/hacker-news.html',
'The Verge' => '/news/the-verge.html',
'Lifehacker' => '/news/lifehacker.html',
'Fast Company' => '/news/fast-company.html',
'ArsTechnica' => '/news/arstechnica.html',
'MakeUseOf' => '/news/makeuseof.html',
'FastCoExist' => '/news/fastcoexist.html',
'How to Geek' => '/news/how-to-geek.html',
'The Next Web' => '/news/the-next-web.html',
'Engadget' => '/news/engadget.html',
'Gizmag' => '/news/gizmag.html',
'QZ' => '/news/qz.html',
'Wired' => '/news/wired.html',
'Techcrunch' => '/news/techcrunch.html',
'Slashdot' => '/news/slashdot.html',
'Extreme Tech' => '/news/extreme-tech.html',
'AnandTech' => '/news/anandtech.html',
'Digital Trends' => '/news/digital-trends.html',
'Next Big Future' => '/news/next-big-future.html',
'Apple Insider' => '/news/apple-insider.html',
'Geek' => '/news/geek.html',
'BBC Technology' => '/news/bbc-technology.html',
'Bit-Tech' => '/news/bit-tech.html',
'Packet Storm Sec' => '/news/packet-storm-sec.html',
'Design' => '/news/design.html',
'High Scalability' => '/news/high-scalability.html',
'Smashing Mag' => '/news/smashing-mag.html',
'The Tech Block' => '/news/the-tech-block.html',
'A VC' => '/news/a-vc.html',
'Tech in Asia' => '/news/tech-in-asia.html',
'ReadWriteWeb' => '/news/readwriteweb.html',
'PC Mag' => '/news/pc-mag.html',
'Continuations' => '/news/continuations.html',
'Copyblogger' => '/news/copyblogger.html',
'Cult of Mac' => '/news/cult-of-mac.html',
'BetaBeat' => '/news/betabeat.html',
'MedGadget' => '/news/medgadget.html',
'SecuriTeam' => '/news/securiteam.html',
'Venture Beat' => '/news/venture-beat.html',
),
'Trend' => array(
'Trend Hunter' => '/news/trend-hunter.html',
'ApartmentT' => '/news/apartmentt.html',
'GQ' => '/news/gq.html',
'Digital Trends' => '/news/digital-trends.html',
'Cool Hunting' => '/news/cool-hunting.html',
'FastCoDesign' => '/news/fastcodesign.html',
'TC Startups' => '/news/tc-startups.html',
'Killer Startups' => '/news/killer-startups.html',
'DigiInfo' => '/news/digiinfo.html',
'New Startups' => '/news/new-startups.html',
'DigiTrends' => '/news/digitrends.html',
),
'Watches' => array(
'Hodinkee' => '/news/hodinkee.html',
'Quill and Pad' => '/news/quill-and-pad.html',
'Monochrome' => '/news/monochrome.html',
'Deployant' => '/news/deployant.html',
'Watches by SJX' => '/news/watches-by-sjx.html',
'Fratello Watches' => '/news/fratello-watches.html',
'A Blog to Watch' => '/news/a-blog-to-watch.html',
'Wound for Life' => '/news/wound-for-life.html',
'Watch Paper' => '/news/watch-paper.html',
'Watch Report' => '/news/watch-report.html',
'Perpetuelle' => '/news/perpetuelle.html',
),
'Youtube' => array(
'LinusTechTips' => '/news/linustechtips.html',
'MetalJesusRocks' => '/news/metaljesusrocks.html',
'TotalBiscuit' => '/news/totalbiscuit.html',
'DexBonus' => '/news/dexbonus.html',
'Lon Siedman' => '/news/lon-siedman.html',
'MKBHD' => '/news/mkbhd.html',
'Terry A Davis' => '/news/terry-a-davis.html',
'HappyConsole' => '/news/happyconsole.html',
'Austin Evans' => '/news/austin-evans.html',
'NCIX' => '/news/ncix.html',
),
)
),
),
self::CONTEXT_CUSTOM => array(
'config' => array(
'name' => 'Configuration',
'type' => 'text',
'required' => true,
'title' => 'Enter feed numbers from Skimfeed!',
'exampleValue' => '5,8,2,l,p,9,23'
)
),
'global' => array(
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'title' => 'Limits the number of returned items in the feed',
'exampleValue' => 10
)
)
);
public function getURI() {
switch($this->queriedContext) {
case self::CONTEXT_NEWS_BOX:
$channel = $this->getInput('box_channel');
if($channel) {
return static::URI . $channel;
}
break;
case self::CONTEXT_HOT_TOPICS:
return static::URI;
case self::CONTEXT_TECH_NEWS:
$channel = $this->getInput('tech_channel');
if($channel) {
return static::URI . $channel;
}
break;
case self::CONTEXT_CUSTOM:
$config = $this->getInput('config');
return static::URI . '/custom.php?f=' . urlencode($config);
}
return parent::getURI();
}
public function getName() {
switch($this->queriedContext) {
case self::CONTEXT_NEWS_BOX:
$channel = $this->getInput('box_channel');
$title = array_search(
$channel,
static::PARAMETERS[self::CONTEXT_NEWS_BOX]['box_channel']['values']
);
return $title . ' - ' . static::NAME;
case self::CONTEXT_HOT_TOPICS:
return 'Hot topics - ' . static::NAME;
case self::CONTEXT_TECH_NEWS:
$channel = $this->getInput('tech_channel');
$titles = array();
foreach(static::PARAMETERS[self::CONTEXT_TECH_NEWS]['tech_channel']['values'] as $ch) {
$titles = array_merge($titles, $ch);
}
$title = array_search($channel, $titles);
return $title . ' - ' . static::NAME;
case self::CONTEXT_CUSTOM:
return 'Custom - ' . static::NAME;
}
return parent::getName();
}
public function collectData() {
// enable to export parameter lists
// $this->exportBoxChannels(); die;
// $this->exportTechChannels(); die;
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Request to ' . $this->getURI() . ' failed!');
defaultLinkTo($html, static::URI);
switch($this->queriedContext) {
case self::CONTEXT_NEWS_BOX:
$author = array_search(
$this->getInput('box_channel'),
static::PARAMETERS[self::CONTEXT_NEWS_BOX]['box_channel']['values']
);
$author = '<a href="'
. $this->getURI()
. '">'
. $author
. '</a>';
$this->extractFeed($html, $author);
break;
case self::CONTEXT_HOT_TOPICS:
$this->extractHotTopics($html);
break;
case self::CONTEXT_TECH_NEWS:
$authors = array();
foreach(static::PARAMETERS[self::CONTEXT_TECH_NEWS]['tech_channel']['values'] as $ch) {
$authors = array_merge($authors, $ch);
}
$author = '<a href="'
. $this->getURI()
. '">'
. array_search($this->getInput('tech_channel'), $authors)
. '</a>';
$this->extractFeed($html, $author);
break;
case self::CONTEXT_CUSTOM:
$this->extractCustomFeed($html);
break;
}
}
private function extractFeed($html, $author) {
$articles = $html->find('li')
or returnServerError('Could not find articles!');
if(count($articles) === 1
&& stristr($articles[0]->plaintext, 'Nothing new in the last 48 hours')) {
return; // Nothing to show
}
$limit = $this->getInput('limit') ?: -1;
foreach($articles as $article) {
$anchor = $article->find('a', 0)
or returnServerError('Could not find anchor!');
$item = array();
$item['uri'] = $this->getTarget($anchor);
$item['title'] = trim($anchor->plaintext);
// The timestamp is encoded as relative time (max. the last 48 hours)
// like this: "- 7 hours". It should always be at the end of the article:
$age = substr($article->plaintext, strrpos($article->plaintext, '-'));
$item['timestamp'] = strtotime($age);
$item['author'] = $author;
$this->items[] = $item;
if($limit > 0 && count($this->items) >= $limit) {
return;
}
}
}
private function extractHotTopics($html) {
$topics = $html->find('#popbox ul li')
or returnServerError('Could not find topics!');
$limit = $this->getInput('limit') ?: -1;
foreach($topics as $topic) {
$anchor = $topic->find('a', 0)
or returnServerError('Could not find anchor!');
$item = array();
$item['uri'] = $this->getTarget($anchor);
$item['title'] = $anchor->title;
$this->items[] = $item;
if($limit > 0 && count($this->items) >= $limit) {
return;
}
}
}
private function extractCustomFeed($html) {
$boxes = $html->find('#boxx .boxes')
or returnServerError('Could not find boxes!');
foreach($boxes as $box) {
$anchor = $box->find('span.boxtitles a', 0)
or returnServerError('Could not find box anchor!');
$author = '<a href="' . $anchor->href . '">' . trim($anchor->plaintext) . '</a>';
$uri = $anchor->href;
$box_html = getSimpleHTMLDOM($uri)
or returnServerError('Could not load custom feed!');
$this->extractFeed($box_html, $author);
}
}
private function getTarget($anchor) {
// Anchors are linked to Skimfeed, luckily the target URI is encoded
// in that URI via '&u=<URI>':
$query = parse_url($anchor->href, PHP_URL_QUERY);
foreach(explode('&', $query) as $parameter) {
list($key, $value) = explode('=', $parameter);
if($key !== 'u') {
continue;
}
return urldecode($value);
}
}
/**
* dev-mode!
* Requires '&format=Html'
*
* Returns the 'box' array from the source site
*/
private function exportBoxChannels() {
$html = getSimpleHTMLDOMCached(static::URI)
or returnServerError('No contents received from Skimfeed!');
if(!$this->isCompatible($html)) {
returnServerError('Skimfeed version is not compatible!');
}
$boxes = $html->find('#boxx .boxes')
or returnServerError('Could not find boxes!');
// begin of 'channel' list
$message = <<<EOD
'box_channel' => array(
'name' => 'Channel',
'type' => 'list',
'required' => true,
'title' => 'Select your channel',
'values' => array(
EOD;
foreach($boxes as $box) {
$anchor = $box->find('span.boxtitles a', 0)
or returnServerError('Could not find box anchor!');
$title = trim($anchor->plaintext);
$uri = $anchor->href;
// add value
$message .= "\t\t'{$title}' => '{$uri}', \n";
}
// end of 'box' list
$message .= <<<EOD
)
),
EOD;
echo <<<EOD
<!DOCTYPE html>
<html>
<body>
<code style="white-space: pre-wrap;">{$message}</code>
</body>
</html>
EOD;
}
/**
* dev-mode!
* Requires '&format=Html'
*
* Returns the 'techs' array from the source site
*/
private function exportTechChannels() {
$html = getSimpleHTMLDOMCached(static::URI)
or returnServerError('No contents received from Skimfeed!');
if(!$this->isCompatible($html)) {
returnServerError('Skimfeed version is not compatible!');
}
$channels = $html->find('#menubar a')
or returnServerError('Could not find channels!');
// begin of 'tech_channel' list
$message = <<<EOD
'tech_channel' => array(
'name' => 'Tech channel',
'type' => 'list',
'required' => true,
'title' => 'Select your tech channel',
'values' => array(
EOD;
foreach($channels as $channel) {
if($channel->href === '#'
|| $channel->class === 'homelink'
|| $channel->plaintext === 'Twitter'
|| $channel->plaintext === 'Weather'
|| $channel->plaintext === '+Custom') {
continue;
}
$title = trim($channel->plaintext);
$uri = '/' . $channel->href;
$message .= "\t\t'{$title}' => array(\n";
$channel_html = getSimpleHTMLDOMCached(static::URI . $uri)
or returnServerError('Could not load tech channel ' . $channel->plaintext . '!');
$boxes = $channel_html->find('#boxx .boxes')
or returnServerError('Could not find boxes!');
foreach($boxes as $box) {
$anchor = $box->find('span.boxtitles a', 0)
or returnServerError('Could not find box anchor!');
$boxtitle = trim($anchor->plaintext);
$boxuri = $anchor->href;
$message .= "\t\t\t'{$boxtitle}' => '{$boxuri}', \n";
}
$message .= "\t\t),\n";
}
// end of 'box' list
$message .= <<<EOD
)
),
EOD;
echo <<<EOD
<!DOCTYPE html>
<html>
<body>
<code style="white-space: pre-wrap;">{$message}</code>
</body>
</html>
EOD;
}
/**
* Checks if the reported skimfeed version is compatible
*/
private function isCompatible($html) {
$title = $html->find('title', 0);
if(!$title) {
return false;
}
if($title->plaintext === 'Skimfeed V5.5 - Tech News') {
return true;
}
return false;
}
}

View File

@@ -14,7 +14,7 @@ class SoundCloudBridge extends BridgeAbstract {
)
));
const CLIENT_ID = '0aca19eae3843844e4053c6d8fdb7875';
const CLIENT_ID = '4jkoEFmZEDaqjwJ9Eih4ATNhcH3vMVfp';
public function collectData(){

View File

@@ -2,9 +2,9 @@
class SteamBridge extends BridgeAbstract {
const NAME = 'Steam Bridge';
const URI = 'https://steamcommunity.com/';
const URI = 'https://store.steampowered.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns games list';
const DESCRIPTION = 'Returns apps list';
const MAINTAINER = 'jacknumber';
const PARAMETERS = array(
'Wishlist' => array(
@@ -47,16 +47,6 @@ class SteamBridge extends BridgeAbstract {
'AED' => 'ae',
),
),
'sort' => array(
'name' => 'Sort by',
'type' => 'list',
'values' => array(
'Rank' => 'rank',
'Date Added' => 'added',
'Name' => 'name',
'Price' => 'price',
)
),
'only_discount' => array(
'name' => 'Only discount',
'type' => 'checkbox',
@@ -68,62 +58,100 @@ class SteamBridge extends BridgeAbstract {
$username = $this->getInput('username');
$params = array(
'sort' => $this->getInput('sort'),
'cc' => $this->getInput('currency')
);
$url = self::URI . 'id/' . $username . '/wishlist?' . http_build_query($params);
$url = self::URI . 'wishlist/id/' . $username . '?' . http_build_query($params);
$targetVariable = 'g_rgAppInfo';
$sort = array();
$html = '';
$html = getSimpleHTMLDOM($url)
or returnServerError("Could not request Steam Wishlist. Tried:\n - $url");
foreach($html->find('#wishlist_items .wishlistRow') as $element) {
$jsContent = $html->find('.responsive_page_template_content script', 0)->innertext;
$gameTitle = $element->find('h4', 0)->plaintext;
$gameUri = $element->find('.storepage_btn_ctn a', 0)->href;
$gameImg = $element->find('.gameListRowLogo img', 0)->src;
if(preg_match('/var ' . $targetVariable . ' = (.*?);/s', $jsContent, $matches)) {
$appsData = json_decode($matches[1]);
} else {
returnServerError("Could not parse JS variable ($targetVariable) in page content.");
}
$discountBlock = $element->find('.discount_block', 0);
foreach($appsData as $id => $element) {
$appType = $element->type;
$appIsBuyable = 0;
$appHasDiscount = 0;
$appIsFree = 0;
if($element->subs) {
$appIsBuyable = 1;
if($element->subs[0]->discount_pct) {
$appHasDiscount = 1;
$discountBlock = str_get_html($element->subs[0]->discount_block);
$appDiscountValue = $discountBlock->find('.discount_pct', 0)->plaintext;
$appOldPrice = $discountBlock->find('.discount_original_price', 0)->plaintext;
$appNewPrice = $discountBlock->find('.discount_final_price', 0)->plaintext;
$appPrice = $appNewPrice;
} else {
if($this->getInput('only_discount')) {
continue;
}
$appPrice = $element->subs[0]->price / 100;
}
if($element->find('.discount_block', 0)) {
$gameHasPromo = 1;
} else {
if($this->getInput('only_discount')) {
continue;
}
$gameHasPromo = 0;
}
if($gameHasPromo) {
$gamePromoValue = $discountBlock->find('.discount_pct', 0)->plaintext;
$gameOldPrice = $discountBlock->find('.discount_original_price', 0)->plaintext;
$gameNewPrice = $discountBlock->find('.discount_final_price', 0)->plaintext;
$gamePrice = $gameNewPrice;
} else {
$gamePrice = $element->find('.gameListPriceData .price', 0)->plaintext;
if(isset($element->free) && $element->free = 1) {
$appIsFree = 1;
}
}
$item = array();
$item['uri'] = $gameUri;
$item['title'] = $gameTitle;
$item['price'] = $gamePrice;
$item['hasPromo'] = $gameHasPromo;
$item['uri'] = "http://store.steampowered.com/app/$id/";
$item['title'] = $element->name;
$item['type'] = $appType;
$item['cover'] = str_replace('_292x136', '', $element->capsule);
$item['timestamp'] = $element->added;
$item['isBuyable'] = $appIsBuyable;
$item['hasDiscount'] = $appHasDiscount;
$item['isFree'] = $appIsFree;
$item['priority'] = $element->priority;
if($gameHasPromo) {
if($appIsBuyable) {
$item['price'] = floatval(str_replace(',', '.', $appPrice));
}
$item['promoValue'] = $gamePromoValue;
$item['oldPrice'] = $gameOldPrice;
$item['newPrice'] = $gameNewPrice;
if($appHasDiscount) {
$item['discount']['value'] = $appDiscountValue;
$item['discount']['oldPrice'] = floatval(str_replace(',', '.', $appOldPrice));
$item['discount']['newPrice'] = floatval(str_replace(',', '.', $appNewPrice));
}
$item['enclosures'] = array();
$item['enclosures'][] = str_replace('_292x136', '', $element->capsule);
foreach($element->screenshots as $screenshot) {
$item['enclosures'][] = substr($element->capsule, 0, -31) . $screenshot;
}
$sort[$id] = $element->priority;
$this->items[] = $item;
}
array_multisort($sort, SORT_ASC, $this->items);
}
}

57
bridges/SupInfoBridge.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
class SupInfoBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'SupInfoBridge';
const URI = 'https://www.supinfo.com';
const DESCRIPTION = 'Returns the newest articles.';
const PARAMETERS = array(array(
'tag' => array(
'name' => 'Category (not mandatory)',
'type' => 'text',
)
));
public function collectData() {
if(empty($this->getInput('tag'))) {
$html = getSimpleHTMLDOM(self::URI . '/articles/')
or returnServerError('Unable to fetch articles !');
} else {
$html = getSimpleHTMLDOM(self::URI . '/articles/tag/' . $this->getInput('tag'))
or returnServerError('Unable to fetch articles !');
}
$content = $html->find('#latest', 0)->find('ul[class=courseContent]', 0);
for($i = 0; $i < 5; $i++) {
$this->items[] = $this->fetchArticle($content->find('h4', $i)->find('a', 0)->href);
}
}
public function fetchArticle($link) {
$articleHTML = getSimpleHTMLDOM(self::URI . $link)
or returnServerError('Unable to fetch article !');
$article = $articleHTML->find('div[id=courseDocZero]', 0);
$item = array();
$item['author'] = $article->find('#courseMetas', 0)->find('a', 0)->plaintext;
$item['id'] = $link;
$item['uri'] = self::URI . $link;
$item['title'] = $article->find('h1', 0)->plaintext;
$date = explode(' ', $article->find('#courseMetas', 0)->find('span', 1)->plaintext);
$item['timestamp'] = DateTime::createFromFormat('d/m/Y H:i:s', $date[2] . ' ' . $date[4])->getTimestamp();
$article->find('div[id=courseHeader]', 0)->innertext = '';
$article->find('div[id=author-infos]', 0)->innertext = '';
$article->find('div[id=cartouche-tete]', 0)->innertext = '';
$item['content'] = $article;
return $item;
}
}

View File

@@ -0,0 +1,45 @@
<?php
class SuperSmashBlogBridge extends BridgeAbstract {
const MAINTAINER = 'corenting';
const NAME = 'Super Smash Blog';
const URI = 'https://www.smashbros.com/en_US/blog/index.html';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Latest articles from the Super Smash Blog blog';
public function collectData(){
$dlUrl = 'https://www.smashbros.com/data/bs/en_US/json/en_US.json';
$jsonString = getContents($dlUrl) or returnServerError('Error while downloading the website content');
$json = json_decode($jsonString, true);
foreach($json as $article) {
// Build content
$picture = $article['acf']['image1']['url'];
if (strlen($picture) != 0) {
$picture = str_get_html('<img src="https://www.smashbros.com/' . substr($picture, 8) . '"/>');
} else {
$picture = '';
}
$video = $article['acf']['link_url'];
if (strlen($video) != 0) {
$video = str_get_html('<a href="' . $video .'">Youtube video</a>');
} else {
$video = '';
}
$text = str_get_html($article['acf']['editor']);
$content = $picture . $video . $text;
// Build final item
$item = array();
$item['title'] = $article['title']['rendered'];
$item['timestamp'] = strtotime($article['date']);
$item['content'] = $content;
$item['uri'] = self::URI . '?post=' . $article['id'];
$this->items[] = $item;
}
}
}

View File

@@ -1,96 +0,0 @@
<?php
class T411Bridge extends BridgeAbstract {
const MAINTAINER = 'ORelio';
const NAME = 'T411 Bridge';
const URI = 'https://www.t411.al/';
const DESCRIPTION = 'Returns the 10 newest torrents with specified search
terms <br /> Use url part after "?" mark when using their search engine.';
const PARAMETERS = array( array(
'search' => array(
'name' => 'Search criteria',
'required' => true
)
));
public function collectData(){
//Utility function for retrieving text based on start and end delimiters
function extractFromDelimiters($string, $start, $end){
if(strpos($string, $start) !== false) {
$section_retrieved = substr($string, strpos($string, $start) + strlen($start));
$section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
return $section_retrieved;
}
return false;
}
//Retrieve torrent listing from search results, which does not contain torrent description
$url = self::URI
. 'torrents/search/?search='
. urlencode($this->getInput('search'))
. '&order=added&type=desc';
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request t411: ' . $url);
$results = $html->find('table.results', 0);
if (is_null($results))
returnServerError('No results from t411: ' . $url);
$limit = 0;
//Process each item individually
foreach($results->find('tr') as $element) {
//Limit total amount of requests and ignore table header
if($limit >= 10) {
break;
}
if(is_object($element->find('th', 0))) {
continue;
}
//Requests are rate-limited
usleep(500000); //So we need to wait (500ms)
//Retrieve data from RSS entry
$item_uri = self::URI
. 'torrents/details/?id='
. extractFromDelimiters($element->find('a.nfo', 0)->outertext, '?id=', '"');
$item_title = extractFromDelimiters($element->outertext, '" title="', '"');
$item_date = strtotime($element->find('dd', 0)->plaintext);
//Retrieve full description from torrent page
$item_html = getSimpleHTMLDOM($item_uri);
if(!$item_html) {
continue;
}
//Retrieve data from page contents
$item_desc = $item_html->find('div.description', 0);
$item_author = $item_html->find('a.profile', 0)->innertext;
//Cleanup advertisments
$divs = explode('<div class="align-center">', $item_desc->innertext);
$item_desc = '';
foreach ($divs as $text)
if (strpos($text, 'adprovider.adlure.net') === false)
$item_desc = $item_desc . '<div class="align-center">' . $text;
$item_desc = preg_replace('/<h2 class="align-center">LIENS DE T..?L..?CHARGEMENT<\/h2>/i', '', $item_desc);
//Build and add final item
$item = array();
$item['uri'] = $item_uri;
$item['title'] = $item_title;
$item['author'] = $item_author;
$item['timestamp'] = $item_date;
$item['content'] = $item_desc;
$this->items[] = $item;
$limit++;
}
}
}

38
bridges/TebeoBridge.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
class TebeoBridge extends FeedExpander {
const NAME = 'Tébéo Bridge';
const URI = 'http://www.tebeo.bzh/';
const CACHE_TIMEOUT = 21600; //6h
const DESCRIPTION = 'Returns the newest Tébéo videos by category';
const MAINTAINER = 'Mitsukarenai';
const PARAMETERS = array( array(
'cat' => array(
'name' => 'Catégorie',
'type' => 'list',
'values' => array(
'Toutes les vidéos' => '/',
'Actualité' => '/14-actualite',
'Sport' => '/3-sport',
'Culture-Loisirs' => '/5-culture-loisirs',
'Société' => '/15-societe',
'Langue Bretonne' => '/9-langue-bretonne'
)
)
));
public function collectData(){
$url = self::URI . '/le-replay/' . $this->getInput('cat');
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request Tébéo.');
foreach($html->find('div[id=items_replay] div.replay') as $element) {
$item = array();
$item['uri'] = $element->find('a', 0)->href;
$item['title'] = $element->find('h3', 0)->plaintext;
$item['timestamp'] = strtotime($element->find('p.moment-format-day', 0)->plaintext);
$item['content'] = '<a href="'.$item['uri'].'"><img alt="" src="'.$element->find('img', 0)->src.'"></a>';
$this->items[] = $item;
}
}
}

View File

@@ -11,7 +11,7 @@ class ThePirateBayBridge extends BridgeAbstract {
const PARAMETERS = array( array(
'q' => array(
'name' => 'keywords, separated by semicolons',
'name' => 'keywords/username/category, separated by semicolons',
'exampleValue' => 'first list;second list;…',
'required' => true
),
@@ -24,9 +24,9 @@ class ThePirateBayBridge extends BridgeAbstract {
'user' => 'usr'
)
),
'cat_check' => array(
'catCheck' => array(
'type' => 'checkbox',
'name' => 'Specify category for normal search ?',
'name' => 'Specify category for keyword search ?',
),
'cat' => array(
'name' => 'Category number',
@@ -94,7 +94,7 @@ class ThePirateBayBridge extends BridgeAbstract {
return $timestamp;
}
$catBool = $this->getInput('cat_check');
$catBool = $this->getInput('catCheck');
if($catBool) {
$catNum = $this->getInput('cat');
}

View File

@@ -1,102 +0,0 @@
<?php
class Torrent9Bridge extends BridgeAbstract {
const MAINTAINER = 'lagaisse';
const NAME = 'Torrent9 Bridge';
const URI = 'http://www.torrent9.biz';
const CACHE_TIMEOUT = 86400; // 24h = 86400s
const DESCRIPTION = 'Returns latest torrents';
const PAGE_SERIES = 'torrents_series';
const PAGE_SERIES_VOSTFR = 'torrents_series_vostfr';
const PAGE_SERIES_FR = 'torrents_series_french';
const PARAMETERS = array(
'From search' => array(
'q' => array(
'name' => 'Search',
'required' => true,
'title' => 'Type your search'
)
),
'By page' => array(
'page' => array(
'name' => 'Page',
'type' => 'list',
'required' => false,
'values' => array(
'Series' => self::PAGE_SERIES,
'Series VOST' => self::PAGE_SERIES_VOSTFR,
'Series FR' => self::PAGE_SERIES_FR,
),
'defaultValue' => self::PAGE_SERIES
)
)
);
public function collectData(){
if($this->queriedContext === 'From search') {
$request = str_replace(' ', '-', trim($this->getInput('q')));
$page = self::URI . '/search_torrent/' . urlencode($request) . '.html';
} else {
$request = $this->getInput('page');
$page = self::URI . '/' . $request . '.html';
}
$html = getSimpleHTMLDOM($page)
or returnServerError('No results for this query.');
foreach($html->find('table', 0)->find('tr') as $episode) {
if($episode->parent->tag == 'tbody') {
$urlepisode = self::URI . $episode->find('a', 0)->getAttribute('href');
//30 years = forever
$htmlepisode = getSimpleHTMLDOMCached($urlepisode, 86400 * 366 * 30);
$item = array();
$item['author'] = $episode->find('a', 0)->text();
$item['title'] = $episode->find('a', 0)->text();
$item['id'] = $episode->find('a', 0)->getAttribute('href');
$item['pubdate'] = $this->getCachedDate($urlepisode);
$textefiche = $htmlepisode->find('.movie-information', 0)->find('p', 1);
if(isset($textefiche)) {
$item['content'] = $textefiche->text();
} else {
$p = $htmlepisode->find('.movie-information', 0)->find('p');
if(!empty($p)) {
$item['content'] = $htmlepisode->find('.movie-information', 0)->find('p', 0)->text();
}
}
$item['id'] = $episode->find('a', 0)->getAttribute('href');
$item['uri'] = self::URI . $htmlepisode->find('.download', 0)->getAttribute('href');
$this->items[] = $item;
}
}
}
public function getName(){
if(!is_null($this->getInput('q'))) {
return $this->getInput('q') . ' : ' . self::NAME;
}
return parent::getName();
}
private function getCachedDate($url){
debugMessage('getting pubdate from url ' . $url . '');
// Initialize cache
$cache = Cache::create('FileCache');
$cache->setPath(CACHE_DIR . '/pages');
$params = [$url];
$cache->setParameters($params);
// Get cachefile timestamp
$time = $cache->getTime();
return ($time !== false ? $time : time());
}
}

View File

@@ -44,6 +44,25 @@ class TwitterBridge extends BridgeAbstract {
'type' => 'checkbox',
'title' => 'Hide retweets'
)
),
'By list' => array(
'user' => array(
'name' => 'User',
'required' => true,
'exampleValue' => 'sebsauvage',
'title' => 'Insert a user name'
),
'list' => array(
'name' => 'List',
'required' => true,
'title' => 'Insert the list name'
),
'filter' => array(
'name' => 'Filter',
'exampleValue' => '#rss-bridge',
'required' => false,
'title' => 'Specify term to search for'
)
)
);
@@ -57,6 +76,8 @@ class TwitterBridge extends BridgeAbstract {
$specific = '@';
$param = 'u';
break;
case 'By list':
return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user');
default: return parent::getName();
}
return 'Twitter ' . $specific . $this->getInput($param);
@@ -74,6 +95,11 @@ class TwitterBridge extends BridgeAbstract {
. urlencode($this->getInput('u'));
// Always return without replies!
// . ($this->getInput('norep') ? '' : '/with_replies');
case 'By list':
return self::URI
. urlencode($this->getInput('user'))
. '/lists/'
. str_replace(' ', '-', strtolower($this->getInput('list')));
default: return parent::getURI();
}
}
@@ -88,6 +114,8 @@ class TwitterBridge extends BridgeAbstract {
returnServerError('No results for this query.');
case 'By username':
returnServerError('Requested username can\'t be found.');
case 'By list':
returnServerError('Requested username or list can\'t be found');
}
}
@@ -132,6 +160,18 @@ class TwitterBridge extends BridgeAbstract {
// generate the title
$item['title'] = strip_tags($this->fixAnchorSpacing($tweet->find('p.js-tweet-text', 0), '<a>'));
switch($this->queriedContext) {
case 'By list':
// Check if filter applies to list (using raw content)
if($this->getInput('filter')) {
if(stripos($tweet->find('p.js-tweet-text', 0)->plaintext, $this->getInput('filter')) === false) {
continue 2; // switch + for-loop!
}
}
break;
default:
}
$this->processContentLinks($tweet);
$this->processEmojis($tweet);

View File

@@ -1,40 +0,0 @@
<?php
class VineBridge extends BridgeAbstract {
const MAINTAINER = 'ckiw';
const NAME = 'Vine bridge';
const URI = 'http://vine.co/';
const DESCRIPTION = 'Returns the latests vines from vine user page';
const PARAMETERS = array( array(
'u' => array(
'name' => 'User id',
'required' => true
)
));
public function collectData(){
$html = '';
$uri = self::URI . '/u/' . $this->getInput('u') . '?mode=list';
$html = getSimpleHTMLDOM($uri)
or returnServerError('No results for this query.');
foreach($html->find('.post') as $element) {
$a = $element->find('a', 0);
$a->href = str_replace('https://', 'http://', $a->href);
$time = strtotime(ltrim($element->find('p', 0)->plaintext, ' Uploaded at '));
$video = $element->find('video', 0);
$video->controls = 'true';
$element->find('h2', 0)->outertext = '';
$item = array();
$item['uri'] = $a->href;
$item['timestamp'] = $time;
$item['title'] = $a->plaintext;
$item['content'] = $element;
$this->items[] = $item;
}
}
}

View File

@@ -1,9 +1,11 @@
<?php
class VkBridge extends BridgeAbstract {
class VkBridge extends BridgeAbstract
{
const MAINTAINER = 'ahiles3005';
const NAME = 'VK.com';
const URI = 'http://vk.com/';
const URI = 'https://vk.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Working with open pages';
const PARAMETERS = array(
@@ -15,52 +17,336 @@ class VkBridge extends BridgeAbstract {
)
);
public function getURI(){
if(!is_null($this->getInput('u'))) {
protected $pageName;
public function getURI()
{
if (!is_null($this->getInput('u'))) {
return static::URI . urlencode($this->getInput('u'));
}
return parent::getURI();
}
public function collectData(){
public function getName()
{
if ($this->pageName) {
return $this->pageName;
}
ini_set('user-agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0');
return parent::getName();
}
$text_html = getContents($this->getURI())
or returnServerError('No results for group or user name "' . $this->getInput('u') . '".');
public function collectData()
{
$text_html = $this->getContents()
or returnServerError('No results for group or user name "' . $this->getInput('u') . '".');
$text_html = iconv('windows-1251', 'utf-8', $text_html);
// makes album link generating work correctly
$text_html = str_replace('"class="page_album_link">', '" class="page_album_link">', $text_html);
$html = str_get_html($text_html);
$pageName = $html->find('.page_name', 0);
if (is_object($pageName)) {
$pageName = $pageName->plaintext;
$this->pageName = htmlspecialchars_decode($pageName);
}
$pinned_post_item = null;
$last_post_id = 0;
foreach($html->find('.post') as $post) {
foreach ($html->find('.post') as $post) {
if(is_object($post->find('a.wall_post_more', 0))) {
$is_pinned_post = false;
if (strpos($post->getAttribute('class'), 'post_fixed') !== false) {
$is_pinned_post = true;
}
if (is_object($post->find('a.wall_post_more', 0))) {
//delete link "show full" in content
$post->find('a.wall_post_more', 0)->outertext = '';
}
$item = array();
$item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<br><img>');
if(is_object($post->find('a.page_media_link_title', 0))) {
$link = $post->find('a.page_media_link_title', 0)->getAttribute('href');
//external link in the post
$item['content'] .= "\n\rExternal link: "
. str_replace('/away.php?to=', '', urldecode($link));
$content_suffix = '';
// looking for external links
$external_link_selectors = array(
'a.page_media_link_title',
'div.page_media_link_title > a',
'div.media_desc > a.lnk',
);
foreach($external_link_selectors as $sel) {
if (is_object($post->find($sel, 0))) {
$a = $post->find($sel, 0);
$innertext = $a->innertext;
$parsed_url = parse_url($a->getAttribute('href'));
if (strpos($parsed_url['path'], '/away.php') !== 0) continue;
parse_str($parsed_url['query'], $parsed_query);
$content_suffix .= "<br>External link: <a href='" . $parsed_query['to'] . "'>$innertext</a>";
}
}
//get video on post
if(is_object($post->find('span.post_video_title_content', 0))) {
$titleVideo = $post->find('span.post_video_title_content', 0)->plaintext;
$linkToVideo = self::URI . $post->find('a.page_post_thumb_video', 0)->getAttribute('href');
$item['content'] .= "\n\r {$titleVideo}: {$linkToVideo}";
// remove external link from content
$external_link_selectors_to_remove = array(
'div.page_media_thumbed_link',
'div.page_media_link_desc_wrap',
'div.media_desc > a.lnk',
);
foreach($external_link_selectors_to_remove as $sel) {
if (is_object($post->find($sel, 0))) {
$post->find($sel, 0)->outertext = '';
}
}
// looking for article
$article = $post->find('a.article_snippet', 0);
if (is_object($article)) {
if (strpos($article->getAttribute('class'), 'article_snippet_mini') !== false) {
$article_title_selector = 'div.article_snippet_mini_title';
$article_author_selector = 'div.article_snippet_mini_info > .mem_link,
div.article_snippet_mini_info > .group_link';
$article_thumb_selector = 'div.article_snippet_mini_thumb';
} else {
$article_title_selector = 'div.article_snippet__title';
$article_author_selector = 'div.article_snippet__author';
$article_thumb_selector = 'div.article_snippet__image';
}
$article_title = $article->find($article_title_selector, 0)->innertext;
$article_author = $article->find($article_author_selector, 0)->innertext;
$article_link = self::URI . ltrim($article->getAttribute('href'), '/');
$article_img_element_style = $article->find($article_thumb_selector, 0)->getAttribute('style');
preg_match('/background-image: url\((.*)\)/', $article_img_element_style, $matches);
if (count($matches) > 0) {
$content_suffix .= "<br><img src='" . $matches[1] . "'>";
}
$content_suffix .= "<br>Article: <a href='$article_link'>$article_title ($article_author)</a>";
$article->outertext = '';
}
// get video on post
$video = $post->find('div.post_video_desc', 0);
if (is_object($video)) {
$video_title = $video->find('div.post_video_title', 0)->plaintext;
$video_link = self::URI . ltrim( $video->find('a.lnk', 0)->getAttribute('href'), '/' );
$content_suffix .= "<br>Video: <a href='$video_link'>$video_title</a>";
$video->outertext = '';
}
// get all other videos
foreach($post->find('a.page_post_thumb_video') as $a) {
$video_title = $a->getAttribute('aria-label');
$temp = explode(' ', $video_title, 2);
if (count($temp) > 1) $video_title = $temp[1];
$video_link = self::URI . ltrim( $a->getAttribute('href'), '/' );
$content_suffix .= "<br>Video: <a href='$video_link'>$video_title</a>";
$a->outertext = '';
}
// get all photos
foreach($post->find('div.wall_text > a.page_post_thumb_wrap') as $a) {
$result = $this->getPhoto($a);
if ($result == null) continue;
$a->outertext = '';
$content_suffix .= "<br>$result";
}
// get albums
foreach($post->find('.page_album_wrap') as $el) {
$a = $el->find('.page_album_link', 0);
$album_title = $a->find('.page_album_title_text', 0)->getAttribute('title');
$album_link = self::URI . ltrim($a->getAttribute('href'), '/');
$el->outertext = '';
$content_suffix .= "<br>Album: <a href='$album_link'>$album_title</a>";
}
// get photo documents
foreach($post->find('a.page_doc_photo_href') as $a) {
$doc_link = self::URI . ltrim($a->getAttribute('href'), '/');
$doc_gif_label_element = $a->find('.page_gif_label', 0);
$doc_title_element = $a->find('.doc_label', 0);
if (is_object($doc_gif_label_element)) {
$gif_preview_img = backgroundToImg($a->find('.page_doc_photo', 0));
$content_suffix .= "<br>Gif: <a href='$doc_link'>$gif_preview_img</a>";
} else if (is_object($doc_title_element)) {
$doc_title = $doc_title_element->innertext;
$content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>";
} else {
continue;
}
$a->outertext = '';
}
// get other documents
foreach($post->find('div.page_doc_row') as $div) {
$doc_title_element = $div->find('a.page_doc_title', 0);
if (is_object($doc_title_element)) {
$doc_title = $doc_title_element->innertext;
$doc_link = self::URI . ltrim($doc_title_element->getAttribute('href'), '/');
$content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>";
} else {
continue;
}
$div->outertext = '';
}
// get polls
foreach($post->find('div.page_media_poll_wrap') as $div) {
$poll_title = $div->find('.page_media_poll_title', 0)->innertext;
$content_suffix .= "<br>Poll: $poll_title";
foreach($div->find('div.page_poll_text') as $poll_stat_title) {
$content_suffix .= '<br>- ' . $poll_stat_title->innertext;
}
$div->outertext = '';
}
// get sign
$post_author = $pageName;
foreach($post->find('a.wall_signed_by') as $a) {
$post_author = $a->innertext;
$a->outertext = '';
}
if (is_object($post->find('div.copy_quote', 0))) {
$copy_quote = $post->find('div.copy_quote', 0);
if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) {
$copy_post_header->outertext = '';
}
$copy_quote_content = $copy_quote->innertext;
$copy_quote->outertext = "<br>Reposted: <br>$copy_quote_content";
}
$item = array();
$item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<br><img>');
$item['content'] .= $content_suffix;
$item['categories'] = array();
// get post hashtags
foreach($post->find('a') as $a) {
$href = $a->getAttribute('href');
$prefix = '/feed?section=search&q=%23';
$innertext = $a->innertext;
if ($href && substr($href, 0, strlen($prefix)) === $prefix) {
$item['categories'][] = urldecode(substr($href, strlen($prefix)));
} else if (substr($innertext, 0, 1) == '#') {
$item['categories'][] = $innertext;
}
}
// get post link
$item['uri'] = self::URI . $post->find('a.post_link', 0)->getAttribute('href');
$item['date'] = $post->find('span.rel_date', 0)->plaintext;
$this->items[] = $item;
// var_dump($item['date']);
$post_link = $post->find('a.post_link', 0)->getAttribute('href');
preg_match('/wall-?\d+_(\d+)/', $post_link, $preg_match_result);
$item['post_id'] = intval($preg_match_result[1]);
if (substr(self::URI, -1) == '/') {
$post_link = self::URI . ltrim($post_link, '/');
} else {
$post_link = self::URI . $post_link;
}
$item['uri'] = $post_link;
$item['timestamp'] = $this->getTime($post);
$item['title'] = $this->getTitle($item['content']);
$item['author'] = $post_author;
if ($is_pinned_post) {
// do not append it now
$pinned_post_item = $item;
} else {
$last_post_id = $item['post_id'];
$this->items[] = $item;
}
}
if (is_null($pinned_post_item)) {
return;
} else if (count($this->items) == 0) {
$this->items[] = $pinned_post_item;
} else if ($last_post_id < $pinned_post_item['post_id']) {
$this->items[] = $pinned_post_item;
usort($this->items, function ($item1, $item2) {
return $item2['post_id'] - $item1['post_id'];
});
}
}
private function getPhoto($a) {
$onclick = $a->getAttribute('onclick');
preg_match('/return showPhoto\(.+?({.*})/', $onclick, $preg_match_result);
if (count($preg_match_result) == 0) return;
$arg = htmlspecialchars_decode( str_replace('queue:1', '"queue":1', $preg_match_result[1]) );
$data = json_decode($arg, true);
if ($data == null) return;
$thumb = $data['temp']['base'] . $data['temp']['x_'][0] . '.jpg';
$original = '';
foreach(array('y_', 'z_', 'w_') as $key) {
if (!isset($data['temp'][$key])) continue;
if (!isset($data['temp'][$key][0])) continue;
if (substr($data['temp'][$key][0], 0, 4) == 'http') {
$base = '';
} else {
$base = $data['temp']['base'];
}
$original = $base . $data['temp'][$key][0] . '.jpg';
}
if ($original) {
return "<a href='$original'><img src='$thumb'></a>";
} else {
return "<img src='$thumb'>";
}
}
private function getTitle($content)
{
preg_match('/^["\w\ \p{Cyrillic}\(\)\?#«»-]+/mu', htmlspecialchars_decode($content), $result);
if (count($result) == 0) return 'untitled';
return $result[0];
}
private function getTime($post)
{
if ($time = $post->find('span.rel_date', 0)->getAttribute('time')) {
return $time;
} else {
$strdate = $post->find('span.rel_date', 0)->plaintext;
$date = date_parse($strdate);
if (!$date['year']) {
if (strstr($strdate, 'today') !== false) {
$strdate = date('d-m-Y') . ' ' . $strdate;
} elseif (strstr($strdate, 'yesterday ') !== false) {
$time = time() - 60 * 60 * 24;
$strdate = date('d-m-Y', $time) . ' ' . $strdate;
} else {
$strdate = $strdate . ' ' . date('Y');
}
$date = date_parse($strdate);
}
return strtotime($date['day'] . '-' . $date['month'] . '-' . $date['year'] . ' ' .
$date['hour'] . ':' . $date['minute']);
}
}
public function getContents()
{
ini_set('user-agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0');
$header = array('Accept-language: en', 'Cookie: remixlang=3');
return getContents($this->getURI(), $header);
}
}

View File

@@ -18,10 +18,10 @@ class WhydBridge extends BridgeAbstract {
public function collectData(){
$html = '';
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 ?
$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.');
} else { // input may be the username
$html = getSimpleHTMLDOM(

View File

@@ -1,16 +1,12 @@
<?php
class WorldOfTanksBridge extends BridgeAbstract {
class WorldOfTanksBridge extends FeedExpander {
const MAINTAINER = 'mitsukarenai';
const MAINTAINER = 'Riduidel';
const NAME = 'World of Tanks';
const URI = 'http://worldoftanks.eu/';
const DESCRIPTION = 'News about the tank slaughter game.';
const PARAMETERS = array( array(
'category' => array(
// TODO: should be a list
'name' => 'nom de la catégorie'
),
'lang' => array(
'name' => 'Langue',
'type' => 'list',
@@ -26,47 +22,31 @@ class WorldOfTanksBridge extends BridgeAbstract {
)
));
private $title = '';
public function collectData() {
$this->collectExpandableDatas(sprintf('https://worldoftanks.eu/%s/rss/news/', $this->getInput('lang')));
}
public function getURI(){
if(!is_null($this->getInput('lang'))) {
$lang = $this->getInput('lang');
$uri = self::URI . $lang . '/news/';
if(!empty($this->getInput('category'))) {
$uri .= 'pc-browser/' . $this->getInput('category') . '/';
}
return $uri;
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
$item['content'] = $this->loadFullArticle($item['uri']);
return $item;
}
/**
* Loads the full article and returns the contents
* @param $uri The article URI
* @return The article content
*/
private function loadFullArticle($uri){
$html = getSimpleHTMLDOMCached($uri);
$content = $html->find('article', 0);
// Remove the scripts, please
foreach($content->find('script') as $script) {
$script->outertext = '';
}
return parent::getURI();
}
public function getName(){
return $this->title ?: parent::getName();
}
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request ' . $this->getURI());
debugMessage("loaded HTML from " . $this->getURI());
// customize name
$this->title = $html->find('title', 0)->innertext;
foreach($html->find('.b-imgblock_ico') as $infoLink) {
$this->parseLine($infoLink);
}
}
private function parseLine($infoLink){
$item = array();
$item['uri'] = self::URI . $infoLink->href;
// now load that uri from cache
debugMessage('loading page ' . $item['uri']);
$articlePage = getSimpleHTMLDOMCached($item['uri']);
$content = $articlePage->find('.l-content', 0);
defaultLinkTo($content, self::URI);
$item['title'] = $content->find('h1', 0)->innertext;
$item['content'] = $content->find('.b-content', 0)->innertext;
$item['timestamp'] = $content->find('.b-statistic_time', 0)->getAttribute("data-timestamp");
$this->items[] = $item;
return $content->innertext;
}
}

View File

@@ -0,0 +1,143 @@
<?php
/* This is a mashup of FlickrExploreBridge by sebsauvage and FlickrTagBridge
* by erwang.providing the functionality of both in one.
*/
class YGGTorrentBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'Yggtorrent Bridge';
const URI = 'https://yggtorrent.is';
const DESCRIPTION = 'Returns torrent search from Yggtorrent';
const PARAMETERS = array(
array(
'cat' => array(
'name' => 'category',
'type' => 'list',
'values' => array(
'Toute les catégories' => 'all.all',
'Film/Vidéo - Toutes les sous-catégories' => '2145.all',
'Film/Vidéo - Animation' => '2145.2178',
'Film/Vidéo - Animation Série' => '2145.2179',
'Film/Vidéo - Concert' => '2145.2180',
'Film/Vidéo - Documentaire' => '2145.2181',
'Film/Vidéo - Émission TV' => '2145.2182',
'Film/Vidéo - Film' => '2145.2183',
'Film/Vidéo - Série TV' => '2145.2184',
'Film/Vidéo - Spectacle' => '2145.2185',
'Film/Vidéo - Sport' => '2145.2186',
'Film/Vidéo - Vidéo-clips' => '2145.2186',
'Audio - Toutes les sous-catégories' => '2139.all',
'Audio - Karaoké' => '2139.2147',
'Audio - Musique' => '2139.2148',
'Audio - Podcast Radio' => '2139.2150',
'Audio - Samples' => '2139.2149',
'Jeu vidéo - Toutes les sous-catégories' => '2142.all',
'Jeu vidéo - Autre' => '2142.2167',
'Jeu vidéo - Linux' => '2142.2159',
'Jeu vidéo - MacOS' => '2142.2160',
'Jeu vidéo - Microsoft' => '2142.2162',
'Jeu vidéo - Nintendo' => '2142.2163',
'Jeu vidéo - Smartphone' => '2142.2165',
'Jeu vidéo - Sony' => '2142.2164',
'Jeu vidéo - Tablette' => '2142.2166',
'Jeu vidéo - Windows' => '2142.2161',
'eBook - Toutes les sous-catégories' => '2140.all',
'eBook - Audio' => '2140.2151',
'eBook - Bds' => '2140.2152',
'eBook - Comics' => '2140.2153',
'eBook - Livres' => '2140.2154',
'eBook - Mangas' => '2140.2155',
'eBook - Presse' => '2140.2156',
'Emulation - Toutes les sous-catégories' => '2141.all',
'Emulation - Emulateurs' => '2141.2157',
'Emulation - Roms' => '2141.2158',
'GPS - Toutes les sous-catégories' => '2141.all',
'GPS - Applications' => '2141.2168',
'GPS - Cartes' => '2141.2169',
'GPS - Divers' => '2141.2170'
)
),
'nom' => array(
'name' => 'Nom',
'description' => 'Nom du torrent',
'type' => 'text'
),
'description' => array(
'name' => 'Description',
'description' => 'Description du torrent',
'type' => 'text'
),
'fichier' => array(
'name' => 'Fichier',
'description' => 'Fichier du torrent',
'type' => 'text'
),
'uploader' => array(
'name' => 'Uploader',
'description' => 'Uploader du torrent',
'type' => 'text'
),
)
);
public function collectData() {
$catInfo = explode('.', $this->getInput('cat'));
$category = $catInfo[0];
$subcategory = $catInfo[1];
$html = getSimpleHTMLDOM(self::URI . '/engine/search?name='
. $this->getInput('nom')
. '&description='
. $this->getInput('description')
. '&fichier='
. $this->getInput('fichier')
. '&file='
. $this->getInput('uploader')
. '&category='
. $category
. '&sub_category='
. $subcategory
. '&do=search&order=desc&sort=publish_date')
or returnServerError('Unable to query Yggtorrent !');
$count = 0;
$results = $html->find('.results', 0);
if(!$results) return;
foreach($results->find('tr') as $row) {
$count++;
if($count == 1) continue;
if($count == 12) break;
$item = array();
$item['timestamp'] = $row->find('.hidden', 1)->plaintext;
$item['title'] = $row->find('a', 1)->plaintext;
$torrentData = $this->collectTorrentData($row->find('a', 1)->href);
$item['author'] = $torrentData['author'];
$item['content'] = $torrentData['content'];
$item['seeders'] = $row->find('td', 7)->plaintext;
$item['leechers'] = $row->find('td', 8)->plaintext;
$item['size'] = $row->find('td', 5)->plaintext;
$this->items[] = $item;
}
}
public function collectTorrentData($url) {
//For weird reason, the link we get can be invalid, we fix it.
$url_full = explode('/', $url);
$url_full[4] = urlencode($url_full[4]);
$url_full[5] = urlencode($url_full[5]);
$url_full[6] = urlencode($url_full[6]);
$url = implode('/', $url_full);
$page = getSimpleHTMLDOM($url) or returnServerError('Unable to query Yggtorrent page !');
$author = $page->find('.informations', 0)->find('a', 4)->plaintext;
$content = $page->find('.default', 1);
return array('author' => $author, 'content' => $content);
}
}

View File

@@ -25,14 +25,14 @@ class YoutubeBridge extends BridgeAbstract {
'By channel id' => array(
'c' => array(
'name' => 'channel id',
'exampleValue' => "15",
'exampleValue' => '15',
'required' => true
)
),
'By playlist Id' => array(
'p' => array(
'name' => 'playlist id',
'exampleValue' => "15"
'exampleValue' => '15'
)
),
'Search result' => array(
@@ -45,16 +45,52 @@ class YoutubeBridge extends BridgeAbstract {
'type' => 'number',
'exampleValue' => 1
)
),
'global' => array(
'duration_min' => array(
'name' => 'min. duration (minutes)',
'type' => 'number',
'title' => 'Minimum duration for the video in minutes',
'exampleValue' => 5
),
'duration_max' => array(
'name' => 'max. duration (minutes)',
'type' => 'number',
'title' => 'Maximum duration for the video in minutes',
'exampleValue' => 10
)
)
);
private $feedName = '';
private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time){
$html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid");
$author = $html->innertext;
$author = substr($author, strpos($author, '"author=') + 8);
$author = substr($author, 0, strpos($author, '\u0026'));
$desc = $html->find('div#watch-description-text', 0)->innertext;
$time = strtotime($html->find('meta[itemprop=datePublished]', 0)->getAttribute('content'));
// Skip unavailable videos
if(!strpos($html->innertext, 'IS_UNAVAILABLE_PAGE')) {
return;
}
foreach($html->find('script') as $script) {
$data = trim($script->innertext);
if(strpos($data, '{') !== 0)
continue; // Wrong script
$json = json_decode($data);
if(!isset($json->itemListElement))
continue; // Wrong script
$author = $json->itemListElement[0]->item->name;
}
if(!is_null($html->find('#watch-description-text', 0)))
$desc = $html->find('#watch-description-text', 0)->innertext;
if(!is_null($html->find('meta[itemprop=datePublished]', 0)))
$time = strtotime($html->find('meta[itemprop=datePublished]', 0)->getAttribute('content'));
}
private function ytBridgeAddItem($vid, $title, $author, $desc, $time){
@@ -84,28 +120,58 @@ class YoutubeBridge extends BridgeAbstract {
$vid = str_replace('yt:video:', '', $element->find('id', 0)->plaintext);
$time = strtotime($element->find('published', 0)->plaintext);
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
if(strpos($vid, 'googleads') === false)
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
}
$this->request = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext);
$this->feedName = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext); // feedName will be used by getName()
}
private function ytBridgeParseHtmlListing($html, $element_selector, $title_selector){
$limit = 10;
private function ytBridgeParseHtmlListing($html, $element_selector, $title_selector, $add_parsed_items = true) {
$limit = $add_parsed_items ? 10 : INF;
$count = 0;
$duration_min = $this->getInput('duration_min') ?: -1;
$duration_min = $duration_min * 60;
$duration_max = $this->getInput('duration_max') ?: INF;
$duration_max = $duration_max * 60;
if($duration_max < $duration_min) {
returnClientError('Max duration must be greater than min duration!');
}
foreach($html->find($element_selector) as $element) {
if($count < $limit) {
$author = '';
$desc = '';
$time = 0;
$vid = str_replace('/watch?v=', '', $element->find('a', 0)->href);
$vid = substr($vid, 0, strpos($vid, '&') ?: strlen($vid));
$title = $this->ytBridgeFixTitle($element->find($title_selector, 0)->plaintext);
if($title != '[Private Video]') {
$this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
// The duration comes in one of the formats:
// hh:mm:ss / mm:ss / m:ss
// 01:03:30 / 15:06 / 1:24
$durationText = trim($element->find('span[class="video-time"]', 0)->plaintext);
$durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText);
sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds);
$duration = $hours * 3600 + $minutes * 60 + $seconds;
if($duration < $duration_min || $duration > $duration_max) {
continue;
}
if($title != '[Private Video]' && strpos($vid, 'googleads') === false) {
if ($add_parsed_items) {
$this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
}
$count++;
}
}
}
return $count;
}
private function ytBridgeFixTitle($title) {
@@ -115,10 +181,8 @@ class YoutubeBridge extends BridgeAbstract {
private function ytGetSimpleHTMLDOM($url){
return getSimpleHTMLDOM($url,
$use_include_path = false,
$context = null,
$offset = 0,
$maxLen = null,
$header = array(),
$opts = array(),
$lowercase = true,
$forceTagsClosed = true,
$target_charset = DEFAULT_TARGET_CHARSET,
@@ -145,7 +209,7 @@ class YoutubeBridge extends BridgeAbstract {
}
if(!empty($url_feed) && !empty($url_listing)) {
if($xml = $this->ytGetSimpleHTMLDOM($url_feed)) {
if(!$this->skipFeeds() && $xml = $this->ytGetSimpleHTMLDOM($url_feed)) {
$this->ytBridgeParseXmlFeed($xml);
} elseif($html = $this->ytGetSimpleHTMLDOM($url_listing)) {
$this->ytBridgeParseHtmlListing($html, 'li.channels-content-item', 'h3');
@@ -154,16 +218,25 @@ class YoutubeBridge extends BridgeAbstract {
}
} elseif($this->getInput('p')) { /* playlist mode */
$this->request = $this->getInput('p');
$url_feed = self::URI . 'feeds/videos.xml?playlist_id=' . urlencode($this->request);
$url_listing = self::URI . 'playlist?list=' . urlencode($this->request);
$html = $this->ytGetSimpleHTMLDOM($url_listing)
or returnServerError("Could not request YouTube. Tried:\n - $url_listing");
$this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a');
$this->request = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext);
$item_count = $this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a', false);
if ($item_count <= 15 && !$this->skipFeeds() && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) {
$this->ytBridgeParseXmlFeed($xml);
} else {
$this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a');
}
$this->feedName = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
usort($this->items, function ($item1, $item2) {
return $item2['timestamp'] - $item1['timestamp'];
});
} elseif($this->getInput('s')) { /* search mode */
$this->request = $this->getInput('s');
$page = 1;
if($this->getInput('pa'))
$page = (int)preg_replace("/[^0-9]/", '', $this->getInput('pa'));
$page = (int)preg_replace('/[^0-9]/', '', $this->getInput('pa'));
$url_listing = self::URI
. 'results?search_query='
@@ -175,15 +248,28 @@ class YoutubeBridge extends BridgeAbstract {
$html = $this->ytGetSimpleHTMLDOM($url_listing)
or returnServerError("Could not request YouTube. Tried:\n - $url_listing");
$this->ytBridgeParseHtmlListing($html, 'div.yt-lockup', 'h3');
$this->request = 'Search: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext);
$this->ytBridgeParseHtmlListing($html, 'div.yt-lockup', 'h3 > a');
$this->feedName = 'Search: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
} else { /* no valid mode */
returnClientError("You must either specify either:\n - YouTube
username (?u=...)\n - Channel id (?c=...)\n - Playlist id (?p=...)\n - Search (?s=...)");
}
}
private function skipFeeds() {
return ($this->getInput('duration_min') || $this->getInput('duration_max'));
}
public function getName(){
return (!empty($this->request) ? $this->request . ' - ' : '') . 'YouTube Bridge';
// Name depends on queriedContext:
switch($this->queriedContext) {
case 'By username':
case 'By channel id':
case 'By playlist Id':
case 'Search result':
return $this->feedName . ' - YouTube'; // We already know it's a bridge, right?
default:
return parent::getName();
}
}
}

55
bridges/ZenodoBridge.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
class ZenodoBridge extends BridgeAbstract {
const MAINTAINER = 'theradialactive';
const NAME = 'Zenodo';
const URI = 'https://zenodo.org';
const CACHE_TIMEOUT = 10;
const DESCRIPTION = 'Returns the newest content of Zenodo';
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('zenodo.org not reachable.');
foreach($html->find('div.record-elem') as $element) {
$item = array();
$item['uri'] = self::URI . $element->find('h4', 0)->find('a', 0)->href;
$item['title'] = trim(
htmlspecialchars_decode($element->find('h4', 0)->find('a', 0)->innertext,
ENT_QUOTES
)
);
foreach($element->find('p', 0)->find('span') as $authors) {
$item['author'] = $item['author'] . $authors . '; ';
}
$content = $element->find('p.hidden-xs', 0)->find('a', 0)->innertext . '<br>';
$type = '<br>Type: ' . $element->find('span.label-default', 0)->innertext;
$raw_date = $element->find('small.text-muted', 0)->innertext;
$clean_date = date_parse(str_replace('Uploaded on ', '', $raw_date));
$content = $content . date_parse($clean_date);
$item['timestamp'] = mktime(
$clean_date['hour'],
$clean_date['minute'],
$clean_date['second'],
$clean_date['month'],
$clean_date['day'],
$clean_date['year']
);
$access = '';
if ($element->find('span.label-success', 0)->innertext) {
$access = 'Open Access';
} elseif ($element->find('span.label-warning', 0)->innertext) {
$access = 'Embargoed Access';
} else {
$access = $element->find('span.label-error', 0)->innertext;
}
$access = '<br>Access: ' . $access;
$publication = '<br>Publication Date: ' . $element->find('span.label-info', 0)->innertext;
$item['content'] = $content . $type . $access . $publication;
$this->items[] = $item;
}
}
}

View File

@@ -8,7 +8,9 @@ class FileCache implements CacheInterface {
protected $param;
public function loadData(){
return unserialize(file_get_contents($this->getCacheFile()));
if(file_exists($this->getCacheFile())) {
return unserialize(file_get_contents($this->getCacheFile()));
}
}
public function saveData($datas){
@@ -17,7 +19,7 @@ class FileCache implements CacheInterface {
$writeStream = file_put_contents($this->getCacheFile(), serialize($datas));
if($writeStream === false) {
throw new \Exception("Cannot write the cache... Do you have the right permissions ?");
throw new \Exception('Cannot write the cache... Do you have the right permissions ?');
}
return $this;

44
config.default.ini.php Normal file
View File

@@ -0,0 +1,44 @@
; <?php exit; ?> DO NOT REMOVE THIS LINE
; This file contains the default settings for RSS-Bridge. Do not change this
; file, it will be replaced on the next update of RSS-Bridge! You can specify
; your own configuration in 'config.ini.php' (copy this file).
[cache]
; Allow users to specify custom timeout for specific requests.
; true = enabled
; false = disabled (default)
custom_timeout = false
[proxy]
; Sets the proxy url (i.e. "tcp://192.168.0.0:32")
; "" = Proxy disabled (default)
url = ""
; Sets the proxy name that is shown on the bridge instead of the proxy url.
; "" = Show proxy url
name = "Hidden proxy name"
; Allow users to disable proxy usage for specific requests.
; true = enabled
; false = disabled (default)
by_bridge = false
[authentication]
; Enables authentication for all requests to this RSS-Bridge instance.
;
; Warning: You'll have to upgrade existing feeds after enabling this option!
;
; true = enabled
; false = disabled (default)
enable = false
; The username for authentication. Insert this name when prompted for login.
username = ""
; The password for authentication. Insert this password when prompted for login.
; Use a strong password to prevent others from guessing your login!
password = ""

View File

@@ -15,8 +15,11 @@ class AtomFormat extends FormatAbstract{
$extraInfos = $this->getExtraInfos();
$title = $this->xml_encode($extraInfos['name']);
$uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : 'https://github.com/sebsauvage/rss-bridge';
$icon = $this->xml_encode('http://icons.better-idea.org/icon?url='. $uri .'&size=64');
$uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : 'https://github.com/RSS-Bridge/rss-bridge';
$uriparts = parse_url($uri);
$icon = $this->xml_encode($uriparts['scheme'] . '://' . $uriparts['host'] .'/favicon.ico');
$uri = $this->xml_encode($uri);
$entries = '';
@@ -37,6 +40,16 @@ class AtomFormat extends FormatAbstract{
}
}
$entryCategories = '';
if(isset($item['categories'])) {
foreach($item['categories'] as $category) {
$entryCategories .= '<category term="'
. $this->xml_encode($category)
. '"/>'
. PHP_EOL;
}
}
$entries .= <<<EOD
<entry>
@@ -49,6 +62,7 @@ class AtomFormat extends FormatAbstract{
<updated>{$entryTimestamp}</updated>
<content type="html">{$entryContent}</content>
{$entryEnclosures}
{$entryCategories}
</entry>
EOD;

View File

@@ -47,6 +47,20 @@ class HtmlFormat extends FormatAbstract {
$entryEnclosures .= '</div>';
}
$entryCategories = '';
if(isset($item['categories'])) {
$entryCategories = '<div class="categories"><p>Categories:</p>';
foreach($item['categories'] as $category) {
$entryCategories .= '<li class="category">'
. $this->sanitizeHtml($category)
. '</li>';
}
$entryCategories .= '</div>';
}
$entries .= <<<EOD
<section class="feeditem">
@@ -55,6 +69,7 @@ class HtmlFormat extends FormatAbstract {
{$entryAuthor}
{$entryContent}
{$entryEnclosures}
{$entryCategories}
</section>
EOD;

View File

@@ -18,10 +18,11 @@ class MrssFormat extends FormatAbstract {
if(!empty($extraInfos['uri'])) {
$uri = $this->xml_encode($extraInfos['uri']);
} else {
$uri = 'https://github.com/sebsauvage/rss-bridge';
$uri = 'https://github.com/RSS-Bridge/rss-bridge';
}
$icon = $this->xml_encode('http://icons.better-idea.org/icon?url='. $uri .'&size=64');
$uriparts = parse_url($uri);
$icon = $this->xml_encode($uriparts['scheme'] . '://' . $uriparts['host'] .'/favicon.ico');
$items = '';
foreach($this->getItems() as $item) {
@@ -50,6 +51,16 @@ Some media files might not be shown to you. Consider using the ATOM format inste
}
}
$entryCategories = '';
if(isset($item['categories'])) {
foreach($item['categories'] as $category) {
$entryCategories .= '<category>'
. $category . '</category>'
. PHP_EOL;
}
}
$items .= <<<EOD
<item>
@@ -60,6 +71,7 @@ Some media files might not be shown to you. Consider using the ATOM format inste
<description>{$itemContent}{$entryEnclosuresWarning}</description>
<author>{$itemAuthor}</author>
{$entryEnclosures}
{$entryCategories}
</item>
EOD;

239
index.php
View File

@@ -1,49 +1,43 @@
<?php
/*
TODO :
- factorize the annotation system
- factorize to adapter : Format, Bridge, Cache(actually code is almost the same)
- implement annotation cache for entrance page
- Cache : I think logic must be change as least to avoid to reconvert object from json in FileCache case.
- add namespace to avoid futur problem ?
- see FIXME mentions in the code
- implement header('X-Cached-Version: '.date(DATE_ATOM, filemtime($cachefile)));
*/
require_once __DIR__ . '/lib/RssBridge.php';
// Defines the minimum required PHP version for RSS-Bridge
define('PHP_VERSION_REQUIRED', '5.6.0');
//define('PROXY_URL', 'tcp://192.168.0.0:28');
// Set to true if you allow users to disable proxy usage for specific bridges
define('PROXY_BYBRIDGE', false);
// Comment this line or keep PROXY_NAME empty to display PROXY_URL instead
define('PROXY_NAME', 'Hidden Proxy Name');
date_default_timezone_set('UTC');
error_reporting(0);
// Specify directory for cached files (using FileCache)
define('CACHE_DIR', __DIR__ . '/cache');
// Specify path for whitelist file
define('WHITELIST_FILE', __DIR__ . '/whitelist.txt');
Configuration::verifyInstallation();
Configuration::loadConfiguration();
Authentication::showPromptIfNeeded();
date_default_timezone_set('UTC');
error_reporting(0);
/*
Move the CLI arguments to the $_GET array, in order to be able to use
rss-bridge from the command line
*/
parse_str(implode('&', array_slice($argv, 1)), $cliArgs);
$params = array_merge($_GET, $cliArgs);
/*
Create a file named 'DEBUG' for enabling debug mode.
For further security, you may put whitelisted IP addresses
in the 'DEBUG' file, one IP per line. Empty file allows anyone(!).
Debugging allows displaying PHP error messages and bypasses the cache: this can allow a malicious
client to retrieve data about your server and hammer a provider throught your rss-bridge instance.
For further security, you may put whitelisted IP addresses in the file,
one IP per line. Empty file allows anyone(!).
Debugging allows displaying PHP error messages and bypasses the cache: this
can allow a malicious client to retrieve data about your server and hammer
a provider throught your rss-bridge instance.
*/
if(file_exists('DEBUG')) {
$debug_enabled = true;
$debug_whitelist = trim(file_get_contents('DEBUG'));
if(strlen($debug_whitelist) > 0) {
$debug_enabled = false;
foreach(explode("\n", $debug_whitelist) as $allowed_ip) {
if(trim($allowed_ip) === $_SERVER['REMOTE_ADDR']) {
$debug_enabled = true;
break;
}
}
}
$debug_enabled = empty($debug_whitelist)
|| in_array($_SERVER['REMOTE_ADDR'], explode("\n", $debug_whitelist));
if($debug_enabled) {
ini_set('display_errors', '1');
error_reporting(E_ALL);
@@ -51,23 +45,6 @@ if(file_exists('DEBUG')) {
}
}
require_once __DIR__ . '/lib/RssBridge.php';
// Check PHP version
if(version_compare(PHP_VERSION, PHP_VERSION_REQUIRED) === -1)
die('RSS-Bridge requires at least PHP version ' . PHP_VERSION_REQUIRED . '!');
// extensions check
if(!extension_loaded('openssl'))
die('"openssl" extension not loaded. Please check "php.ini"');
if(!extension_loaded('libxml'))
die('"libxml" extension not loaded. Please check "php.ini"');
// configuration checks
if(ini_get('allow_url_fopen') !== "1")
die('"allow_url_fopen" is not set to "1". Please check "php.ini');
// FIXME : beta test UA spoofing, please report any blacklisting by PHP-fopen-unfriendly websites
$userAgent = 'Mozilla/5.0(X11; Linux x86_64; rv:30.0)';
@@ -77,24 +54,23 @@ $userAgent .= '+https://github.com/RSS-Bridge/rss-bridge)';
ini_set('user_agent', $userAgent);
// default whitelist
$whitelist_file = './whitelist.txt';
$whitelist_default = array(
"BandcampBridge",
"CryptomeBridge",
"DansTonChatBridge",
"DuckDuckGoBridge",
"FacebookBridge",
"FlickrExploreBridge",
"GooglePlusPostBridge",
"GoogleSearchBridge",
"IdenticaBridge",
"InstagramBridge",
"OpenClassroomsBridge",
"PinterestBridge",
"ScmbBridge",
"TwitterBridge",
"WikipediaBridge",
"YoutubeBridge");
'BandcampBridge',
'CryptomeBridge',
'DansTonChatBridge',
'DuckDuckGoBridge',
'FacebookBridge',
'FlickrExploreBridge',
'GooglePlusPostBridge',
'GoogleSearchBridge',
'IdenticaBridge',
'InstagramBridge',
'OpenClassroomsBridge',
'PinterestBridge',
'ScmbBridge',
'TwitterBridge',
'WikipediaBridge',
'YoutubeBridge');
try {
@@ -102,22 +78,26 @@ try {
Format::setDir(__DIR__ . '/formats/');
Cache::setDir(__DIR__ . '/caches/');
if(!file_exists($whitelist_file)) {
if(!file_exists(WHITELIST_FILE)) {
$whitelist_selection = $whitelist_default;
$whitelist_write = implode("\n", $whitelist_default);
file_put_contents($whitelist_file, $whitelist_write);
file_put_contents(WHITELIST_FILE, $whitelist_write);
} else {
$whitelist_file_content = file_get_contents($whitelist_file);
$whitelist_file_content = file_get_contents(WHITELIST_FILE);
if($whitelist_file_content != "*\n") {
$whitelist_selection = explode("\n", $whitelist_file_content);
} else {
$whitelist_selection = Bridge::listBridges();
}
// Prepare for case-insensitive match
$whitelist_selection = array_map('strtolower', $whitelist_selection);
}
$action = filter_input(INPUT_GET, 'action');
$bridge = filter_input(INPUT_GET, 'bridge');
$showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
$action = array_key_exists('action', $params) ? $params['action'] : null;
$bridge = array_key_exists('bridge', $params) ? $params['bridge'] : null;
if($action === 'display' && !empty($bridge)) {
// DEPRECATED: 'nameBridge' scheme is replaced by 'name' in bridge parameter values
@@ -126,7 +106,8 @@ try {
$bridge = substr($bridge, 0, $pos);
}
$format = filter_input(INPUT_GET, 'format');
$format = $params['format']
or returnClientError('You must specify a format!');
// DEPRECATED: 'nameFormat' scheme is replaced by 'name' in format parameter values
// this is to keep compatibility until futher complete removal
@@ -135,7 +116,7 @@ try {
}
// whitelist control
if(!Bridge::isWhitelisted($whitelist_selection, $bridge)) {
if(!Bridge::isWhitelisted($whitelist_selection, strtolower($bridge))) {
throw new \HttpException('This bridge is not whitelisted', 401);
die;
}
@@ -143,12 +124,20 @@ try {
// Data retrieval
$bridge = Bridge::create($bridge);
$noproxy = filter_input(INPUT_GET, '_noproxy', FILTER_VALIDATE_BOOLEAN);
$noproxy = array_key_exists('_noproxy', $params) && filter_var($params['_noproxy'], FILTER_VALIDATE_BOOLEAN);
if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
define('NOPROXY', true);
}
$params = $_GET;
// Custom cache timeout
$cache_timeout = -1;
if(array_key_exists('_cache_timeout', $params)) {
if(!CUSTOM_CACHE_TIMEOUT) {
throw new \HttpException('This server doesn\'t support "_cache_timeout"!');
}
$cache_timeout = filter_var($params['_cache_timeout'], FILTER_VALIDATE_INT);
}
// Initialize cache
$cache = Cache::create('FileCache');
@@ -160,13 +149,19 @@ try {
unset($params['bridge']);
unset($params['format']);
unset($params['_noproxy']);
unset($params['_cache_timeout']);
// Load cache & data
try {
$bridge->setCache($cache);
$bridge->setCacheTimeout($cache_timeout);
$bridge->setDatas($params);
} catch(Error $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildBridgeException($e, $bridge));
} catch(Exception $e) {
header('HTTP/1.1 ' . $e->getCode() . ' ' . Http::getMessageForCode($e->getCode()));
http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildBridgeException($e, $bridge));
}
@@ -177,92 +172,22 @@ try {
$format->setItems($bridge->getItems());
$format->setExtraInfos($bridge->getExtraInfos());
$format->display();
} catch(Exception $e) {
header('HTTP/1.1 ' . $e->getCode() . ' ' . Http::getMessageForCode($e->getCode()));
} catch(Error $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildTransformException($e, $bridge));
} catch(Exception $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildBridgeException($e, $bridge));
}
die;
} else {
echo BridgeList::create($whitelist_selection, $showInactive);
}
} catch(HttpException $e) {
header('HTTP/1.1 ' . $e->getCode() . ' ' . Http::getMessageForCode($e->getCode()));
http_response_code($e->getCode());
header('Content-Type: text/plain');
die($e->getMessage());
} catch(\Exception $e) {
die($e->getMessage());
}
$formats = Format::searchInformation();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Rss-bridge" />
<title>RSS-Bridge</title>
<link href="static/style.css" rel="stylesheet">
<script src="static/search.js"></script>
<noscript>
<style>
.searchbar {
display: none;
}
</style>
</noscript>
</head>
<body onload="search()">
<?php
$status = '';
if(defined('DEBUG') && DEBUG === true) {
$status .= 'debug mode active';
}
echo <<<EOD
<header>
<h1>RSS-Bridge</h1>
<h2>·Reconnecting the Web·</h2>
<p class="status">{$status}</p>
</header>
<section class="searchbar">
<h3>Search</h3>
<input type="text" name="searchfield"
id="searchfield" placeholder="Enter the bridge you want to search for"
onchange="search()" onkeyup="search()">
</section>
EOD;
$activeFoundBridgeCount = 0;
$showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
$inactiveBridges = '';
$bridgeList = Bridge::listBridges();
foreach($bridgeList as $bridgeName) {
if(Bridge::isWhitelisted($whitelist_selection, $bridgeName)) {
echo displayBridgeCard($bridgeName, $formats);
$activeFoundBridgeCount++;
} elseif($showInactive) {
// inactive bridges
$inactiveBridges .= displayBridgeCard($bridgeName, $formats, false) . PHP_EOL;
}
}
echo $inactiveBridges;
?>
<section class="footer">
<a href="https://github.com/RSS-Bridge/rss-bridge">RSS-Bridge 2017-08-03 ~ Public Domain</a><br />
<?= $activeFoundBridgeCount; ?>/<?= count($bridgeList) ?> active bridges. <br />
<?php
if($activeFoundBridgeCount !== count($bridgeList)) {
// FIXME: This should be done in pure CSS
if(!$showInactive)
echo '<a href="?show_inactive=1"><button class="small">Show inactive bridges</button></a><br />';
else
echo '<a href="?show_inactive=0"><button class="small">Hide inactive bridges</button></a><br />';
}
?>
</section>
</body>
</html>

31
lib/Authentication.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
class Authentication {
public static function showPromptIfNeeded() {
if(Configuration::getConfig('authentication', 'enable') === true) {
if(!Authentication::verifyPrompt()) {
header('WWW-Authenticate: Basic realm="RSS-Bridge"');
header('HTTP/1.0 401 Unauthorized');
die('Please authenticate in order to access this instance !');
}
}
}
public static function verifyPrompt() {
if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
if(Configuration::getConfig('authentication', 'username') === $_SERVER['PHP_AUTH_USER']
&& Configuration::getConfig('authentication', 'password') === $_SERVER['PHP_AUTH_PW']) {
return true;
} else {
error_log('[RSS-Bridge] Failed authentication attempt from ' . $_SERVER['REMOTE_ADDR']);
}
}
return false;
}
}

View File

@@ -8,16 +8,6 @@ class Bridge {
throw new \LogicException('Please use ' . __CLASS__ . '::create for new object.');
}
/**
* Checks if a bridge is an instantiable bridge.
* @param string $nameBridge name of the bridge that you want to use
* @return true if it is an instantiable bridge, false otherwise.
*/
static public function isInstantiable($nameBridge){
$re = new ReflectionClass($nameBridge);
return $re->IsInstantiable();
}
/**
* Create a new bridge object
* @param string $nameBridge Defined bridge name you want use
@@ -42,11 +32,11 @@ EOD;
require_once $pathBridge;
if(Bridge::isInstantiable($nameBridge)) {
if((new ReflectionClass($nameBridge))->isInstantiable()) {
return new $nameBridge();
} else {
return false;
}
return false;
}
static public function setDir($dirBridge){
@@ -62,13 +52,11 @@ EOD;
}
static public function getDir(){
$dirBridge = self::$dirBridge;
if(is_null($dirBridge)) {
if(is_null(self::$dirBridge)) {
throw new \LogicException(__CLASS__ . ' class need to know bridge path !');
}
return $dirBridge;
return self::$dirBridge;
}
/**
@@ -76,9 +64,8 @@ EOD;
* @return array List of the bridges
*/
static public function listBridges(){
$pathDirBridge = self::getDir();
$listBridge = array();
$dirFiles = scandir($pathDirBridge);
$dirFiles = scandir(self::getDir());
if($dirFiles !== false) {
foreach($dirFiles as $fileName) {
@@ -92,14 +79,10 @@ EOD;
}
static public function isWhitelisted($whitelist, $name){
if(in_array($name, $whitelist)
return in_array($name, $whitelist)
|| in_array($name . '.php', $whitelist)
|| in_array($name . 'Bridge', $whitelist) // DEPRECATED
|| in_array($name . 'Bridge.php', $whitelist) // DEPRECATED
|| (count($whitelist) === 1 && trim($whitelist[0]) === '*')) {
return true;
} else {
return false;
}
|| in_array($name . 'bridge', $whitelist) // DEPRECATED
|| in_array($name . 'bridge.php', $whitelist) // DEPRECATED
|| (count($whitelist) === 1 && trim($whitelist[0]) === '*');
}
}

View File

@@ -14,6 +14,7 @@ abstract class BridgeAbstract implements BridgeInterface {
protected $items = array();
protected $inputs = array();
protected $queriedContext = '';
protected $cacheTimeout;
/**
* Return cachable datas (extrainfos and items) stored in the bridge
@@ -171,7 +172,7 @@ abstract class BridgeAbstract implements BridgeInterface {
if(!is_null($this->cache)) {
$time = $this->cache->getTime();
if($time !== false
&& (time() - static::CACHE_TIMEOUT < $time)
&& (time() - $this->getCacheTimeout() < $time)
&& (!defined('DEBUG') || DEBUG !== true)) {
$cached = $this->cache->loadData();
if(isset($cached['items']) && isset($cached['extraInfos'])) {
@@ -268,4 +269,17 @@ abstract class BridgeAbstract implements BridgeInterface {
public function setCache(\CacheInterface $cache){
$this->cache = $cache;
}
public function setCacheTimeout($timeout){
if(is_numeric($timeout) && ($timeout < 1 || $timeout > 86400)) {
$this->cacheTimeout = static::CACHE_TIMEOUT;
return;
}
$this->cacheTimeout = $timeout;
}
public function getCacheTimeout(){
return isset($this->cacheTimeout) ? $this->cacheTimeout : static::CACHE_TIMEOUT;
}
}

259
lib/BridgeCard.php Normal file
View File

@@ -0,0 +1,259 @@
<?php
final class BridgeCard {
private static function buildFormatButtons($formats) {
$buttons = '';
foreach($formats as $name) {
$buttons .= '<button type="submit" name="format" value="'
. $name
. '">'
. $name
. '</button>'
. PHP_EOL;
}
return $buttons;
}
private static function getFormHeader($bridgeName, $isHttps = false) {
$form = <<<EOD
<form method="GET" action="?">
<input type="hidden" name="action" value="display" />
<input type="hidden" name="bridge" value="{$bridgeName}" />
EOD;
if(!$isHttps) {
$form .= '<div class="secure-warning">Warning :
This bridge is not fetching its content through a secure connection</div>';
}
return $form;
}
private static function getForm($bridgeName,
$formats,
$isActive = false,
$isHttps = false,
$parameterName = '',
$parameters = array()) {
$form = BridgeCard::getFormHeader($bridgeName, $isHttps);
foreach($parameters as $id => $inputEntry) {
if(!isset($inputEntry['exampleValue']))
$inputEntry['exampleValue'] = '';
if(!isset($inputEntry['defaultValue']))
$inputEntry['defaultValue'] = '';
$idArg = 'arg-'
. urlencode($bridgeName)
. '-'
. urlencode($parameterName)
. '-'
. urlencode($id);
$form .= '<label for="'
. $idArg
. '">'
. filter_var($inputEntry['name'], FILTER_SANITIZE_STRING)
. ' : </label>'
. PHP_EOL;
if(!isset($inputEntry['type']) || $inputEntry['type'] === 'text') {
$form .= BridgeCard::getTextInput($inputEntry, $idArg, $id);
} elseif($inputEntry['type'] === 'number') {
$form .= BridgeCard::getNumberInput($inputEntry, $idArg, $id);
} else if($inputEntry['type'] === 'list') {
$form .= BridgeCard::getListInput($inputEntry, $idArg, $id);
} elseif($inputEntry['type'] === 'checkbox') {
$form .= BridgeCard::getCheckboxInput($inputEntry, $idArg, $id);
}
}
if($isActive) {
$form .= BridgeCard::buildFormatButtons($formats);
} else {
$form .= '<span style="font-weight: bold;">Inactive</span>';
}
return $form . '</form>' . PHP_EOL;
}
private static function getInputAttributes($entry) {
$retVal = '';
if(isset($entry['required']) && $entry['required'] === true)
$retVal .= ' required';
if(isset($entry['pattern']))
$retVal .= ' pattern="' . $entry['pattern'] . '"';
if(isset($entry['title']))
$retVal .= ' title="' . filter_var($entry['title'], FILTER_SANITIZE_STRING) . '"';
return $retVal;
}
private static function getTextInput($entry, $id, $name) {
return '<input '
. BridgeCard::getInputAttributes($entry)
. ' id="'
. $id
. '" type="text" value="'
. filter_var($entry['defaultValue'], FILTER_SANITIZE_STRING)
. '" placeholder="'
. filter_var($entry['exampleValue'], FILTER_SANITIZE_STRING)
. '" name="'
. $name
. '" /><br>'
. PHP_EOL;
}
private static function getNumberInput($entry, $id, $name) {
return '<input '
. BridgeCard::getInputAttributes($entry)
. ' id="'
. $id
. '" type="number" value="'
. filter_var($entry['defaultValue'], FILTER_SANITIZE_NUMBER_INT)
. '" placeholder="'
. filter_var($entry['exampleValue'], FILTER_SANITIZE_NUMBER_INT)
. '" name="'
. $name
. '" /><br>'
. PHP_EOL;
}
private static function getListInput($entry, $id, $name) {
$list = '<select '
. BridgeCard::getInputAttributes($entry)
. ' id="'
. $id
. '" name="'
. $name
. '" >';
foreach($entry['values'] as $name => $value) {
if(is_array($value)) {
$list .= '<optgroup label="' . htmlentities($name) . '">';
foreach($value as $subname => $subvalue) {
if($entry['defaultValue'] === $subname
|| $entry['defaultValue'] === $subvalue) {
$list .= '<option value="'
. $subvalue
. '" selected>'
. $subname
. '</option>';
} else {
$list .= '<option value="'
. $subvalue
. '">'
. $subname
. '</option>';
}
}
$list .= '</optgroup>';
} else {
if($entry['defaultValue'] === $name
|| $entry['defaultValue'] === $value) {
$list .= '<option value="'
. $value
. '" selected>'
. $name
. '</option>';
} else {
$list .= '<option value="'
. $value
. '">'
. $name
. '</option>';
}
}
}
$list .= '</select><br>';
return $list;
}
private static function getCheckboxInput($entry, $id, $name) {
return '<input '
. BridgeCard::getInputAttributes($entry)
. ' id="'
. $id
. '" type="checkbox" name="'
. $name
. '" '
. ($entry['defaultValue'] === 'checked' ?: '')
. ' /><br>'
. PHP_EOL;
}
static function displayBridgeCard($bridgeName, $formats, $isActive = true){
$bridge = Bridge::create($bridgeName);
if($bridge == false)
return '';
$isHttps = strpos($bridge->getURI(), 'https') === 0;
$uri = $bridge->getURI();
$name = $bridge->getName();
$description = $bridge->getDescription();
$parameters = $bridge->getParameters();
if(defined('PROXY_URL') && PROXY_BYBRIDGE) {
$parameters['global']['_noproxy'] = array(
'name' => 'Disable proxy (' . ((defined('PROXY_NAME') && PROXY_NAME) ? PROXY_NAME : PROXY_URL) . ')',
'type' => 'checkbox'
);
}
if(CUSTOM_CACHE_TIMEOUT) {
$parameters['global']['_cache_timeout'] = array(
'name' => 'Cache timeout in seconds',
'type' => 'number',
'defaultValue' => $bridge->getCacheTimeout()
);
}
$card = <<<CARD
<section id="bridge-{$bridgeName}" data-ref="{$bridgeName}">
<h2><a href="{$uri}">{$name}</a></h2>
<p class="description">{$description}</p>
<input type="checkbox" class="showmore-box" id="showmore-{$bridgeName}" />
<label class="showmore" for="showmore-{$bridgeName}">Show more</label>
CARD;
// If we don't have any parameter for the bridge, we print a generic form to load it.
if(count($parameters) === 0
|| count($parameters) === 1 && array_key_exists('global', $parameters)) {
$card .= BridgeCard::getForm($bridgeName, $formats, $isActive, $isHttps);
} else {
foreach($parameters as $parameterName => $parameter) {
if(!is_numeric($parameterName) && $parameterName === 'global')
continue;
if(array_key_exists('global', $parameters))
$parameter = array_merge($parameter, $parameters['global']);
if(!is_numeric($parameterName))
$card .= '<h5>' . $parameterName . '</h5>' . PHP_EOL;
$card .= BridgeCard::getForm($bridgeName, $formats, $isActive, $isHttps, $parameterName, $parameter);
}
}
$card .= '<label class="showless" for="showmore-' . $bridgeName . '">Show less</label>';
$card .= '<p class="maintainer">' . $bridge->getMaintainer() . '</p>';
$card .= '</section>';
return $card;
}
}

View File

@@ -68,4 +68,20 @@ interface BridgeInterface {
* @param object CacheInterface The cache instance
*/
public function setCache(\CacheInterface $cache);
/**
* Sets the timeout for clearing the cache files. The timeout must be
* specified between 1..86400 seconds (max. 24 hours). The default timeout
* (specified by the bridge maintainer) applies for invalid values.
*
* @param int $timeout The cache timeout in seconds
*/
public function setCacheTimeout($timeout);
/**
* Returns the cache timeout
*
* @return int Cache timeout
*/
public function getCacheTimeout();
}

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