1
0
mirror of https://github.com/RSS-Bridge/rss-bridge.git synced 2025-08-17 14:00:43 +02:00

Compare commits

..

154 Commits

Author SHA1 Message Date
logmanoriginal
2120cc42fb [Configuration] Bump version to 2019-07-06 2019-07-06 12:34:42 +02:00
logmanoriginal
5067501661 [README] Update list of contributors 2019-07-06 12:34:42 +02:00
LogMANOriginal
aea8484ccc [FicbookBridge] Add new bridge (#1185) 2019-07-06 12:29:36 +02:00
logmanoriginal
6b9394dc78 [DemonoidBridge] Remove bridge
The public service demonoid.pw is no longer available and is
currently being rebuild under demonoid.info which hides torrents
behind a login wall. As this is not supported by RSS-Bridge, the
bridge will be removed.

Find more details on Reddit:
https://www.reddit.com/r/Demonoid/
2019-07-06 12:25:23 +02:00
logmanoriginal
4b51d42b8c cache: Keep subfolders in the repository
References #1200
2019-07-06 12:12:59 +02:00
Joseph
d3fbf0d872 Fix bridge description (#1207) 2019-07-06 11:59:55 +02:00
Joseph
41a8eb74a1 [PinterestBridge] Remove search (#1206)
* Remove getSearchResults()
* Remove ''From search' from PARAMETERS array
* Update getURI() and getName()
* Update collectData()
* Add '.rss' to URL in `collectData` instead of in `getURI`
2019-07-06 11:57:48 +02:00
Joseph
7e6c58b67a [HaveIBeenPwnedBridge] Display breach type (#1203)
* Extract breach types for each data breach
* Add paragraph tag
2019-07-06 11:55:31 +02:00
triatic
a31e518a07 [TelegramBridge] Fix forwarded videos (#1202)
Videos forwarded from other channels use a slightly different format, This fixes it.
2019-07-06 11:52:56 +02:00
logmanoriginal
50162f52b6 [XenForoBridge] Fix minor issues with CSS selectors 2019-07-03 19:34:43 +02:00
logmanoriginal
c0edf6e424 [ShanaprojectBridge] Add filter options
- Filter by minimum number of episodes
- Filter by minimum number of total episodes
- Filter by banner image
2019-07-03 19:34:43 +02:00
logmanoriginal
2ea8d73ac1 [ShanaprojectBridge] Return url to current season 2019-07-02 20:46:38 +02:00
logmanoriginal
465cd8c768 [ShanaprojectBridge] Add support for https and cleanup 2019-07-02 20:45:31 +02:00
logmanoriginal
73f4bc078e [CastorusBridge] Fix broken activity selector 2019-06-28 20:31:49 +02:00
Nicolas Delsaux
1add201d3b [WorldOfTanksBridge] Fix bridge (#1197)
* Fix #1196 by better protecting page
2019-06-28 19:32:26 +02:00
Nicolas Delsaux
09113c2594 [GQMagazineBridge] Fix bridge (#1195)
* Fix bridge by changing the way the articles are loaded AND their titles are found
2019-06-28 19:29:32 +02:00
Joseph
c39e642877 [HaveIBeenPwnedBridge] Convert HTML entities to characters (#1198) 2019-06-28 16:08:56 +02:00
Joseph
e2460ead18 [InternetArchiveBridge] Add new bridge (#1186) 2019-06-28 15:45:27 +02:00
logmanoriginal
60c1339612 [InstructablesBridge] Fix after layout changes 2019-06-27 21:05:50 +02:00
logmanoriginal
d324aa5da1 [InstructablesBridge] Update available categories 2019-06-27 20:29:21 +02:00
logmanoriginal
6f24987601 [InstructablesBridge] Fix listCategories() to work with new layout 2019-06-27 20:28:23 +02:00
logmanoriginal
54fb29d443 [InstructablesBridge] Add support for HTTPS 2019-06-27 20:16:53 +02:00
Joseph
ebe463dd08 [TelegramBridge] Set 'username' parameter as required (#1192) 2019-06-27 20:03:18 +02:00
logmanoriginal
987f42d6d4 logo: Add logo to the project
References #1087
2019-06-25 18:42:11 +02:00
logmanoriginal
fa8253c8bf [GiteaBridge] Add new bridge
Gitea is a fork of Gogs and therefore shares most of its features
except for releases.
2019-06-23 09:21:00 +02:00
logmanoriginal
e4444e6432 [GogsBridge] Add new bridge 2019-06-23 09:21:00 +02:00
triatic
3769850ba3 [TelegramBridge] Fix entries for "media too big" (#1184)
When a large video is posted, "Media is too big" appears in web preview. This adds code to detect this and offer a link.
2019-06-23 08:54:52 +02:00
LogMANOriginal
89e3da0b6f [IndeedBridge] Add new bridge (#1166)
Implements a bridge for
https://www.indeed.com/ (or any of the local variants)

Features:
- Takes a company name and returns a list of reviews and comments
- Limit the maximum number of items to return (default: 20)
- No upper limit on the number of items to return
- Search by language code (45 options)
- Supports detectParameters for any supported URL
2019-06-22 18:50:06 +02:00
logmanoriginal
99d4571c6b core: Make RSS-Bridge more usable via mobile devices
Adds styles for display sizes smaller than 768px where
elements are currently hardly usable. Note that RSS-Bridge
is not designed for mobile use, but some users may want
to try things on their mobile phone before using it in
real life applications.

Resolves #796
2019-06-22 18:46:37 +02:00
triatic
69acc6228a [TelegramBridge] Populate author (#1183) 2019-06-22 18:45:15 +02:00
triatic
5e2f0fb626 [TelegramBridge] Prevent double encoding entities (#1182) 2019-06-22 18:44:25 +02:00
triatic
372461b1a3 [TelegramBridge] Fix timestamp for videos (#1181) 2019-06-22 18:34:02 +02:00
logmanoriginal
1591e18027 core: Add context hinting for new feeds
RSS-Bridge currently has to guess the queried context from the data
provided by the user. This, however, can cause issues for bridges
that have multiple contexts with conflicting parameters (i.e. none).

This commit adds context hinting to queries via '&context=<context>'
which can be omitted in which case the context is determined as before.
2019-06-21 19:12:29 +02:00
husimo
e2bca5bb05 [MastodonBridge] Add new bridge (#1178) 2019-06-21 17:30:34 +02:00
logmanoriginal
7926ffad73 [KununuBridge] Improve feed contents
- Add support for ratings
- Add support for benefits
- Fix broken timestamp
2019-06-21 00:00:44 +02:00
logmanoriginal
7ff97c0c7b [HtmlFormat] Dynamically build buttons for other feed formats
Adding or removing feed formats from the "formats/" directory
currently has no effect on the buttons shown in the HTML format.
This can cause errors if users press one of the buttons for a
format that is no longer available on the server.

This commit changes the behavior to dynamically add buttons based
on the available formats. Syndication feeds, however, are no longer
supported as they require knowledge about the content type, which
is not known without further changes to the formats API (may be
added later if there is a demand).

Closes #942
2019-06-19 23:13:37 +02:00
Joseph
1989252608 [TelegramBridge] Add new bridge (#1175) 2019-06-19 22:40:56 +02:00
LogMANOriginal
91e73b00b5 [NationalGeographicBridge] Add new bridge (#1065)
Closes #1029
2019-06-18 22:57:42 +02:00
LogMANOriginal
5c6c79baf4 [VimeoBridge] Add new bridge (#933)
Closes #932
2019-06-18 22:50:31 +02:00
Joseph
99d1343045 [SplCenterBridge] Add new bridge (#1177) 2019-06-18 22:18:52 +02:00
logmanoriginal
14e6dbb645 [ListActionTest] Fix broken test 2019-06-18 19:21:28 +02:00
logmanoriginal
fc8421ed50 format: Refactor format factory to non-static class
The format factory can be based on the abstract factory class if it
wasn't static. This allows for higher abstraction and makes future
extensions possible. Also, not all parts of RSS-Bridge need to work
on the same instance of the factory.

References #1001
2019-06-18 19:15:20 +02:00
logmanoriginal
2460b67886 cache: Refactor cache factory to non-static class
The cache factory can be based on the abstract factory class if it
wasn't static. This allows for higher abstraction and makes future
extensions possible. Also, not all parts of RSS-Bridge need to work
on the same instance of the factory.

References #1001
2019-06-18 19:04:19 +02:00
logmanoriginal
705b9daa0b bridge: Refactor bridge factory to non-static class
The bridge factory can be based on the abstract factory class if it
wasn't static. This allows for higher abstraction and makes future
extensions possible. Also, not all parts of RSS-Bridge need to work
on the same instance of the bridge factory.

References #1001
2019-06-18 18:55:32 +02:00
logmanoriginal
1ada9c26f8 format: Sanitize format name in the format factory
RSS-Bridge currently sanitizes the format name only for the display
action, which can cause problems if other actions depend on formats
as well.

It is therefore better to do sanitization in the factory class for
formats. Additionally, formats should not require a perfect match,
so 'Atom' and 'aToM' make no difference. This will also allow users
to define formats in their own style (i.e. only lowercase via CLI).

References #1001
2019-06-18 18:36:16 +02:00
Corentin Garcia
55e1703741 [EliteDangerousGalnetBridge] Remove duplicate items (#1167) 2019-06-16 20:35:23 +02:00
Tobias Alexander Franke
849eaeb50e [SteamCommunityBridge] Add Workshop category (#1172) 2019-06-16 20:21:48 +02:00
Thibault Couraud
aeca4cfd60 [BAEBridge] Use defaultLinkTo rather than str_replace (#1168) 2019-06-16 19:40:21 +02:00
Thibault Couraud
686f21bc50 [FindACrew] Improve bridge results (#1120) 2019-06-16 19:35:43 +02:00
LogMANOriginal
8dd8be9694 [.gitattributes] Keep files in export for Heroku
Heroku requires the file `app.json` as well as the composer files
`composer.json` and `composer.lock` to deploy a service. Deploy
doesn't work if these files are ignored during export (because of
the way this service deploys projects).

This commit adds comments to .gitattributes to prevent this issue
from re-appearing in the future. All affected lines are commented
out.

Also added some spacing for better readability.

References #1165
2019-06-16 19:15:28 +02:00
logmanoriginal
dfa9c651cd [BridgeList] Change placeholder message in the search bar
The search bar should indicate that searching by URL is
supported.

References #1099
2019-06-13 19:55:10 +02:00
logmanoriginal
6d6d6037a3 [GithubIssueBridge] Don't return error messages in detectParameters()
detectParameters() is called in a loop for all bridges on a URL, thus
if a bridge returns an error message, the output messages get mixed
up and all detect operations fail.

This seems to be a limitation of the detect function for now.
2019-06-13 19:49:54 +02:00
Joseph
2559dbbf49 [BrutBridge] Create custom feed name for each category and edition (#1164) 2019-06-13 19:13:02 +02:00
logmanoriginal
de53120843 [SakugabooruBridge] Remove bridge
The target server for this bridge is no longer reachable and
there doesn't seem to be any attempt to get it back online.
2019-06-12 20:22:53 +02:00
logmanoriginal
b1b7e4edce [DollbooruBridge] Remove bridge
The target site for this bridge has been down for at least a year
now and there doesn't seem to be any attempt to get it back up.
Their twitter account is also silent since 2012, so no harm
removing this bridge.

https://twitter.com/dollbooru?lang=en
2019-06-12 20:11:34 +02:00
logmanoriginal
b27487ace0 [TwitterBridge] Fix detection of retweets on lists
References #1161
2019-06-12 18:27:35 +02:00
logmanoriginal
d005acca83 [TwitterBridge] Add extensive description to keyword search query
References #1163
2019-06-11 21:53:22 +02:00
LogMANOriginal
93de8c239b [README] Remove GooglePlus from supported sites 2019-06-10 15:40:57 +02:00
logmanoriginal
75b0213684 [GithubIssueBridge] Add support for detect action
References #1100
2019-06-10 15:32:57 +02:00
Eugene Molotov
f76a23f0a5 [YoutubeBridge] Add playlist caching (#1162) 2019-06-10 15:31:35 +02:00
logmanoriginal
e4e04a7865 [GithubIssueBridge] Fix broken feed item URLs
References #1100
2019-06-10 00:02:13 +02:00
logmanoriginal
da339fd5cc [GithubIssueBridge] Include issue author comment in the feed
- Add function to build an URL to the GitHub issue comment
- Change scope of internal functions from protected to private
- Use IDs instead of classes as comment selectors, to include the
issue author in the output feed.

References #1100
2019-06-09 20:39:45 +02:00
logmanoriginal
ba116d9ab6 [GithubIssueBridge] Fix bridge after DOM changes 2019-06-09 19:57:48 +02:00
logmanoriginal
ea08445946 [GlassdoorBridge] Fix broken bridge 2019-06-09 19:35:53 +02:00
logmanoriginal
ade09b2aad [XenForoBridge] Fix broken bridge 2019-06-09 19:35:53 +02:00
logmanoriginal
28d46b6721 [ShanaprojectBridge] Fix broken bridge 2019-06-09 19:35:46 +02:00
logmanoriginal
1efb7c7bce [DesoutterBridge] Fix bridge after DOM changes 2019-06-09 19:01:54 +02:00
Joseph
d34411137f [TwitterBridge] Display all images from a tweet (#1160) 2019-06-09 17:24:40 +02:00
logmanoriginal
70542686bb [contents] Fix parsing of incomplete headers
Response headers may contain fields with no values.

Example:
  "Referrer-Policy: "

In this case the current implementation of explode() results in an
error because there is no content after ": ". Changing the delimiter
to ":" and trimming the value manually fixes that issue.
2019-06-09 17:18:08 +02:00
LogMANOriginal
edf10be93a [README] Change color for Guix release to blue
This prevents confusion with the build status for Travis-CI and Docker
2019-06-08 20:36:59 +02:00
LogMANOriginal
a725fdd315 [README] Add logos to badges where applicable 2019-06-08 20:27:41 +02:00
logmanoriginal
84ba0c4a9e [Configuration] Bump version to dev.2019-06-08 2019-06-08 20:12:04 +02:00
logmanoriginal
c17b864242 [Configuration] Bump version to 2019-06-08 2019-06-08 20:04:57 +02:00
logmanoriginal
5ff3d0121c [README] Update list of contributors 2019-06-08 20:04:06 +02:00
Joseph
f00a054e0f [BrutBridge] Add new bridge (#1159) 2019-06-08 19:30:42 +02:00
logmanoriginal
5a9519967b [Exceptions] Add button to search for similar issues on GitHub
Users currently only get one option: to open a new issue on GitHub.
This can, however, result in duplicate issues, which is not desired.

This commit adds a second button to the error message, which links
to the GitHub issues tracker with the search query set to find
errors for the current bridge. That way, users can collaborate
on the same issue.
2019-06-08 17:05:35 +02:00
logmanoriginal
17f587fcbe [index] Don't set the timezone in index.php 2019-06-08 16:16:03 +02:00
logmanoriginal
f28cbecc02 [style] Fix placeholder should be hidden on focus
The placeholder is currently visible on key focus and only hidden
once a user starts typing. This can be confusing and doesn't look
good.

As it turns out, ::placeholder is an official selector:
https://developer.mozilla.org/en-US/docs/Web/CSS/::placeholder

For some reason, listing placeholder selectors with "," doesn't
work on some browsers (tested in FF 60 ESR). Making each of the
selectors explicit works, however.
2019-06-08 15:50:16 +02:00
LogMANOriginal
84450371b5 [README] Remove Deploy to Docker Cloud button
In December 2018 Docker Cloud has become part of Docker Hub:
https://blog.docker.com/2018/12/the-new-docker-hub/

Since then the "Deploy to Docker Cloud" button is broken (error 404)
with no alternative for Docker Hub, so the button should be removed.

Docker images are still available at
https://hub.docker.com/r/rssbridge/rss-bridge/
2019-06-08 15:19:56 +02:00
logmanoriginal
69dd33ac82 [.gitattributes] Use the same indentation style for the entire file 2019-06-08 15:07:08 +02:00
logmanoriginal
95388cdf44 [.gitattributes] Exclude demo bridges from release builds 2019-06-08 15:03:25 +02:00
logmanoriginal
b74dda7af9 [.gitattributes] Exclude Composer and Heroku files from release builds 2019-06-08 15:00:07 +02:00
logmanoriginal
ca1a5feba5 [.gitattributes] Annotate export-ignore sections 2019-06-08 14:58:18 +02:00
Squirrel
69a0498732 [README] Add deploy button to Heroku (#1150)
* Add deploy button to Heroku
* Add composer.json and composer.lock (required by Heroku)
2019-06-08 14:53:26 +02:00
logmanoriginal
3d231a417f bridges: Don't kill scripts with die()
Bridges should generally utilize the API functions instead of killing
the script. Find more information on the Wiki.

- returnServerError
https://github.com/RSS-Bridge/rss-bridge/wiki/The-returnServerError-function

- returnClientError
https://github.com/RSS-Bridge/rss-bridge/wiki/The-returnClientError-function

- returnError
https://github.com/RSS-Bridge/rss-bridge/wiki/The-returnError-function
2019-06-07 20:38:09 +02:00
logmanoriginal
35bd706391 [Configuration] Use common format to report errors to the user
Incorrect configuration values are currently handled individually
for each condition, resulting in a lot of repetitive operations.

This commit adds two new private functions to report errors to the
user and end execution of the script.
2019-06-07 20:27:20 +02:00
logmanoriginal
0e30468e0f [rssbridge] Use PATH_ROOT whenever possible 2019-06-07 19:51:06 +02:00
logmanoriginal
ccf375e917 config: Use global constant for config files
The configuration files are currently hard-coded in the configuration
classes and error messages. However, the implementation should not
rely on specific details like the file name. Instead, the files should
be part of the global definition.

This commit introduces two global constants for the configuration files

- FILE_CONFIG => 'config.ini.php'
- FILE_CONFIG_DEFAULT => 'config.default.ini.php'
2019-06-07 19:48:29 +02:00
logmanoriginal
946a99d334 config: Add [system] => 'timezone'
RSS-Bridge currently statically sets the timezone to UTC which can
result in incorrect timestamps if the server is hosted in another
region.

This commit adds a new configuration parameter to allow admins to
specify their own timezone for their servers. Invalid values will
result in an error message.

Example:

  [system]
  timezone = "UTC"

For compatibility reasons the default value is set to UTC.

This parameter accepts any of the supported timezones listed at
https://www.php.net/manual/en/timezones.php

Closes #956
References #1001
2019-06-07 19:22:51 +02:00
logmanoriginal
e2e0ced055 [Bridge] Improve performance for correctly written whitelist.txt
If the bridge name matches exactly, it is not necessary to perform
a strtolower compare of bridges. In some situations this can lead
to much faster response times (depending on the amount of bridges
in whitelist.txt).
2019-06-06 20:59:33 +02:00
logmanoriginal
d4e867f240 core: Move default bridges to whitelist.default.txt
Default bridges are currently statically defined in index.php, which
is not the right place if we want to keep responsibilities separated.

This commit introduces a new file whitelist.default.txt that holds
the default bridges and which is loaded automatically, if whitelist.txt
doesn't exist.

Due to this it is also no longer necessary to have write permission
for the root directory.

References #1001
2019-06-06 20:53:46 +02:00
Eugene Molotov
b0a780acda [VkBridge] Ignore illegal characters in input html for iconv (#1154) 2019-06-06 20:05:41 +02:00
Antoine Cadoret
1814116d67 [SteamBridge] Follow source changes (#1143)
* Follow source data fetching changes
* Improve media path building
* Improve price fetching and display
2019-06-06 19:59:30 +02:00
LogMANOriginal
d89326fe2d Remove old bridge request template 2019-06-06 19:57:04 +02:00
LogMANOriginal
62198ecfa2 Rename bridge request template
Use the same naming convention for all templates
2019-06-06 19:55:57 +02:00
LogMANOriginal
94e4ef8f27 Add template for generic feature requests 2019-06-06 19:54:34 +02:00
logmanoriginal
6c4098d655 Revert "all: Use ->remove() instead of ->outertext = ''"
This reverts commit 052844f5e1.

There is a bug in ->remove() that causes the parser to incorrectly
identify elements in the DOM tree that shouldn't exist anymore.

References #1151
2019-06-02 13:06:16 +02:00
logmanoriginal
468d8be72d [Exceptions] Fix GitHub query labels for bug reports
All bug reports now use the Bridge-Broken label by default
2019-06-01 22:35:56 +02:00
LogMANOriginal
ed539bacf9 Add issue template for generic bug reports
This commit adds a new template for generic bug reports based on the standard template provided by GitHub.
2019-06-01 22:35:33 +02:00
LogMANOriginal
82a9bb5b1c [.github] Update issue template for bridge requests
* Automatically label bridge requests
* Propose default title for new bridge requests
2019-06-01 22:22:05 +02:00
Eugene Molotov
15c374e317 [PikabuBridge] More options and fixes (#1149)
* Add gif support
* Use page title as feed title
* Implement community support
2019-06-01 21:35:18 +02:00
logmanoriginal
052844f5e1 all: Use ->remove() instead of ->outertext = ''
simplehtmldom 1.9 introduced new functions to recursively remove
nodes from the DOM. This allows removing elements without the need
to re-load the document by using $html->load($html->save()), which
is very inefficient.

Find more information about remove() at
https://simplehtmldom.sourceforge.io/docs/1.9/api/simple_html_dom_node/remove/
2019-06-01 21:29:57 +02:00
logmanoriginal
014b698f67 [html] Use find('*') over custom solution
find('*') wasn't supported in older versions of simplehtmldom but it
is supported now. Thus, all custom implementations can be replaced
by the correct solution.
2019-06-01 21:05:12 +02:00
logmanoriginal
5656792cee [simplehtmldom] Update to version 1.9
Find the release notes at
https://sourceforge.net/projects/simplehtmldom/files/simplehtmldom/1.9/
2019-06-01 20:02:07 +02:00
fulmeek
66c5b732cf [FeedItem] Avoid repeated UID hashing after loading from cache (#1148)
This fixes the following issue:

1. bridge sets unique ids for the items (ids get hashed)
2. items go to the cache
3. on next run items get loaded from cache
4. these items have different ids because they were hashed again
5. they show up twice in feed reader
2019-06-01 19:36:46 +02:00
Joseph
b889e867fd [SoundCloudBridge] Use account avatar as feed icon (#1146) 2019-06-01 15:04:42 +02:00
sysadminstory
b519d350bf [RadioMelodieBridge] Fix bridge after website update (#1145)
- The bridge has been adapted to the new website layout
- The content now shows the header picture below the date
2019-06-01 12:12:17 +02:00
Joseph
2a254855d8 [HaveIBeenPwnedBridge] Add new bridge (#1144) 2019-06-01 12:06:58 +02:00
Nemo
72bcc173eb [Docker] Switch Docker Image to official php base image (#1140)
* Switch Docker Image to official php base image

Switch from the unofficial Alpine+php image to the official php-apache image.
This has 2 advantages:

1. Official image is guaranteed to have regular updates, etc
2. The persistent Docker Alpine DNS Issue goes away;
https://github.com/gliderlabs/docker-alpine/issues/255

* [Docker] Ignore more files from Docker Image
2019-06-01 11:25:01 +02:00
Tobias Alexander Franke
4a60f05fd6 [BinanceBridge] Add new bridge (#1135) 2019-06-01 11:18:30 +02:00
somini
84d48d5614 [QPlayBridge]: New Bridge (#1118)
* [QPlayBridge]: New Bridge
2019-05-29 22:51:52 +02:00
Tobias Alexander Franke
7cf898b5af [SteamCommunityBridge] Add new bridge (#1136)
* [SteamCommunityBridge] Add new bridge
2019-05-29 22:50:04 +02:00
killruana
16bd2aec7a [MediapartBridge] Add new bridge (#1130)
* If no cookie session is defined, use the default rss stream
* Add a parameter for enabling/disabling the single page mode
2019-05-15 21:51:23 +02:00
Dreckiger-Dan
3d87ecbf8c [.gitignore] Add robots.txt to the ignore list (#1128) 2019-05-15 21:40:50 +02:00
Lyra
2cd310c025 Bump version to 2019-05-08 2019-05-08 22:36:22 +02:00
sysadminstory
b764204c3a [YoutubeBridge] Playlist bug fix (#1117)
This commit allow the bridge to parse an infinite number of items of a
Youtube playlist.

It should fix #647 !
2019-05-08 22:17:48 +02:00
Tobias Alexander Franke
a9e2574016 [ArtStationBridge] Added new bridge (#1122)
* [ArtStationBridge] Added new bridge
2019-05-08 22:14:53 +02:00
pofilo
e3f6e1c6db [DELETE] Deletion Google Plus bridge (#1124) 2019-05-08 22:11:50 +02:00
Lyra
8150a73922 [CourrierInternationalBridge] Use newer https-based URL 2019-05-08 22:09:49 +02:00
Lyra
a2f3866383 [RoadAndTrackBridge] Major rewrite, due to the depreciation of their API 2019-05-08 21:57:59 +02:00
Obsidienne
a3446ae77b [AO3Bridge] Add new bridge (#1123)
* [AO3Bridge] Add new bridge
2019-05-06 13:28:42 +02:00
Eugene Molotov
75359bc11b [core] Implemented MemcachedCache (#1000)
* [core] Implemented MemcachedCache
2019-05-03 11:56:07 +02:00
Roliga
fe103974f5 [BadDragonBridge] Add new bridge (#1082)
* [BadDragonBridge] Add new bridge
2019-05-02 22:02:13 +02:00
fulmeek
33c16f8be5 [BakaUpdatesMangaReleasesBridge] Sanitize hash for more solid UIDs (#1113)
This should minimize occasional hiccups on regular updates.
2019-04-30 21:01:48 +02:00
fulmeek
21d3bf3b60 caches: Refactor the API (#1060)
- For consistency, functions should always return null on non-existing data.

- WordPressPluginUpdateBridge appears to have used its own cache instance in the past. Obviously not used anymore.

- Since $key can be anything, the cache implementation must ensure to assign the related data reliably; most commonly by serializing and hashing the key in an appropriate way.

- Even though the default path for storage is perfectly fine, some people may want to use a different location. This is an example how a cache implementation is responsible for its requirements.
2019-04-29 20:12:43 +02:00
sysadminstory
3b8f3da09d [AutoJMBridge] Use title from website for Feed Title (#1093)
* [AutoJMBridge] Use title from website for Feed Title
2019-04-20 22:22:06 +02:00
sysadminstory
f9c4a84c25 [RadioMelodieBridge] Update to support new Website (#1101)
* [RadioMelodieBridge] Update to support new Website
2019-04-20 22:19:22 +02:00
Lorenzo Stanco
7b8dd93a8e [InstagramBridge] Fix image link 2019-04-20 22:15:30 +02:00
somini
8f5151b222 [SIMARBridge]: Add new bridge (#1055)
* [SIMARBridge]: Add new bridge
2019-04-16 09:58:22 +02:00
Lyra
98c2530984 [HDWallpapers] Adapt to some website changes (Fixes #1088). Add wallpapers to enclosures, and select "HD" as the default resolution 2019-04-07 22:02:11 +02:00
sysadminstory
90bf90d167 [BingSearch] Make the bridge compatible with PHP 5.6 (#1084)
* [BingSearch] Make the bridge compatible with PHP 5.6

The use of isset() with an expression is not possible in PHP 5.6. I
fixed it by replacing isset() with "null !== ".
2019-04-07 21:51:48 +02:00
Eugene Molotov
6feda2220e [VkBridge] Add option to hide reposts (#1089) 2019-04-07 21:50:58 +02:00
Lyra
92775abe11 Fix phpcs 2019-04-05 10:59:30 +02:00
Lyra
24cdeabed8 [GithubSearchBridge] Update the bridge to match Github's layout 2019-04-05 10:53:28 +02:00
Roliga
380fdf2e40 [ParameterValidator] Handle missing parameter type (#1057)
* [ParameterValidator] Handle missing parameter type
2019-04-04 22:55:46 +02:00
Tobias Alexander Franke
50c90eb5df [EconomistBridge] Add new bridge (#1067)
* [EconomistBridge] Added new bridge
2019-04-04 22:54:08 +02:00
DJCrashdummy
d9ee9e272e [FDroidBridge] fixed bridge (#1075)
because an additional widget (i guess the language selector) was added to the homepage.
2019-04-04 22:52:59 +02:00
Dreckiger-Dan
4ba0d8bebe Update .gitignore (#1078)
ignore .htaccess .htpasswd
2019-04-04 22:50:33 +02:00
somini
c9b0cd1315 ComboiosDePortugalBridge: HACK: Encode the URL (#1074)
This seems like a weird bug somewhere.

Either the HTML parser should return the valid page, or the CMS should
not convert the URL first, or the URL validation regex is buggy.
2019-04-04 22:48:25 +02:00
Lyra
2dc0c36e9b Merge branch 'master' of github.com:RSS-Bridge/rss-bridge 2019-04-04 22:46:49 +02:00
Lyra
0aa8858551 [RoadAndTrackBridge] Generate a signature key for every client instead of hardcoding it 2019-04-04 22:45:41 +02:00
Thibault Couraud
966d450d27 [FindACrew] Update bridge according new findacrew.net website (#1080)
* update bridge according new crewbay.com website
2019-04-04 22:44:44 +02:00
sysadminstory
291e8c2a23 [AutoJMBridge] Fix bridge after website change (#1081)
* [AutoJMBridge] Fix bridge after website change

The website was totally reworked, so the bridge had to be reworked too.
The bridge parameters changed, therefore old RSS feed will not work
anymore, but it was impossible to do it in another way.
2019-04-04 22:39:39 +02:00
DnAp
b6943de0ca [BingSearch] Add new bridge (#1046) 2019-03-23 16:40:19 +01:00
Aleś Bułojčyk
b9bbc9bdda [FacebookBridge] Fix decoding of cyrillic letters in group names (#842) 2019-03-23 16:39:09 +01:00
logmanoriginal
835e3b1163 [MozillaBugTrackerBridge] Fix typo 2019-03-23 16:30:15 +01:00
Antoine Turmel
3212156925 [MozillaBugTrackerBridge] New Bridge (#916)
This Bridge is a clone of KernelBugTrackerBridge but for Mozilla Bugzilla. There is some difference in the class used to get the right comments.
2019-03-23 16:27:07 +01:00
Dreckiger-Dan
281eaacaeb [HeiseBridge] Add new bridge (#744) 2019-03-23 16:22:44 +01:00
Xurxo Fresco
18d5ef192c [IvooxBridge] Add new bridge (#597) 2019-03-22 21:33:46 +01:00
logmanoriginal
6293c3d33d [FeedItem] Filter duplicate enclosures 2019-03-21 19:42:44 +01:00
logmanoriginal
835af1faf1 travis: Update build script to test more reasonable configurations
PHP nightly recently got updated to dev-8.x, which is not supported
by any of the test scripts. This makes the test pretty useless and
doesn't help in any way.

Instead, the build script should focus on current versions of PHP,
starting from 5.6 to 7.3 (current stable release).

PHP 7.3 is a reasonable version to use for finding breaking changes
in the test scripts (phpunit especially warns about changes). These
tests can fail, of course.
2019-03-20 19:31:34 +01:00
logmanoriginal
88aae6fd95 core: Apply changes to fix broken Travis builds
Travis-CI recently got updated, which causes existing builds to fail.
For example: https://travis-ci.org/RSS-Bridge/rss-bridge/builds/507568117

Indenting multi-line arguments of functions fixes it.
2019-03-20 19:23:22 +01:00
Nemo
684558e276 [StockFilingsBridge] Add new bridge (#1011) 2019-03-17 20:40:21 +01:00
logmanoriginal
d7094b7feb [Configuration] Bump version to dev.2019-03-17 2019-03-17 20:31:17 +01:00
118 changed files with 6607 additions and 2578 deletions

View File

@@ -1,7 +1,14 @@
.git
.gitattributes
.github/*
.travis.yml
cache/*
CONTRIBUTING.md
DEBUG
Dockerfile
whitelist.txt
phpcompatibility.xml
phpcs.xml
CONTRIBUTING.md
phpcs.xml
scalingo.json
tests/*
whitelist.txt

77
.gitattributes vendored
View File

@@ -10,27 +10,60 @@
*.dbproj merge=union
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
# Ignore files in git archive (i.e. GitHub release builds)
Dockerfile export-ignore
.travis.yml export-ignore
.github/ export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.dockerignore export-ignore
scalingo.json export-ignore
phpunit.xml export-ignore
phpcs.xml export-ignore
phpcompatibility.xml export-ignore
tests/ export-ignore
cache/.gitkeep export-ignore
## Docker
Dockerfile export-ignore
.dockerignore export-ignore
## Travis
.travis.yml export-ignore
## GitHub
.github/ export-ignore
## Git
.gitattributes export-ignore
.gitignore export-ignore
## Scalingo
scalingo.json export-ignore
## RSS-Bridge
phpunit.xml export-ignore
phpcs.xml export-ignore
phpcompatibility.xml export-ignore
tests/ export-ignore
cache/.gitkeep export-ignore
bridges/DemoBridge.php export-ignore
bridges/FeedExpanderExampleBridge.php export-ignore
## Composer
#
# Keep the following lines commented out. Heroku does
# not function if the composer files are ignored during
# export. For more information see
# https://github.com/rss-bridge/rss-bridge/issues/1165
#
# composer.json export-ignore
# composer.lock export-ignore
## Heroku
#
# Keep the following line commented out. Heroku does
# not function if app.json is ignored during export.
# For more information see
# https://github.com/rss-bridge/rss-bridge/issues/1165
#
# app.json export-ignore

View File

@@ -1,6 +1,9 @@
---
name: Bridge request template
name: Bridge request
about: Use this template for requesting a new bridge
title: Bridge request for ...
labels: Bridge-Request
assignees: ''
---

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: Bug-Report
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: Feature-Request
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

7
.gitignore vendored
View File

@@ -236,3 +236,10 @@ config.ini.php
#Builder
.buildconfig
#Auth
.htaccess
.htpasswd
#Crawler
robots.txt

View File

@@ -4,41 +4,43 @@ language: php
install:
- composer global require dealerdirect/phpcodesniffer-composer-installer;
- composer global require phpcompatibility/php-compatibility;
# Use PHPUnit 6 for unit tests (stable), requires PHP 7
- if [[ $TRAVIS_PHP_VERSION == "7.0" ]]; then
composer global require phpunit/phpunit ^6;
fi
# Use latest PHPUnit on nightly to detect breaking changes
- if [[ $TRAVIS_PHP_VERSION == "nightly" ]]; then
composer global require phpunit/phpunit;
- if [[ "$PHPUNIT" ]]; then
composer global require phpunit/phpunit ^$PHPUNIT;
fi
script:
- phpenv rehash
# Run PHP_CodeSniffer on all versions
- ~/.config/composer/vendor/bin/phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
# Check PHP compatibility for the lowest supported version
- if [[ $TRAVIS_PHP_VERSION == "5.6" ]]; then
# Check PHP compatibility for the lowest and highest supported version
- if [[ $TRAVIS_PHP_VERSION == "5.6" || $TRAVIS_PHP_VERSION == "7.3" ]]; then
~/.config/composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --extensions=php -p;
fi
# Run unit tests (stable)
- if [[ $TRAVIS_PHP_VERSION == "7.0" ]]; then
phpunit --configuration=phpunit.xml --include-path=lib/;
fi
# Run unit tests (latest/nightly)
# Check PHP compatibility for all versions, starting at the lowest supported version in order to detect breaking changes
- if [[ $TRAVIS_PHP_VERSION == "nightly" ]]; then
phpunit --configuration=phpunit.xml --include-path=lib/;
~/.config/composer/vendor/bin/phpcs . --standard=PHPCompatibility --extensions=php -p --runtime-set testVersion 5.6-;
# Run unit tests on highest major version
- if [[ ${TRAVIS_PHP_VERSION:0:1} == "7" ]]; then
~/.config/composer/vendor/bin/phpunit --configuration=phpunit.xml --include-path=lib/;
fi
php:
- 7.3
env:
- PHPUNIT=6
- PHPUNIT=7
- PHPUNIT=8
matrix:
fast_finish: true
include:
- php: 5.6
env: PHPUNIT=
- php: 7.0
- php: nightly
- php: 7.1
- php: 7.2
allow_failures:
- php: nightly
- php: 7.3
env: PHPUNIT=7
- php: 7.3
env: PHPUNIT=8

View File

@@ -1,5 +1,11 @@
FROM ulsmith/alpine-apache-php7
FROM php:7-apache
COPY ./ /app/public/
ENV APACHE_DOCUMENT_ROOT=/app
RUN chown -R apache:root /app/public
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
&& apt-get --yes update && apt-get --yes install libxml2-dev \
&& docker-php-ext-install -j$(nproc) simplexml \
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
COPY --chown=www-data:www-data ./ /app/

207
README.md
View File

@@ -1,6 +1,6 @@
rss-bridge
![RSS-Bridge](static/logo_600px.png)
===
[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![Debian Release](https://img.shields.io/badge/dynamic/json.svg?label=debian%20release&url=https%3A%2F%2Fsources.debian.org%2Fapi%2Fsrc%2Frss-bridge%2F&query=%24.versions%5B0%5D.version&colorB=blue)](https://tracker.debian.org/pkg/rss-bridge) [![Guix Release](https://img.shields.io/badge/guix%20release-unknown-light--gray.svg)](https://www.gnu.org/software/guix/packages/R/) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge) [![Docker Build Status](https://img.shields.io/docker/build/rssbridge/rss-bridge.svg)](https://hub.docker.com/r/rssbridge/rss-bridge/)
[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg?logo=github)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![Debian Release](https://img.shields.io/badge/dynamic/json.svg?logo=debian&label=debian%20release&url=https%3A%2F%2Fsources.debian.org%2Fapi%2Fsrc%2Frss-bridge%2F&query=%24.versions%5B0%5D.version&colorB=blue)](https://tracker.debian.org/pkg/rss-bridge) [![Guix Release](https://img.shields.io/badge/guix%20release-unknown-blue.svg)](https://www.gnu.org/software/guix/packages/R/) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge) [![Docker Build Status](https://img.shields.io/docker/build/rssbridge/rss-bridge.svg?logo=docker)](https://hub.docker.com/r/rssbridge/rss-bridge/)
RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites which don't have one. It can be used on webservers or as stand alone application in CLI mode.
@@ -15,7 +15,6 @@ Supported sites/pages (examples)
* `DuckDuckGo`: Most recent results from [DuckDuckGo.com](https://duckduckgo.com/)
* `Facebook` : Returns the latest posts on a page or profile on [Facebook](https://facebook.com/)
* `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr
* `GooglePlus` : Most recent posts of user timeline
* `GoogleSearch` : Most recent results from Google Search
* `Identi.ca` : Identica user timeline (Should be compatible with other Pump.io instances)
* `Instagram`: Most recent photos from an Instagram user
@@ -85,7 +84,7 @@ Deploy
Thanks to the community, hosting your own instance of RSS-Bridge is as easy as clicking a button!
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/sebsauvage/rss-bridge)
[![Deploy to Docker Cloud](https://files.cloud.docker.com/images/deploy-to-dockercloud.svg)](https://cloud.docker.com/stack/deploy/?repo=https://github.com/rss-bridge/rss-bridge)
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
Getting involved
===
@@ -111,101 +110,111 @@ Use this script to generate the list automatically (using the GitHub API):
https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
-->
* [16mhz](https://github.com/16mhz)
* [adamchainz](https://github.com/adamchainz)
* [Ahiles3005](https://github.com/Ahiles3005)
* [Albirew](https://github.com/Albirew)
* [aledeg](https://github.com/aledeg)
* [alexAubin](https://github.com/alexAubin)
* [AmauryCarrade](https://github.com/AmauryCarrade)
* [AntoineTurmel](https://github.com/AntoineTurmel)
* [ArthurHoaro](https://github.com/ArthurHoaro)
* [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42)
* [az5he6ch](https://github.com/az5he6ch)
* [b1nj](https://github.com/b1nj)
* [benasse](https://github.com/benasse)
* [captn3m0](https://github.com/captn3m0)
* [chemel](https://github.com/chemel)
* [ckiw](https://github.com/ckiw)
* [cnlpete](https://github.com/cnlpete)
* [corenting](https://github.com/corenting)
* [couraudt](https://github.com/couraudt)
* [da2x](https://github.com/da2x)
* [Daiyousei](https://github.com/Daiyousei)
* [disk0x](https://github.com/disk0x)
* [Djuuu](https://github.com/Djuuu)
* [Draeli](https://github.com/Draeli)
* [em92](https://github.com/em92)
* [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM)
* [fluffy-critter](https://github.com/fluffy-critter)
* [Frenzie](https://github.com/Frenzie)
* [fulmeek](https://github.com/fulmeek)
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [Glandos](https://github.com/Glandos)
* [GregThib](https://github.com/GregThib)
* [griffaurel](https://github.com/griffaurel)
* [Grummfy](https://github.com/Grummfy)
* [hunhejj](https://github.com/hunhejj)
* [j0k3r](https://github.com/j0k3r)
* [JackNUMBER](https://github.com/JackNUMBER)
* [jdigilio](https://github.com/jdigilio)
* [JeremyRand](https://github.com/JeremyRand)
* [Jocker666z](https://github.com/Jocker666z)
* [klimplant](https://github.com/klimplant)
* [kranack](https://github.com/kranack)
* [kraoc](https://github.com/kraoc)
* [l1n](https://github.com/l1n)
* [laBecasse](https://github.com/laBecasse)
* [lagaisse](https://github.com/lagaisse)
* [lalannev](https://github.com/lalannev)
* [ldidry](https://github.com/ldidry)
* [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal)
* [lorenzos](https://github.com/lorenzos)
* [m0zes](https://github.com/m0zes)
* [matthewseal](https://github.com/matthewseal)
* [mcbyte-it](https://github.com/mcbyte-it)
* [mdemoss](https://github.com/mdemoss)
* [melangue](https://github.com/melangue)
* [metaMMA](https://github.com/metaMMA)
* [mickael-bertrand](https://github.com/mickael-bertrand)
* [mitsukarenai](https://github.com/mitsukarenai)
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
* [mr-flibble](https://github.com/mr-flibble)
* [mro](https://github.com/mro)
* [mxmehl](https://github.com/mxmehl)
* [nel50n](https://github.com/nel50n)
* [niawag](https://github.com/niawag)
* [Nono-m0le](https://github.com/Nono-m0le)
* [ORelio](https://github.com/ORelio)
* [PaulVayssiere](https://github.com/PaulVayssiere)
* [pellaeon](https://github.com/pellaeon)
* [Piranhaplant](https://github.com/Piranhaplant)
* [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
* [pitchoule](https://github.com/pitchoule)
* [pmaziere](https://github.com/pmaziere)
* [prysme01](https://github.com/prysme01)
* [quentinus95](https://github.com/quentinus95)
* [regisenguehard](https://github.com/regisenguehard)
* [Riduidel](https://github.com/Riduidel)
* [rogerdc](https://github.com/rogerdc)
* [Roliga](https://github.com/Roliga)
* [sebsauvage](https://github.com/sebsauvage)
* [somini](https://github.com/somini)
* [squeek502](https://github.com/squeek502)
* [Strubbl](https://github.com/Strubbl)
* [sublimz](https://github.com/sublimz)
* [sysadminstory](https://github.com/sysadminstory)
* [tameroski](https://github.com/tameroski)
* [teromene](https://github.com/teromene)
* [TheRadialActive](https://github.com/TheRadialActive)
* [triatic](https://github.com/triatic)
* [WalterBarrett](https://github.com/WalterBarrett)
* [wtuuju](https://github.com/wtuuju)
* [yardenac](https://github.com/yardenac)
* [ZeNairolf](https://github.com/ZeNairolf)
* [16mhz](https://github.com/16mhz)
* [adamchainz](https://github.com/adamchainz)
* [Ahiles3005](https://github.com/Ahiles3005)
* [Albirew](https://github.com/Albirew)
* [aledeg](https://github.com/aledeg)
* [alex73](https://github.com/alex73)
* [alexAubin](https://github.com/alexAubin)
* [AmauryCarrade](https://github.com/AmauryCarrade)
* [ArthurHoaro](https://github.com/ArthurHoaro)
* [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42)
* [az5he6ch](https://github.com/az5he6ch)
* [azdkj532](https://github.com/azdkj532)
* [b1nj](https://github.com/b1nj)
* [benasse](https://github.com/benasse)
* [captn3m0](https://github.com/captn3m0)
* [chemel](https://github.com/chemel)
* [ckiw](https://github.com/ckiw)
* [cnlpete](https://github.com/cnlpete)
* [corenting](https://github.com/corenting)
* [couraudt](https://github.com/couraudt)
* [da2x](https://github.com/da2x)
* [Daiyousei](https://github.com/Daiyousei)
* [disk0x](https://github.com/disk0x)
* [DJCrashdummy](https://github.com/DJCrashdummy)
* [Djuuu](https://github.com/Djuuu)
* [DnAp](https://github.com/DnAp)
* [Draeli](https://github.com/Draeli)
* [Dreckiger-Dan](https://github.com/Dreckiger-Dan)
* [em92](https://github.com/em92)
* [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM)
* [fluffy-critter](https://github.com/fluffy-critter)
* [Frenzie](https://github.com/Frenzie)
* [fulmeek](https://github.com/fulmeek)
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [Glandos](https://github.com/Glandos)
* [GregThib](https://github.com/GregThib)
* [griffaurel](https://github.com/griffaurel)
* [Grummfy](https://github.com/Grummfy)
* [hunhejj](https://github.com/hunhejj)
* [husim0](https://github.com/husim0)
* [j0k3r](https://github.com/j0k3r)
* [JackNUMBER](https://github.com/JackNUMBER)
* [jdigilio](https://github.com/jdigilio)
* [JeremyRand](https://github.com/JeremyRand)
* [Jocker666z](https://github.com/Jocker666z)
* [killruana](https://github.com/killruana)
* [klimplant](https://github.com/klimplant)
* [kranack](https://github.com/kranack)
* [kraoc](https://github.com/kraoc)
* [l1n](https://github.com/l1n)
* [laBecasse](https://github.com/laBecasse)
* [lagaisse](https://github.com/lagaisse)
* [lalannev](https://github.com/lalannev)
* [ldidry](https://github.com/ldidry)
* [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal)
* [lorenzos](https://github.com/lorenzos)
* [m0zes](https://github.com/m0zes)
* [matthewseal](https://github.com/matthewseal)
* [mcbyte-it](https://github.com/mcbyte-it)
* [mdemoss](https://github.com/mdemoss)
* [melangue](https://github.com/melangue)
* [metaMMA](https://github.com/metaMMA)
* [mitsukarenai](https://github.com/mitsukarenai)
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
* [mr-flibble](https://github.com/mr-flibble)
* [mro](https://github.com/mro)
* [mxmehl](https://github.com/mxmehl)
* [nel50n](https://github.com/nel50n)
* [niawag](https://github.com/niawag)
* [Nono-m0le](https://github.com/Nono-m0le)
* [ObsidianWitch](https://github.com/ObsidianWitch)
* [ORelio](https://github.com/ORelio)
* [PaulVayssiere](https://github.com/PaulVayssiere)
* [pellaeon](https://github.com/pellaeon)
* [Piranhaplant](https://github.com/Piranhaplant)
* [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
* [pitchoule](https://github.com/pitchoule)
* [pmaziere](https://github.com/pmaziere)
* [Pofilo](https://github.com/Pofilo)
* [prysme01](https://github.com/prysme01)
* [quentinus95](https://github.com/quentinus95)
* [regisenguehard](https://github.com/regisenguehard)
* [Riduidel](https://github.com/Riduidel)
* [rogerdc](https://github.com/rogerdc)
* [Roliga](https://github.com/Roliga)
* [sebsauvage](https://github.com/sebsauvage)
* [somini](https://github.com/somini)
* [squeek502](https://github.com/squeek502)
* [Strubbl](https://github.com/Strubbl)
* [sublimz](https://github.com/sublimz)
* [sysadminstory](https://github.com/sysadminstory)
* [tameroski](https://github.com/tameroski)
* [teromene](https://github.com/teromene)
* [thefranke](https://github.com/thefranke)
* [TheRadialActive](https://github.com/TheRadialActive)
* [triatic](https://github.com/triatic)
* [VerifiedJoseph](https://github.com/VerifiedJoseph)
* [WalterBarrett](https://github.com/WalterBarrett)
* [wtuuju](https://github.com/wtuuju)
* [xurxof](https://github.com/xurxof)
* [yardenac](https://github.com/yardenac)
* [ZeNairolf](https://github.com/ZeNairolf)
Licenses
===

View File

@@ -19,13 +19,16 @@ class DetectAction extends ActionAbstract {
$format = $this->userData['format']
or returnClientError('You must specify a format!');
foreach(Bridge::getBridgeNames() as $bridgeName) {
$bridgeFac = new \BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
if(!Bridge::isWhitelisted($bridgeName)) {
foreach($bridgeFac->getBridgeNames() as $bridgeName) {
if(!$bridgeFac->isWhitelisted($bridgeName)) {
continue;
}
$bridge = Bridge::create($bridgeName);
$bridge = $bridgeFac->create($bridgeName);
if($bridge === false) {
continue;

View File

@@ -18,20 +18,17 @@ class DisplayAction extends ActionAbstract {
$format = $this->userData['format']
or returnClientError('You must specify a format!');
// DEPRECATED: 'nameFormat' scheme is replaced by 'name' in format parameter values
// this is to keep compatibility until futher complete removal
if(($pos = strpos($format, 'Format')) === (strlen($format) - strlen('Format'))) {
$format = substr($format, 0, $pos);
}
$bridgeFac = new \BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
// whitelist control
if(!Bridge::isWhitelisted($bridge)) {
if(!$bridgeFac->isWhitelisted($bridge)) {
throw new \Exception('This bridge is not whitelisted', 401);
die;
}
// Data retrieval
$bridge = Bridge::create($bridge);
$bridge = $bridgeFac->create($bridge);
$noproxy = array_key_exists('_noproxy', $this->userData)
&& filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN);
@@ -85,10 +82,12 @@ class DisplayAction extends ActionAbstract {
);
// Initialize cache
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$cache->setPath(PATH_CACHE);
$cacheFac = new CacheFactory();
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope('');
$cache->purgeCache(86400); // 24 hours
$cache->setParameters($cache_params);
$cache->setKey($cache_params);
$items = array();
$infos = array();
@@ -216,7 +215,9 @@ class DisplayAction extends ActionAbstract {
// Data transformation
try {
$format = Format::create($format);
$formatFac = new FormatFactory();
$formatFac->setWorkingDir(PATH_LIB_FORMATS);
$format = $formatFac->create($format);
$format->setItems($items);
$format->setExtraInfos($infos);
$format->setLastModified($cache->getTime());

View File

@@ -17,9 +17,12 @@ class ListAction extends ActionAbstract {
$list->bridges = array();
$list->total = 0;
foreach(Bridge::getBridgeNames() as $bridgeName) {
$bridgeFac = new \BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
$bridge = Bridge::create($bridgeName);
foreach($bridgeFac->getBridgeNames() as $bridgeName) {
$bridge = $bridgeFac->create($bridgeName);
if($bridge === false) { // Broken bridge, show as inactive
@@ -31,7 +34,7 @@ class ListAction extends ActionAbstract {
}
$status = Bridge::isWhitelisted($bridgeName) ? 'active' : 'inactive';
$status = $bridgeFac->isWhitelisted($bridgeName) ? 'active' : 'inactive';
$list->bridges[$bridgeName] = array(
'status' => $status,

8
app.json Normal file
View File

@@ -0,0 +1,8 @@
{
"service": "Heroku",
"name": "RSS-Bridge",
"description": "RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites which don't have one.",
"repository": "https://github.com/RSS-Bridge/rss-bridge",
"keywords": ["php", "rss-bridge", "rss"]
}

121
bridges/AO3Bridge.php Normal file
View File

@@ -0,0 +1,121 @@
<?php
class AO3Bridge extends BridgeAbstract {
const NAME = 'AO3';
const URI = 'https://archiveofourown.org/';
const CACHE_TIMEOUT = 1800;
const DESCRIPTION = 'Returns works or chapters from Archive of Our Own';
const MAINTAINER = 'Obsidienne';
const PARAMETERS = array(
'List' => array(
'url' => array(
'name' => 'url',
'required' => true,
// Example: F/F tag, complete works only
'exampleValue' => self::URI
. 'works?work_search[complete]=T&tag_id=F*s*F',
),
),
'Bookmarks' => array(
'user' => array(
'name' => 'user',
'required' => true,
// Example: Nyaaru's bookmarks
'exampleValue' => 'Nyaaru',
),
),
'Work' => array(
'id' => array(
'name' => 'id',
'required' => true,
// Example: latest chapters from A Better Past by LysSerris
'exampleValue' => '18181853',
),
)
);
// Feed for lists of works (e.g. recent works, search results, filtered tags,
// bookmarks, series, collections).
private function collectList($url) {
$html = getSimpleHTMLDOM($url)
or returnServerError('could not request AO3');
$html = defaultLinkTo($html, self::URI);
foreach($html->find('.index.group > li') as $element) {
$item = array();
$title = $element->find('div h4 a', 0);
if (!isset($title)) continue; // discard deleted works
$item['title'] = $title->plaintext;
$item['content'] = $element;
$item['uri'] = $title->href;
$strdate = $element->find('div p.datetime', 0)->plaintext;
$item['timestamp'] = strtotime($strdate);
$chapters = $element->find('dl dd.chapters', 0);
// bookmarked series and external works do not have a chapters count
$chapters = (isset($chapters) ? $chapters->plaintext : 0);
$item['uid'] = $item['uri'] . "/$strdate/$chapters";
$this->items[] = $item;
}
}
// Feed for recent chapters of a specific work.
private function collectWork($id) {
$url = self::URI . "/works/$id/navigate";
$html = getSimpleHTMLDOM($url)
or returnServerError('could not request AO3');
$html = defaultLinkTo($html, self::URI);
$this->title = $html->find('h2 a', 0)->plaintext;
foreach($html->find('ol.index.group > li') as $element) {
$item = array();
$item['title'] = $element->find('a', 0)->plaintext;
$item['content'] = $element;
$item['uri'] = $element->find('a', 0)->href;
$strdate = $element->find('span.datetime', 0)->plaintext;
$strdate = str_replace('(', '', $strdate);
$strdate = str_replace(')', '', $strdate);
$item['timestamp'] = strtotime($strdate);
$item['uid'] = $item['uri'] . "/$strdate";
$this->items[] = $item;
}
$this->items = array_reverse($this->items);
}
public function collectData() {
switch($this->queriedContext) {
case 'Bookmarks':
$user = $this->getInput('user');
$this->title = $user;
$url = self::URI
. '/users/' . $user
. '/bookmarks?bookmark_search[sort_column]=bookmarkable_date';
return $this->collectList($url);
case 'List': return $this->collectList(
$this->getInput('url')
);
case 'Work': return $this->collectWork(
$this->getInput('id')
);
}
}
public function getName() {
$name = parent::getName() . " $this->queriedContext";
if (isset($this->title)) $name .= " - $this->title";
return $name;
}
public function getIcon() {
return self::URI . '/favicon.ico';
}
}

View File

@@ -0,0 +1,93 @@
<?php
class ArtStationBridge extends BridgeAbstract {
const NAME = 'ArtStation';
const URI = 'https://www.artstation.com';
const DESCRIPTION = 'Fetches the latest ten artworks from a search query on ArtStation.';
const MAINTAINER = 'thefranke';
const CACHE_TIMEOUT = 3600; // 1h
const PARAMETERS = array(
'Search Query' => array(
'q' => array(
'name' => 'Search term',
'required' => true
)
)
);
public function getIcon() {
return 'https://www.artstation.com/assets/favicon-58653022bc38c1905ac7aa1b10bffa6b.ico';
}
public function getName() {
return self::NAME . ': ' . $this->getInput('q');
}
private function fetchSearch($searchQuery) {
$data = '{"query":"' . $searchQuery . '","page":1,"per_page":50,"sorting":"date",';
$data .= '"pro_first":"1","filters":[],"additional_fields":[]}';
$header = array(
'Content-Type: application/json',
'Accept: application/json'
);
$opts = array(
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data,
CURLOPT_RETURNTRANSFER => true
);
$jsonSearchURL = self::URI . '/api/v2/search/projects.json';
$jsonSearchStr = getContents($jsonSearchURL, $header, $opts)
or returnServerError('Could not fetch JSON for search query.');
return json_decode($jsonSearchStr);
}
private function fetchProject($hashID) {
$jsonProjectURL = self::URI . '/projects/' . $hashID . '.json';
$jsonProjectStr = getContents($jsonProjectURL)
or returnServerError('Could not fetch JSON for project.');
return json_decode($jsonProjectStr);
}
public function collectData() {
$searchTerm = $this->getInput('q');
$jsonQuery = $this->fetchSearch($searchTerm);
foreach($jsonQuery->data as $media) {
// get detailed info about media item
$jsonProject = $this->fetchProject($media->hash_id);
// create item
$item = array();
$item['title'] = $media->title;
$item['uri'] = $media->url;
$item['timestamp'] = strtotime($jsonProject->published_at);
$item['author'] = $media->user->full_name;
$item['categories'] = implode(',', $jsonProject->tags);
$item['content'] = '<a href="'
. $media->url
. '"><img style="max-width: 100%" src="'
. $jsonProject->cover_url
. '"></a><p>'
. $jsonProject->description
. '</p>';
$numAssets = count($jsonProject->assets);
if ($numAssets > 1)
$item['content'] .= '<p><a href="'
. $media->url
. '">Project contains '
. ($numAssets - 1)
. ' more item(s).</a></p>';
$this->items[] = $item;
if (count($this->items) >= 10)
break;
}
}
}

View File

@@ -91,7 +91,8 @@ class Arte7Bridge extends BridgeAbstract {
'Authorization: Bearer ' . self::API_TOKEN
);
$input = getContents($url, $header) or die('Could not request ARTE.');
$input = getContents($url, $header)
or returnServerError('Could not request ARTE.');
$input_json = json_decode($input, true);
foreach($input_json['videos'] as $element) {

View File

@@ -3,63 +3,195 @@
class AutoJMBridge extends BridgeAbstract {
const NAME = 'AutoJM';
const URI = 'http://www.autojm.fr/';
const URI = 'https://www.autojm.fr/';
const DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
'Afficher les offres de véhicules disponible en fonction des critères du site AutoJM' => array(
'url' => array(
'name' => 'URL de la recherche',
'name' => 'URL du modèle',
'type' => 'text',
'required' => true,
'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/',
'exampleValue' => 'gammes/index/398?order_by=finition_asc&energie[]=3&transmission[]=2&dispo=all'
'exampleValue' => 'achat-voitures-neuves-peugeot-nouvelle-308-5p'
),
'isDispo' => array(
'name' => 'Disponibilité',
'type' => 'list',
'values' => array(
'-' => '',
'En stock' => 1,
'Sur commande' => 0
),
'title' => 'Critère de disponibilité'
),
'energy' => array(
'name' => 'Carburant',
'type' => 'list',
'values' => array(
'-' => '',
'Diesel' => 1,
'Essence' => 3,
'Hybride' => 5
),
'title' => 'Carburant'
),
'transmission' => array(
'name' => 'Transmission',
'type' => 'list',
'values' => array(
'-' => '',
'Automatique' => 1,
'Manuelle' => 2
),
'title' => 'Transmission'
),
'priceMin' => array(
'name' => 'Prix minimum',
'type' => 'number',
'required' => false,
'title' => 'Prix minimum du véhicule',
'exampleValue' => '10000',
'defaultValue' => '0'
),
'priceMax' => array(
'name' => 'Prix maximum',
'type' => 'number',
'required' => false,
'title' => 'Prix maximum du véhicule',
'exampleValue' => '15000',
'defaultValue' => '150000'
)
)
);
const CACHE_TIMEOUT = 3600;
public function getIcon() {
return self::URI . 'assets/images/favicon.ico';
return self::URI . 'favicon.ico';
}
public function collectData() {
$html = getSimpleHTMLDOM(self::URI . $this->getInput('url'))
or returnServerError('Could not request AutoJM.');
$list = $html->find('div[class*=ligne_modele]');
foreach($list as $element) {
$image = $element->find('img[class=width-100]', 0)->src;
$serie = $element->find('div[class=serie]', 0)->find('span', 0)->plaintext;
$url = $element->find('div[class=serie]', 0)->find('a[class=btn_ligne color-black]', 0)->href;
if($element->find('div[class*=hasStock-info]', 0) != null) {
$dispo = 'Disponible';
} else {
$dispo = 'Sur commande';
}
$carburant = str_replace('dispo |', '', $element->find('div[class=carburant]', 0)->plaintext);
$transmission = $element->find('div[class*=bv]', 0)->plaintext;
$places = $element->find('div[class*=places]', 0)->plaintext;
$portes = $element->find('div[class*=nb_portes]', 0)->plaintext;
$carosserie = $element->find('div[class*=coloris]', 0)->plaintext;
$remise = $element->find('div[class*=remise]', 0)->plaintext;
$prix = $element->find('div[class*=prixjm]', 0)->plaintext;
$item = array();
$item['uri'] = $url;
$item['title'] = $serie;
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />' . $serie . '</p>';
$item['content'] .= '<ul><li>Disponibilité : ' . $dispo . '</li>';
$item['content'] .= '<li>Carburant : ' . $carburant . '</li>';
$item['content'] .= '<li>Transmission : ' . $transmission . '</li>';
$item['content'] .= '<li>Nombre de places : ' . $places . '</li>';
$item['content'] .= '<li>Nombre de portes : ' . $portes . '</li>';
$item['content'] .= '<li>Série : ' . $serie . '</li>';
$item['content'] .= '<li>Carosserie : ' . $carosserie . '</li>';
$item['content'] .= '<li>Remise : ' . $remise . '</li>';
$item['content'] .= '<li>Prix : ' . $prix . '</li></ul>';
$this->items[] = $item;
public function getName() {
switch($this->queriedContext) {
case 'Afficher les offres de véhicules disponible en fonction des critères du site AutoJM':
$html = getSimpleHTMLDOMCached(self::URI . $this->getInput('url'), 86400);
$name = html_entity_decode($html->find('title', 0)->plaintext);
return $name;
break;
default:
return parent::getName();
}
}
public function collectData() {
$model_url = self::URI . $this->getInput('url');
// Get the session cookies and the form token
$this->getInitialParameters($model_url);
// Build the form
$post_data = array(
'form[isDispo]' => $this->getInput('isDispo'),
'form[energy]' => $this->getInput('energy'),
'form[transmission]' => $this->getInput('transmission'),
'form[priceMin]' => $this->getInput('priceMin'),
'form[priceMin]' => $this->getInput('priceMin'),
'form[_token]' => $this->token
);
// Set the Form request content type
$header = array(
'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
);
// Set the curl options (POST query and content, and session cookies
$curl_opts = array(
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($post_data),
CURLOPT_COOKIE => $this->cookies
);
// Get the JSON content of the form
$json = getContents($model_url, $header, $curl_opts)
or returnServerError('Could not request AutoJM.');
// Extract the HTML content from the JSON result
$data = json_decode($json);
$html = str_get_html($data->content);
// Go through every finisha of the model
$list = $html->find('h2');
foreach ($list as $finish) {
$finish_name = $finish->plaintext;
$motorizations = $finish->next_sibling()->find('li');
foreach ($motorizations as $element) {
$image = $element->find('div[class=block-product-image]', 0)->{'data-ga-banner'};
$serie = $element->find('span[class=model]', 0)->plaintext;
$url = self::URI . substr($element->find('a', 0)->href, 1);
if ($element->find('span[class*=block-product-nbModel]', 0) != null) {
$availability = 'En Stock';
} else {
$availability = 'Sur commande';
}
$discount_html = $element->find('span[class*=tag--promo]', 0);
if ($discount_html != null) {
$discount = $discount_html->plaintext;
} else {
$discount = 'inconnue';
}
$price = $element->find('span[class=price red h1]', 0)->plaintext;
$item = array();
$item['title'] = $finish_name . ' ' . $serie;
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />'
. $finish_name . ' ' . $serie . '</p>';
$item['content'] .= '<ul><li>Disponibilité : ' . $availability . '</li>';
$item['content'] .= '<li>Série : ' . $serie . '</li>';
$item['content'] .= '<li>Remise : ' . $discount . '</li>';
$item['content'] .= '<li>Prix : ' . $price . '</li></ul>';
// Add a fictionnal anchor to the RSS element URL, based on the item content ;
// As the URL could be identical even if the price change, some RSS reader will not show those offers as new items
$item['uri'] = $url . '#' . md5($item['content']);
$this->items[] = $item;
}
}
}
/**
* Gets the session cookie and the form token
*
* @param string $pageURL The URL from which to get the values
*/
private function getInitialParameters($pageURL) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $pageURL);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = curl_exec($ch);
// Separate the response header and the content
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header = substr($data, 0, $headerSize);
$content = substr($data, $headerSize);
curl_close($ch);
// Extract the cookies from the headers
$cookies = '';
$http_response_header = explode("\r\n", $header);
foreach ($http_response_header as $hdr) {
if (strpos($hdr, 'Set-Cookie') !== false) {
$cLine = explode(':', $hdr)[1];
$cLine = explode(';', $cLine)[0];
$cookies .= ';' . $cLine;
}
}
$this->cookies = trim(substr($cookies, 1));
// Get the token from the content
$html = str_get_html($content);
$token = $html->find('input[type=hidden][id=form__token]', 0);
$this->token = $token->value;
}
}

View File

@@ -55,9 +55,7 @@ class BAEBridge extends BridgeAbstract {
$content .= '<hr>';
$content .= $htmlDetail->find('section', 0)->innertext;
$content = str_replace('src="/', 'src="' . parent::getURI() . '/', $content);
$content = str_replace('href="/', 'href="' . parent::getURI() . '/', $content);
$item['content'] = $content;
$item['content'] = defaultLinkTo($content, parent::getURI());
$image = $htmlDetail->find('#zoom', 0);
if ($image) {
$item['enclosures'] = array(parent::getURI() . $image->getAttribute('src'));

435
bridges/BadDragonBridge.php Normal file
View File

@@ -0,0 +1,435 @@
<?php
class BadDragonBridge extends BridgeAbstract {
const NAME = 'Bad Dragon Bridge';
const URI = 'https://bad-dragon.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Returns sales or new clearance items';
const MAINTAINER = 'Roliga';
const PARAMETERS = array(
'Sales' => array(
),
'Clearance' => array(
'ready_made' => array(
'name' => 'Ready Made',
'type' => 'checkbox'
),
'flop' => array(
'name' => 'Flops',
'type' => 'checkbox'
),
'skus' => array(
'name' => 'Products',
'exampleValue' => 'chanceflared, crackers',
'title' => 'Comma separated list of product SKUs'
),
'onesize' => array(
'name' => 'One-Size',
'type' => 'checkbox'
),
'mini' => array(
'name' => 'Mini',
'type' => 'checkbox'
),
'small' => array(
'name' => 'Small',
'type' => 'checkbox'
),
'medium' => array(
'name' => 'Medium',
'type' => 'checkbox'
),
'large' => array(
'name' => 'Large',
'type' => 'checkbox'
),
'extralarge' => array(
'name' => 'Extra Large',
'type' => 'checkbox'
),
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'All' => 'all',
'Accessories' => 'accessories',
'Merchandise' => 'merchandise',
'Dildos' => 'insertable',
'Masturbators' => 'penetrable',
'Packers' => 'packer',
'Lil\' Squirts' => 'shooter',
'Lil\' Vibes' => 'vibrator',
'Wearables' => 'wearable'
),
'defaultValue' => 'all',
),
'soft' => array(
'name' => 'Soft Firmness',
'type' => 'checkbox'
),
'med_firm' => array(
'name' => 'Medium Firmness',
'type' => 'checkbox'
),
'firm' => array(
'name' => 'Firm',
'type' => 'checkbox'
),
'split' => array(
'name' => 'Split Firmness',
'type' => 'checkbox'
),
'maxprice' => array(
'name' => 'Max Price',
'type' => 'number',
'required' => true,
'defaultValue' => 300
),
'minprice' => array(
'name' => 'Min Price',
'type' => 'number',
'defaultValue' => 0
),
'cumtube' => array(
'name' => 'Cumtube',
'type' => 'checkbox'
),
'suctionCup' => array(
'name' => 'Suction Cup',
'type' => 'checkbox'
),
'noAccessories' => array(
'name' => 'No Accessories',
'type' => 'checkbox'
)
)
);
/*
* This sets index $strFrom (or $strTo if set) in $outArr to 'on' if
* $inArr[$param] contains $strFrom.
* It is used for translating BD's shop filter URLs into something we can use.
*
* For the query '?type[]=ready_made&type[]=flop' we would have an array like:
* Array (
* [type] => Array (
* [0] => ready_made
* [1] => flop
* )
* )
* which could be translated into:
* Array (
* [ready_made] => on
* [flop] => on
* )
* */
private function setParam($inArr, &$outArr, $param, $strFrom, $strTo = null) {
if(isset($inArr[$param]) && in_array($strFrom, $inArr[$param])) {
$outArr[($strTo ?: $strFrom)] = 'on';
}
}
public function detectParameters($url) {
$params = array();
// Sale
$regex = '/^(https?:\/\/)?bad-dragon\.com\/sales/';
if(preg_match($regex, $url, $matches) > 0) {
return $params;
}
// Clearance
$regex = '/^(https?:\/\/)?bad-dragon\.com\/shop\/clearance/';
if(preg_match($regex, $url, $matches) > 0) {
parse_str(parse_url($url, PHP_URL_QUERY), $urlParams);
$this->setParam($urlParams, $params, 'type', 'ready_made');
$this->setParam($urlParams, $params, 'type', 'flop');
if(isset($urlParams['skus'])) {
$skus = array();
foreach($urlParams['skus'] as $sku) {
is_string($sku) && $skus[] = $sku;
is_array($sku) && $skus[] = $sku[0];
}
$params['skus'] = implode(',', $skus);
}
$this->setParam($urlParams, $params, 'sizes', 'onesize');
$this->setParam($urlParams, $params, 'sizes', 'mini');
$this->setParam($urlParams, $params, 'sizes', 'small');
$this->setParam($urlParams, $params, 'sizes', 'medium');
$this->setParam($urlParams, $params, 'sizes', 'large');
$this->setParam($urlParams, $params, 'sizes', 'extralarge');
if(isset($urlParams['category'])) {
$params['category'] = strtolower($urlParams['category']);
} else{
$params['category'] = 'all';
}
$this->setParam($urlParams, $params, 'firmnessValues', 'soft');
$this->setParam($urlParams, $params, 'firmnessValues', 'medium', 'med_firm');
$this->setParam($urlParams, $params, 'firmnessValues', 'firm');
$this->setParam($urlParams, $params, 'firmnessValues', 'split');
if(isset($urlParams['price'])) {
isset($urlParams['price']['max'])
&& $params['maxprice'] = $urlParams['price']['max'];
isset($urlParams['price']['min'])
&& $params['minprice'] = $urlParams['price']['min'];
}
isset($urlParams['cumtube'])
&& $urlParams['cumtube'] === '1'
&& $params['cumtube'] = 'on';
isset($urlParams['suctionCup'])
&& $urlParams['suctionCup'] === '1'
&& $params['suctionCup'] = 'on';
isset($urlParams['noAccessories'])
&& $urlParams['noAccessories'] === '1'
&& $params['noAccessories'] = 'on';
return $params;
}
return null;
}
public function getName() {
switch($this->queriedContext) {
case 'Sales':
return 'Bad Dragon Sales';
case 'Clearance':
return 'Bad Dragon Clearance Search';
default:
return parent::getName();
}
}
public function getURI() {
switch($this->queriedContext) {
case 'Sales':
return self::URI . 'sales';
case 'Clearance':
return $this->inputToURL();
default:
return parent::getURI();
}
}
public function collectData() {
switch($this->queriedContext) {
case 'Sales':
$sales = json_decode(getContents(self::URI . 'api/sales'))
or returnServerError('Failed to query BD API');
foreach($sales as $sale) {
$item = array();
$item['title'] = $sale->title;
$item['timestamp'] = strtotime($sale->startDate);
$item['uri'] = $this->getURI() . '/' . $sale->slug;
$contentHTML = '<p><img src="' . $sale->image->url . '"></p>';
if(isset($sale->endDate)) {
$contentHTML .= '<p><b>This promotion ends on '
. gmdate('M j, Y \a\t g:i A T', strtotime($sale->endDate))
. '</b></p>';
} else {
$contentHTML .= '<p><b>This promotion never ends</b></p>';
}
$ul = false;
$content = json_decode($sale->content);
foreach($content->blocks as $block) {
switch($block->type) {
case 'header-one':
$contentHTML .= '<h1>' . $block->text . '</h1>';
break;
case 'header-two':
$contentHTML .= '<h2>' . $block->text . '</h2>';
break;
case 'header-three':
$contentHTML .= '<h3>' . $block->text . '</h3>';
break;
case 'unordered-list-item':
if(!$ul) {
$contentHTML .= '<ul>';
$ul = true;
}
$contentHTML .= '<li>' . $block->text . '</li>';
break;
default:
if($ul) {
$contentHTML .= '</ul>';
$ul = false;
}
$contentHTML .= '<p>' . $block->text . '</p>';
break;
}
}
$item['content'] = $contentHTML;
$this->items[] = $item;
}
break;
case 'Clearance':
$toyData = json_decode(getContents($this->inputToURL(true)))
or returnServerError('Failed to query BD API');
$productList = json_decode(getContents(self::URI
. 'api/inventory-toy/product-list'))
or returnServerError('Failed to query BD API');
foreach($toyData->toys as $toy) {
$item = array();
$item['uri'] = $this->getURI()
. '#'
. $toy->id;
$item['timestamp'] = strtotime($toy->created);
foreach($productList as $product) {
if($product->sku == $toy->sku) {
$item['title'] = $product->name;
break;
}
}
// images
$content = '<p>';
foreach($toy->images as $image) {
$content .= '<a href="'
. $image->fullFilename
. '"><img src="'
. $image->thumbFilename
. '" /></a>';
}
// price
$content .= '</p><p><b>Price:</b> $'
. $toy->price
// size
. '<br /><b>Size:</b> '
. $toy->size
// color
. '<br /><b>Color:</b> '
. $toy->color
// features
. '<br /><b>Features:</b> '
. ($toy->suction_cup ? 'Suction cup' : '')
. ($toy->suction_cup && $toy->cumtube ? ', ' : '')
. ($toy->cumtube ? 'Cumtube' : '')
. ($toy->suction_cup || $toy->cumtube ? '' : 'None');
// firmness
$firmnessTexts = array(
'2' => 'Extra soft',
'3' => 'Soft',
'5' => 'Medium',
'8' => 'Firm'
);
$firmnesses = explode('/', $toy->firmness);
if(count($firmnesses) === 2) {
$content .= '<br /><b>Firmness:</b> '
. $firmnessTexts[$firmnesses[0]]
. ', '
. $firmnessTexts[$firmnesses[1]];
} else{
$content .= '<br /><b>Firmness:</b> '
. $firmnessTexts[$firmnesses[0]];
}
// flop
if($toy->type === 'flop') {
$content .= '<br /><b>Flop reason:</b> '
. $toy->flop_reason;
}
$content .= '</p>';
$item['content'] = $content;
$enclosures = array();
foreach($toy->images as $image) {
$enclosures[] = $image->fullFilename;
}
$item['enclosures'] = $enclosures;
$categories = array();
$categories[] = $toy->sku;
$categories[] = $toy->type;
$categories[] = $toy->size;
if($toy->cumtube) {
$categories[] = 'cumtube';
}
if($toy->suction_cup) {
$categories[] = 'suction_cup';
}
$item['categories'] = $categories;
$this->items[] = $item;
}
break;
}
}
private function inputToURL($api = false) {
$url = self::URI;
$url .= ($api ? 'api/inventory-toys?' : 'shop/clearance?');
// Default parameters
$url .= 'limit=60';
$url .= '&page=1';
$url .= '&sort[field]=created';
$url .= '&sort[direction]=desc';
// Product types
$url .= ($this->getInput('ready_made') ? '&type[]=ready_made' : '');
$url .= ($this->getInput('flop') ? '&type[]=flop' : '');
// Product names
foreach(array_filter(explode(',', $this->getInput('skus'))) as $sku) {
$url .= '&skus[]=' . urlencode(trim($sku));
}
// Size
$url .= ($this->getInput('onesize') ? '&sizes[]=onesize' : '');
$url .= ($this->getInput('mini') ? '&sizes[]=mini' : '');
$url .= ($this->getInput('small') ? '&sizes[]=small' : '');
$url .= ($this->getInput('medium') ? '&sizes[]=medium' : '');
$url .= ($this->getInput('large') ? '&sizes[]=large' : '');
$url .= ($this->getInput('extralarge') ? '&sizes[]=extralarge' : '');
// Category
$url .= ($this->getInput('category') ? '&category='
. urlencode($this->getInput('category')) : '');
// Firmness
if($api) {
$url .= ($this->getInput('soft') ? '&firmnessValues[]=3' : '');
$url .= ($this->getInput('med_firm') ? '&firmnessValues[]=5' : '');
$url .= ($this->getInput('firm') ? '&firmnessValues[]=8' : '');
if($this->getInput('split')) {
$url .= '&firmnessValues[]=3/5';
$url .= '&firmnessValues[]=3/8';
$url .= '&firmnessValues[]=8/3';
$url .= '&firmnessValues[]=5/8';
$url .= '&firmnessValues[]=8/5';
}
} else{
$url .= ($this->getInput('soft') ? '&firmnessValues[]=soft' : '');
$url .= ($this->getInput('med_firm') ? '&firmnessValues[]=medium' : '');
$url .= ($this->getInput('firm') ? '&firmnessValues[]=firm' : '');
$url .= ($this->getInput('split') ? '&firmnessValues[]=split' : '');
}
// Price
$url .= ($this->getInput('maxprice') ? '&price[max]='
. $this->getInput('maxprice') : '&price[max]=300');
$url .= ($this->getInput('minprice') ? '&price[min]='
. $this->getInput('minprice') : '&price[min]=0');
// Features
$url .= ($this->getInput('cumtube') ? '&cumtube=1' : '');
$url .= ($this->getInput('suctionCup') ? '&suctionCup=1' : '');
$url .= ($this->getInput('noAccessories') ? '&noAccessories=1' : '');
return $url;
}
}

View File

@@ -68,7 +68,7 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract {
$item['title'] = implode(' ', $title);
$item['uri'] = $this->getURI();
$item['uid'] = hash('sha1', $item['title']);
$item['uid'] = $this->getSanitizedHash($item['title']);
$this->items[] = $item;
}
@@ -89,8 +89,12 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract {
return parent::getName();
}
private function getSanitizedHash($string) {
return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string))));
}
private function filterText($text) {
return rtrim($text, '*');
return rtrim($text, '* ');
}
private function filterHTML($text) {

103
bridges/BinanceBridge.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
class BinanceBridge extends BridgeAbstract {
const NAME = 'Binance';
const URI = 'https://www.binance.com';
const DESCRIPTION = 'Subscribe to the Binance blog or the Binance Zendesk announcements.';
const MAINTAINER = 'thefranke';
const CACHE_TIMEOUT = 3600; // 1h
const PARAMETERS = array( array(
'category' => array(
'name' => 'category',
'type' => 'list',
'exampleValue' => 'Blog',
'title' => 'Select a category',
'values' => array(
'Blog' => 'Blog',
'Announcements' => 'Announcements'
)
)
));
public function getIcon() {
return 'https://bin.bnbstatic.com/static/images/common/favicon.ico';
}
public function getName() {
return self::NAME . ' ' . $this->getInput('category');
}
public function getURI() {
if ($this->getInput('category') == 'Blog')
return self::URI . '/en/blog';
else
return 'https://binance.zendesk.com/hc/en-us/categories/115000056351-Announcements';
}
protected function collectBlogData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not fetch Binance blog data.');
foreach($html->find('div[direction="row"]') as $element) {
$date = $element->find('div[direction="column"]', 0);
$day = $date->find('div', 0)->innertext;
$month = $date->find('div', 1)->innertext;
$extractedDate = $day . ' ' . $month;
$abstract = $element->find('div[direction="column"]', 1);
$a = $abstract->find('a', 0);
$uri = self::URI . $a->href;
$title = $a->innertext;
$full = getSimpleHTMLDOMCached($uri);
$content = $full->find('div.desc', 1);
$item = array();
$item['title'] = $title;
$item['uri'] = $uri;
$item['timestamp'] = strtotime($extractedDate);
$item['author'] = 'Binance';
$item['content'] = $content;
$this->items[] = $item;
if (count($this->items) >= 10)
break;
}
}
protected function collectAnnouncementData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not fetch Zendesk announcement data.');
foreach($html->find('a.article-list-link') as $a) {
$title = $a->innertext;
$uri = 'https://binance.zendesk.com' . $a->href;
$full = getSimpleHTMLDOMCached($uri);
$content = $full->find('div.article-body', 0);
$date = $full->find('time', 0)->getAttribute('datetime');
$item = array();
$item['title'] = $title;
$item['uri'] = $uri;
$item['timestamp'] = strtotime($date);
$item['author'] = 'Binance';
$item['content'] = $content;
$this->items[] = $item;
if (count($this->items) >= 10)
break;
}
}
public function collectData() {
if ($this->getInput('category') == 'Blog')
$this->collectBlogData();
else
$this->collectAnnouncementData();
}
}

View File

@@ -0,0 +1,119 @@
<?php
class BingSearchBridge extends BridgeAbstract
{
const NAME = 'Bing search';
const URI = 'https://www.bing.com/';
const DESCRIPTION = 'Return images from bing search discover';
const MAINTAINER = 'DnAp';
const PARAMETERS = array(
'Image Discover' => array(
'category' => array(
'name' => 'Categories',
'type' => 'list',
'values' => self::IMAGE_DISCOVER_CATEGORIES
),
'image_size' => array(
'name' => 'Image size',
'type' => 'list',
'values' => array(
'Small' => 'turl',
'Full size' => 'imgurl'
)
)
)
);
const IMAGE_DISCOVER_CATEGORIES = array(
'Abstract' => 'abstract',
'Animals' => 'animals',
'Anime' => 'anime',
'Architecture' => 'architecture',
'Arts and Crafts' => 'arts-and-crafts',
'Beauty' => 'beauty',
'Cars and Motorcycles' => 'cars-and-motorcycles',
'Cats' => 'cats',
'Celebrities' => 'celebrities',
'Comics' => 'comics',
'DIY' => 'diy',
'Dogs' => 'dogs',
'Fitness' => 'fitness',
'Food and Drink' => 'food-and-drink',
'Funny' => 'funny',
'Gadgets' => 'gadgets',
'Gardening' => 'gardening',
'Geeky' => 'geeky',
'Hairstyles' => 'hairstyles',
'Home Decor' => 'home-decor',
'Marine Life' => 'marine-life',
'Men\'s Fashion' => 'men%27s-fashion',
'Nature' => 'nature',
'Outdoors' => 'outdoors',
'Parenting' => 'parenting',
'Phone Wallpapers' => 'phone-wallpapers',
'Photography' => 'photography',
'Quotes' => 'quotes',
'Recipes' => 'recipes',
'Snow' => 'snow',
'Tattoos' => 'tattoos',
'Travel' => 'travel',
'Video Games' => 'video-games',
'Weddings' => 'weddings',
'Women\'s Fashion' => 'women%27s-fashion',
);
public function getIcon()
{
return 'https://www.bing.com/sa/simg/bing_p_rr_teal_min.ico';
}
public function collectData()
{
$this->items = $this->imageDiscover($this->getInput('category'));
}
public function getName()
{
if ($this->getInput('category')) {
if (self::IMAGE_DISCOVER_CATEGORIES[$this->getInput('categories')] !== null) {
$category = self::IMAGE_DISCOVER_CATEGORIES[$this->getInput('categories')];
} else {
$category = 'Unknown';
}
return 'Best ' . $category . ' - Bing Image Discover';
}
return parent::getName();
}
private function imageDiscover($category)
{
$html = getSimpleHTMLDOM(self::URI . '/discover/' . $category)
or returnServerError('Could not request ' . self::NAME);
$sizeKey = $this->getInput('image_size');
$items = [];
foreach ($html->find('a.iusc') as $element) {
$data = json_decode(htmlspecialchars_decode($element->getAttribute('m')), true);
$item = array();
$item['title'] = basename(rtrim($data['imgurl'], '/'));
$item['uri'] = $data['imgurl'];
$item['content'] = '<a href="' . $data['imgurl'] . '">
<img src="' . $data[$sizeKey] . '" alt="' . $item['title'] . '"></a>
<p>Source: <a href="' . $this->curUrl($data['surl']) . '"> </a></p>';
$item['enclosures'] = $data['imgurl'];
$items[] = $item;
}
return $items;
}
private function curUrl($url)
{
if (strlen($url) <= 80) {
return $url;
}
return substr($url, 0, 80) . '...';
}
}

157
bridges/BrutBridge.php Normal file
View File

@@ -0,0 +1,157 @@
<?php
class BrutBridge extends BridgeAbstract {
const NAME = 'Brut Bridge';
const URI = 'https://www.brut.media';
const DESCRIPTION = 'Returns 5 newest videos by category and edition';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(array(
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'News' => 'news',
'International' => 'international',
'Economy' => 'economy',
'Science and Technology' => 'science-and-technology',
'Entertainment' => 'entertainment',
'Sports' => 'sport',
'Nature' => 'nature',
),
'defaultValue' => 'news',
),
'edition' => array(
'name' => ' Edition',
'type' => 'list',
'values' => array(
'United States' => 'us',
'United Kingdom' => 'uk',
'France' => 'fr',
'India' => 'in',
'Mexico' => 'mx',
),
'defaultValue' => 'us',
)
)
);
const CACHE_TIMEOUT = 1800; // 30 mins
private $videoId = '';
private $videoType = '';
private $videoImage = '';
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request: ' . $this->getURI());
$results = $html->find('div.results', 0);
foreach($results->find('li.col-6.col-sm-4.col-md-3.col-lg-2.px-2.pb-4') as $index => $li) {
$item = array();
$videoPath = self::URI . $li->children(0)->href;
$videoPageHtml = getSimpleHTMLDOMCached($videoPath, 3600)
or returnServerError('Could not request: ' . $videoPath);
$this->videoImage = $videoPageHtml->find('meta[name="twitter:image"]', 0)->content;
$this->processTwitterImage();
$description = $videoPageHtml->find('div.description', 0);
$item['uri'] = $videoPath;
$item['title'] = $description->find('h1', 0)->plaintext;
if ($description->find('div.date', 0)->children(0)) {
$description->find('div.date', 0)->children(0)->outertext = '';
}
$item['content'] = $this->processContent(
$description
);
$item['timestamp'] = $this->processDate($description);
$item['enclosures'][] = $this->videoImage;
$this->items[] = $item;
if (count($this->items) >= 5) {
break;
}
}
}
public function getURI() {
if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
return self::URI . '/' . $this->getInput('edition') . '/' . $this->getInput('category');
}
return parent::getURI();
}
public function getName() {
if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
$parameters = $this->getParameters();
$editionValues = array_flip($parameters[0]['edition']['values']);
$categoryValues = array_flip($parameters[0]['category']['values']);
return $categoryValues[$this->getInput('category')] . ' - ' .
$editionValues[$this->getInput('edition')] . ' - Brut.';
}
return parent::getName();
}
private function processDate($description) {
if ($this->getInput('edition') === 'uk') {
$date = DateTime::createFromFormat('d/m/Y H:i', $description->find('div.date', 0)->innertext);
return strtotime($date->format('Y-m-d H:i:s'));
}
return strtotime($description->find('div.date', 0)->innertext);
}
private function processContent($description) {
$content = '<video controls poster="' . $this->videoImage . '" preload="none">
<source src="https://content.brut.media/video/' . $this->videoId . '-' . $this->videoType . '-web.mp4"
type="video/mp4">
</video>';
$content .= '<p>' . $description->find('h2.mb-1', 0)->innertext . '</p>';
if ($description->find('div.text.pb-3', 0)->children(1)->class != 'date') {
$content .= '<p>' . $description->find('div.text.pb-3', 0)->children(1)->innertext . '</p>';
}
return $content;
}
private function processTwitterImage() {
/**
* Extract video ID + type from twitter image
*
* Example (wrapped):
* https://img.brut.media/thumbnail/
* the-life-of-rita-moreno-2cce75b5-d448-44d2-a97c-ca50d6470dd4-square.jpg
* ?ts=1559337892
*/
$fpath = parse_url($this->videoImage, PHP_URL_PATH);
$fname = basename($fpath);
$fname = substr($fname, 0, strrpos($fname, '.'));
$parts = explode('-', $fname);
if (end($parts) === 'auto') {
$key = array_search('auto', $parts);
unset($parts[$key]);
}
$this->videoId = implode('-', array_splice($parts, -6, 5));
$this->videoType = end($parts);
}
}

View File

@@ -83,7 +83,7 @@ class CastorusBridge extends BridgeAbstract {
if(!$html)
returnServerError('Could not load data from ' . self::URI . '!');
$activities = $html->find('div#activite/li');
$activities = $html->find('div#activite > li');
if(!$activities)
returnServerError('Failed to find activities!');

View File

@@ -14,7 +14,7 @@ class ComboiosDePortugalBridge extends BridgeAbstract {
$item = array();
$item['title'] = $element->innertext;
$item['uri'] = self::BASE_URI . $element->href;
$item['uri'] = self::BASE_URI . implode('/', array_map('urlencode', explode('/', $element->href)));
$this->items[] = $item;
}

View File

@@ -3,7 +3,7 @@ class CourrierInternationalBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'Courrier International Bridge';
const URI = 'http://CourrierInternational.com/';
const URI = 'https://www.courrierinternational.com/';
const CACHE_TIMEOUT = 300; // 5 min
const DESCRIPTION = 'Courrier International bridge';

View File

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

View File

@@ -159,13 +159,13 @@ class DesoutterBridge extends BridgeAbstract {
foreach($html->find('article') as $article) {
$item = array();
$item['uri'] = $article->find('[itemprop="name"]', 0)->href;
$item['title'] = $article->find('[itemprop="name"]', 0)->title;
$item['uri'] = $article->find('a', 0)->href;
$item['title'] = $article->find('a[title]', 0)->title;
if($this->getInput('full')) {
$item['content'] = $this->getFullNewsArticle($item['uri']);
} else {
$item['content'] = $article->find('[itemprop="description"]', 0)->plaintext;
$item['content'] = $article->find('div.tile-body p', 0)->plaintext;
}
$this->items[] = $item;

View File

@@ -1,9 +0,0 @@
<?php
require_once('Shimmie2Bridge.php');
class DollbooruBridge extends Shimmie2Bridge {
const MAINTAINER = 'mitsukarenai';
const NAME = 'Dollbooru';
const URI = 'http://dollbooru.org/';
const DESCRIPTION = 'Returns images from given page';
}

View File

@@ -0,0 +1,63 @@
<?php
class EconomistBridge extends BridgeAbstract {
const NAME = 'The Economist: Latest Updates';
const URI = 'https://www.economist.com';
const DESCRIPTION = 'Fetches the latest updates from the Economist.';
const MAINTAINER = 'thefranke';
const CACHE_TIMEOUT = 3600; // 1h
public function getIcon() {
return 'https://www.economist.com/sites/default/files/econfinal_favicon.ico';
}
public function collectData() {
$html = getSimpleHTMLDOM(self::URI . '/latest/')
or returnServerError('Could not fetch latest updates form The Economist.');
foreach($html->find('article') as $element) {
$a = $element->find('a', 0);
$href = self::URI . $a->href;
$full = getSimpleHTMLDOMCached($href);
$article = $full->find('article', 0);
$header = $article->find('h1', 0);
$author = $article->find('span[itemprop="author"]', 0);
$time = $article->find('time[itemprop="dateCreated"]', 0);
$content = $article->find('div[itemprop="description"]', 0);
// Remove newsletter subscription box
$newsletter = $content->find('div[class="newsletter-form__message"]', 0);
if ($newsletter)
$newsletter->outertext = '';
$newsletterForm = $content->find('form', 0);
if ($newsletterForm)
$newsletterForm->outertext = '';
// Remove next and previous article URLs at the bottom
$nextprev = $content->find('div[class="blog-post__next-previous-wrapper"]', 0);
if ($nextprev)
$nextprev->outertext = '';
$section = [ $article->find('h3[itemprop="articleSection"]', 0)->plaintext ];
$item = array();
$item['title'] = $header->find('span', 0)->innertext . ': '
. $header->find('span', 1)->innertext;
$item['uri'] = $href;
$item['timestamp'] = strtotime($time->datetime);
$item['author'] = $author->innertext;
$item['categories'] = $section;
$item['content'] = '<img style="max-width: 100%" src="'
. $a->find('img', 0)->src . '">' . $content->innertext;
$this->items[] = $item;
if (count($this->items) >= 10)
break;
}
}
}

View File

@@ -47,5 +47,8 @@ class EliteDangerousGalnetBridge extends BridgeAbstract {
$this->items[] = $item;
}
//Remove duplicates that sometimes show up on the website
$this->items = array_unique($this->items, SORT_REGULAR);
}
}

View File

@@ -120,9 +120,11 @@ class ElloBridge extends BridgeAbstract {
}
private function getAPIKey() {
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$cache->setPath(PATH_CACHE);
$cache->setParameters(['key']);
$cacheFac = new CacheFactory();
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope(get_called_class());
$cache->setKey(['key']);
$key = $cache->loadData();
if($key == null) {

View File

@@ -72,15 +72,15 @@ class FB2Bridge extends BridgeAbstract {
$pageInfo = $this->getPageInfos($page, $cookies);
if($pageInfo['userId'] === null) {
echo <<<EOD
returnClientError(<<<EOD
Unable to get the page id. You should consider getting the ID by hand, then importing it into FB2Bridge
EOD;
die();
EOD
);
} elseif($pageInfo['userId'] == -1) {
echo <<<EOD
returnClientError(<<<EOD
This page is not accessible without being logged in.
EOD;
die();
EOD
);
}
}
@@ -95,7 +95,7 @@ EOD;
foreach($html->find('article') as $content) {
$item = array();
//echo $content; die();
preg_match('/publish_time\\\":([0-9]+),/', $content->getAttribute('data-store', 0), $match);
if(isset($match[1]))
$timestamp = $match[1];

View File

@@ -28,14 +28,14 @@ class FDroidBridge extends BridgeAbstract {
or returnServerError('Could not request F-Droid.');
// targetting the corresponding widget based on user selection
// "updated" is the 4th widget on the page, "added" is the 5th
// "updated" is the 5th widget on the page, "added" is the 6th
switch($this->getInput('u')) {
case 'updated':
$html_widget = $html->find('div.sidebar-widget', 4);
$html_widget = $html->find('div.sidebar-widget', 5);
break;
default:
$html_widget = $html->find('div.sidebar-widget', 5);
$html_widget = $html->find('div.sidebar-widget', 6);
break;
}

View File

@@ -219,8 +219,7 @@ class FacebookBridge extends BridgeAbstract {
$ogtitle = $html->find('meta[property="og:title"]', 0)
or returnServerError('Unable to find group title!');
return htmlspecialchars_decode($ogtitle->content, ENT_QUOTES);
return html_entity_decode($ogtitle->content, ENT_QUOTES);
}
private function extractGroupURI($post) {

164
bridges/FicbookBridge.php Normal file
View File

@@ -0,0 +1,164 @@
<?php
class FicbookBridge extends BridgeAbstract {
const NAME = 'Ficbook Bridge';
const URI = 'https://ficbook.net/';
const DESCRIPTION = 'No description provided';
const MAINTAINER = 'logmanoriginal';
const PARAMETERS = array(
'Site News' => array(),
'Fiction Updates' => array(
'fiction_id' => array(
'name' => 'Fanfiction ID',
'type' => 'text',
'pattern' => '[0-9]+',
'required' => true,
'title' => 'Insert fanfiction ID',
'exampleValue' => '5783919',
),
'include_contents' => array(
'name' => 'Include contents',
'type' => 'checkbox',
'title' => 'Activate to include contents in the feed',
),
),
'Fiction Comments' => array(
'fiction_id' => array(
'name' => 'Fanfiction ID',
'type' => 'text',
'pattern' => '[0-9]+',
'required' => true,
'title' => 'Insert fanfiction ID',
'exampleValue' => '5783919',
),
),
);
public function getURI() {
switch($this->queriedContext) {
case 'Site News': {
// For some reason this is not HTTPS
return 'http://ficbook.net/sitenews';
}
case 'Fiction Updates': {
return self::URI
. 'readfic/'
. urlencode($this->getInput('fiction_id'));
}
case 'Fiction Comments': {
return self::URI
. 'readfic/'
. urlencode($this->getInput('fiction_id'))
. '/comments#content';
}
default: return parent::getURI();
}
}
public function collectData() {
$header = array('Accept-Language: en-US');
$html = getSimpleHTMLDOM($this->getURI(), $header)
or returnServerError('Could not request ' . $this->getURI());
$html = defaultLinkTo($html, self::URI);
switch($this->queriedContext) {
case 'Site News': return $this->collectSiteNews($html);
case 'Fiction Updates': return $this->collectUpdatesData($html);
case 'Fiction Comments': return $this->collectCommentsData($html);
}
}
private function collectSiteNews($html) {
foreach($html->find('.news_view') as $news) {
$this->items[] = array(
'title' => $news->find('h1.title', 0)->plaintext,
'timestamp' => strtotime($this->fixDate($news->find('span[title]', 0)->title)),
'content' => $news->find('.news_text', 0),
);
}
}
private function collectCommentsData($html) {
foreach($html->find('article.post') as $article) {
$this->items[] = array(
'uri' => $article->find('.comment_link_to_fic > a', 0)->href,
'title' => $article->find('.comment_author', 0)->plaintext,
'author' => $article->find('.comment_author', 0)->plaintext,
'timestamp' => strtotime($this->fixDate($article->find('time[datetime]', 0)->datetime)),
'content' => $article->find('.comment_message', 0),
'enclosures' => array($article->find('img', 0)->src),
);
}
}
private function collectUpdatesData($html) {
foreach($html->find('ul.table-of-contents > li') as $chapter) {
$item = array(
'uri' => $chapter->find('a', 0)->href,
'title' => $chapter->find('a', 0)->plaintext,
'timestamp' => strtotime($this->fixDate($chapter->find('span[title]', 0)->title)),
);
if($this->getInput('include_contents')) {
$content = getSimpleHTMLDOMCached($item['uri']);
$item['content'] = $content->find('#content', 0);
}
$this->items[] = $item;
// Sort by time, descending
usort($this->items, function($a, $b){ return $b['timestamp'] - $a['timestamp']; });
}
}
private function fixDate($date) {
// FIXME: This list was generated using Google tranlator. Someone who
// actually knows russian should check this list! Please keep in mind
// that month names must match exactly the names returned by Ficbook.
$ru_month = array(
'января',
'февраля',
'марта',
'апреля',
'мая',
'июня',
'июля',
'августа',
'Сентября',
'октября',
'Ноября',
'Декабря',
);
$en_month = array(
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
);
$fixed_date = str_replace($ru_month, $en_month, $date);
if($fixed_date === $date) {
Debug::log('Unable to fix date: ' . $date);
return null;
}
return $fixed_date;
}
}

View File

@@ -62,11 +62,16 @@ class FindACrewBridge extends BridgeAbstract {
foreach ($annonces as $annonce) {
$item = array();
$img = parent::getURI() . $annonce->find('.css_LstPic img', 0)->getAttribute('src');
$item['title'] = $annonce->find('.css_LstCtrls span', 0)->plaintext;
$item['uri'] = parent::getURI() . $annonce->find('.css_PnlCtrls a', 0)->href;
$content = $annonce->find('.css_LstDtl div', 2)->innertext;
$item['content'] = "<img src='$img' /><br>$content";
$link = parent::getURI() . $annonce->find('.lst-ctrls a', 0)->href;
$htmlDetail = getSimpleHTMLDOMCached($link . '?mdl=2'); // add ?mdl=2 for xhr content not full html page
$img = parent::getURI() . $htmlDetail->find('img.img-responsive', 0)->getAttribute('src');
$item['title'] = $annonce->find('.lst-tags span', 0)->plaintext;
$item['uri'] = $link;
$content = $htmlDetail->find('.panel-body div.clearfix.row > div', 1)->innertext;
$content .= $htmlDetail->find('.panel-body > div', 1)->innertext;
$content = defaultLinkTo($content, parent::getURI());
$item['content'] = $content;
$item['enclosures'] = array($img);
$item['categories'] = array($annonce->find('.css_AccLocCur', 0)->plaintext);
$this->items[] = $item;

View File

@@ -8,8 +8,8 @@ class GOGBridge extends BridgeAbstract {
public function collectData() {
$values = getContents('https://www.gog.com/games/ajax/filtered?limit=25&sort=new') or
die('Unable to get the news pages from GOG !');
$values = getContents('https://www.gog.com/games/ajax/filtered?limit=25&sort=new')
or returnServerError('Unable to get the news pages from GOG !');
$decodedValues = json_decode($values);
$limit = 0;
@@ -38,8 +38,8 @@ class GOGBridge extends BridgeAbstract {
private function buildGameContentPage($game) {
$gameDescriptionText = getContents('https://api.gog.com/products/' . $game->id . '?expand=description') or
die('Unable to get game description from GOG !');
$gameDescriptionText = getContents('https://api.gog.com/products/' . $game->id . '?expand=description')
or returnServerError('Unable to get game description from GOG !');
$gameDescriptionValue = json_decode($gameDescriptionText);

View File

@@ -40,6 +40,11 @@ class GQMagazineBridge extends BridgeAbstract
'data-original' => 'src'
);
const POSSIBLE_TITLES = array(
'h2',
'h3'
);
private function getDomain() {
$domain = $this->getInput('domain');
if (empty($domain))
@@ -54,6 +59,17 @@ class GQMagazineBridge extends BridgeAbstract
return $this->getDomain() . '/' . $this->getInput('page');
}
private function findTitleOf($link) {
foreach (self::POSSIBLE_TITLES as $tag) {
$title = $link->find($tag, 0);
if($title !== null) {
if($title->plaintext !== null) {
return $title->plaintext;
}
}
}
}
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI()) or returnServerError('Could not request ' . $this->getURI());
@@ -62,30 +78,33 @@ class GQMagazineBridge extends BridgeAbstract
$main = $html->find('main', 0);
foreach ($main->find('a') as $link) {
$uri = $link->href;
$title = $link->find('h2', 0);
$date = $link->find('time', 0);
$item = array();
$author = $link->find('span[itemprop=name]', 0);
$item['author'] = $author->plaintext;
$item['title'] = $title->plaintext;
if(substr($uri, 0, 1) === 'h') { // absolute uri
$item['uri'] = $uri;
} else if(substr($uri, 0, 1) === '/') { // domain relative url
$item['uri'] = $this->getDomain() . $uri;
} else {
$item['uri'] = $this->getDomain() . '/' . $uri;
if($author !== null) {
$item['author'] = $author->plaintext;
$item['title'] = $this->findTitleOf($link);
switch(substr($uri, 0, 1)) {
case 'h': // absolute uri
$item['uri'] = $uri;
break;
case '/': // domain relative uri
$item['uri'] = $this->getDomain() . $uri;
break;
default:
$item['uri'] = $this->getDomain() . '/' . $uri;
}
$article = $this->loadFullArticle($item['uri']);
if($article) {
$item['content'] = $this->replaceUriInHtmlElement($article);
} else {
$item['content'] = "<strong>Article body couldn't be loaded</strong>. It must be a bug!";
}
$short_date = $date->datetime;
$item['timestamp'] = strtotime($short_date);
$this->items[] = $item;
}
$article = $this->loadFullArticle($item['uri']);
if($article) {
$item['content'] = $this->replaceUriInHtmlElement($article);
} else {
$item['content'] = "<strong>Article body couldn't be loaded</strong>. It must be a bug!";
}
$short_date = $date->datetime;
$item['timestamp'] = strtotime($short_date);
$this->items[] = $item;
}
}
@@ -96,16 +115,7 @@ class GQMagazineBridge extends BridgeAbstract
*/
private function loadFullArticle($uri){
$html = getSimpleHTMLDOMCached($uri);
// Once again, that generated css classes madness is an obstacle ... which i can go over easily
foreach($html->find('div') as $div) {
// List the CSS classes of that div
$classes = $div->class;
// I can't directly lookup that class since GQ since to generate random names like "ArticleBodySection-fkggUW"
if(strpos($classes, 'ArticleBodySection') !== false) {
return $div;
}
}
return null;
return $html->find('section[data-test-id=ArticleBodyContent]', 0);
}
/**

27
bridges/GiteaBridge.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
/**
* Gitea is a fork of Gogs which may diverge in the future.
* https://docs.gitea.io/en-us/
*/
require_once 'GogsBridge.php';
class GiteaBridge extends GogsBridge {
const NAME = 'Gitea';
const URI = 'https://gitea.io';
const DESCRIPTION = 'Returns the latest issues, commits or releases';
const MAINTAINER = 'logmanoriginal';
const CACHE_TIMEOUT = 300; // 5 minutes
protected function collectReleasesData($html) {
$releases = $html->find('#release-list > li')
or returnServerError('Unable to find releases');
foreach($releases as $release) {
$this->items[] = array(
'uri' => $release->find('a', 0)->href,
'title' => 'Release ' . $release->find('h3', 0)->plaintext,
);
}
}
}

View File

@@ -66,10 +66,21 @@ class GithubIssueBridge extends BridgeAbstract {
return parent::getURI();
}
protected function extractIssueEvent($issueNbr, $title, $comment){
$comment = $comment->firstChild();
$uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p')
. '/issues/' . $issueNbr . '#' . $comment->getAttribute('id');
private function buildGitHubIssueCommentUri($issue_number, $comment_id) {
// https://github.com/<user>/<project>/issues/<issue-number>#<id>
return static::URI
. $this->getInput('u')
. '/'
. $this->getInput('p')
. '/issues/'
. $issue_number
. '#'
. $comment_id;
}
private function extractIssueEvent($issueNbr, $title, $comment){
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
$author = $comment->find('.author', 0)->plaintext;
@@ -94,22 +105,21 @@ class GithubIssueBridge extends BridgeAbstract {
return $item;
}
protected function extractIssueComment($issueNbr, $title, $comment){
$uri = static::URI . $this->getInput('u') . '/'
. $this->getInput('p') . '/issues/' . $issueNbr;
private function extractIssueComment($issueNbr, $title, $comment){
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->parent->id);
$author = $comment->find('.author', 0)->plaintext;
$title .= ' / ' . trim(
$comment->find('.comment .timeline-comment-header-text', 0)->plaintext
$comment->find('.timeline-comment-header-text', 0)->plaintext
);
$content = $comment->find('.comment-body', 0)->innertext;
$item = array();
$item['author'] = $author;
$item['uri'] = $uri
. '#' . $comment->firstChild()->nextSibling()->getAttribute('id');
$item['uri'] = $uri;
$item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
$item['timestamp'] = strtotime(
$comment->find('relative-time', 0)->getAttribute('datetime')
@@ -118,25 +128,32 @@ class GithubIssueBridge extends BridgeAbstract {
return $item;
}
protected function extractIssueComments($issue){
private function extractIssueComments($issue){
$items = array();
$title = $issue->find('.gh-header-title', 0)->plaintext;
$issueNbr = trim(
substr($issue->find('.gh-header-number', 0)->plaintext, 1)
);
$comments = $issue->find('.js-discussion', 0);
foreach($comments->children() as $comment) {
$comments = $issue->find('
[id^="issue-"] > .comment,
[id^="issuecomment-"] > .comment,
[id^="event-"],
[id^="ref-"]
');
foreach($comments as $comment) {
if (!$comment->hasChildNodes()) {
continue;
}
$comment = $comment->firstChild();
$classes = explode(' ', $comment->getAttribute('class'));
if (in_array('timeline-comment-wrapper', $classes)) {
if (!$comment->hasClass('discussion-item-header')) {
$item = $this->extractIssueComment($issueNbr, $title, $comment);
$items[] = $item;
continue;
}
while (in_array('discussion-item', $classes)) {
while ($comment->hasClass('discussion-item-header')) {
$item = $this->extractIssueEvent($issueNbr, $title, $comment);
$items[] = $item;
$comment = $comment->nextSibling();
@@ -145,6 +162,7 @@ class GithubIssueBridge extends BridgeAbstract {
}
$classes = explode(' ', $comment->getAttribute('class'));
}
}
return $items;
}
@@ -192,8 +210,13 @@ class GithubIssueBridge extends BridgeAbstract {
ENT_QUOTES,
'UTF-8'
);
$comments = trim($issue->find('.col-5', 0)->plaintext);
$item['content'] .= "\n" . 'Comments: ' . ($comments ? $comments : '0');
$comment_count = 0;
if($span = $issue->find('a[aria-label*="comment"] span', 0)) {
$comment_count = $span->plaintext;
}
$item['content'] .= "\n" . 'Comments: ' . $comment_count;
$item['uri'] = self::URI
. $issue->find('.js-navigation-open', 0)->getAttribute('href');
$this->items[] = $item;
@@ -216,4 +239,43 @@ class GithubIssueBridge extends BridgeAbstract {
$item['title'] = preg_replace('/\s+/', ' ', $item['title']);
});
}
public function detectParameters($url) {
if(filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false
|| strpos($url, self::URI) !== 0) {
return null;
}
$url_components = parse_url($url);
$path_segments = array_values(array_filter(explode('/', $url_components['path'])));
switch(count($path_segments)) {
case 2: { // Project issues
list($user, $project) = $path_segments;
$show_comments = 'off';
} break;
case 3: { // Project issues with issue comments
if($path_segments[2] !== 'issues') {
return null;
}
list($user, $project) = $path_segments;
$show_comments = 'on';
} break;
case 4: { // Issue comments
list($user, $project, /* issues */, $issue) = $path_segments;
} break;
default: {
return null;
}
}
return array(
'u' => $user,
'p' => $project,
'c' => isset($show_comments) ? $show_comments : null,
'i' => isset($issue) ? $issue : null,
);
}
}

View File

@@ -24,7 +24,7 @@ class GithubSearchBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM($url)
or returnServerError('Error while downloading the website content');
foreach($html->find('div.repo-list-item') as $element) {
foreach($html->find('li.repo-list-item') as $element) {
$item = array();
$uri = $element->find('h3 a', 0)->href;

View File

@@ -141,7 +141,7 @@ class GlassdoorBridge extends BridgeAbstract {
}
private function collectReviewData($html, $limit) {
$reviews = $html->find('#EmployerReviews li[id^="empReview]')
$reviews = $html->find('#ReviewsFeed li[id^="empReview]')
or returnServerError('Unable to find reviews!');
foreach($reviews as $review) {
@@ -153,7 +153,19 @@ class GlassdoorBridge extends BridgeAbstract {
$item['timestamp'] = strtotime($review->find('time', 0)->datetime);
$mainText = $review->find('p.mainText', 0)->plaintext;
$description = $review->find('div.prosConsAdvice', 0)->innertext;
$description = '';
foreach($review->find('div.description p') as $p) {
if ($p->hasClass('strong')) {
$p->tag = 'strong';
$p->removeClass('strong');
}
$description .= $p;
}
$item['content'] = "<p>{$mainText}</p><p>{$description}</p>";
$this->items[] = $item;

206
bridges/GogsBridge.php Normal file
View File

@@ -0,0 +1,206 @@
<?php
class GogsBridge extends BridgeAbstract {
const NAME = 'Gogs';
const URI = 'https://gogs.io';
const DESCRIPTION = 'Returns the latest issues, commits or releases';
const MAINTAINER = 'logmanoriginal';
const CACHE_TIMEOUT = 300; // 5 minutes
const PARAMETERS = array(
'global' => array(
'host' => array(
'name' => 'Host',
'exampleValue' => 'https://gogs.io',
'required' => true,
'title' => 'Host name without trailing slash',
),
'user' => array(
'name' => 'Username',
'exampleValue' => 'gogs',
'required' => true,
'title' => 'User name as it appears in the URL',
),
'project' => array(
'name' => 'Project name',
'exampleValue' => 'gogs',
'required' => true,
'title' => 'Project name as it appears in the URL',
),
),
'Commits' => array(
'branch' => array(
'name' => 'Branch name',
'defaultValue' => 'master',
'required' => true,
'title' => 'Branch name as it appears in the URL',
),
),
'Issues' => array(
'include_description' => array(
'name' => 'Include issue description',
'type' => 'checkbox',
'title' => 'Activate to include the issue description',
),
),
'Single issue' => array(
'issue' => array(
'name' => 'Issue number',
'type' => 'number',
'exampleValue' => 102,
'required' => true,
'title' => 'Issue number from the issues list',
),
),
'Releases' => array(),
);
private $title = '';
/**
* Note: detectParamters doesn't make sense for this bridge because there is
* no "single" host for this service. Anyone can host it.
*/
public function getURI() {
switch($this->queriedContext) {
case 'Commits': {
return $this->getInput('host')
. '/' . $this->getInput('user')
. '/' . $this->getInput('project')
. '/commits/' . $this->getInput('branch');
} break;
case 'Issues': {
return $this->getInput('host')
. '/' . $this->getInput('user')
. '/' . $this->getInput('project')
. '/issues/';
} break;
case 'Single issue': {
return $this->getInput('host')
. '/' . $this->getInput('user')
. '/' . $this->getInput('project')
. '/issues/' . $this->getInput('issue');
} break;
case 'Releases': {
return $this->getInput('host')
. '/' . $this->getInput('user')
. '/' . $this->getInput('project')
. '/releases/';
} break;
default: return parent::getURI();
}
}
public function getName() {
switch($this->queriedContext) {
case 'Commits':
case 'Issues':
case 'Releases': return $this->title . ' ' . $this->queriedContext;
case 'Single issue': return $this->title . ' Issue ' . $this->getInput('issue');
default: return parent::getName();
}
}
public function getIcon() {
return 'https://gogs.io/img/favicon.ico';
}
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request ' . $this->getURI());
$html = defaultLinkTo($html, $this->getURI());
$this->title = $html->find('[property="og:title"]', 0)->content;
switch($this->queriedContext) {
case 'Commits': {
$this->collectCommitsData($html);
} break;
case 'Issues': {
$this->collectIssuesData($html);
} break;
case 'Single issue': {
$this->collectSingleIssueData($html);
} break;
case 'Releases': {
$this->collectReleasesData($html);
} break;
}
}
protected function collectCommitsData($html) {
$commits = $html->find('#commits-table tbody tr')
or returnServerError('Unable to find commits');
foreach($commits as $commit) {
$this->items[] = array(
'uri' => $commit->find('a.sha', 0)->href,
'title' => $commit->find('.message span', 0)->plaintext,
'author' => $commit->find('.author', 0)->plaintext,
'timestamp' => $commit->find('.time-since', 0)->title,
'uid' => $commit->find('.sha', 0)->plaintext,
);
}
}
protected function collectIssuesData($html) {
$issues = $html->find('.issue.list li')
or returnServerError('Unable to find issues');
foreach($issues as $issue) {
$uri = $issue->find('a', 0)->href;
$item = array(
'uri' => $uri,
'title' => $issue->find('.label', 0)->plaintext . ' | ' . $issue->find('a.title', 0)->plaintext,
'author' => $issue->find('.desc a', 0)->plaintext,
'timestamp' => $issue->find('.time-since', 0)->title,
'uid' => $issue->find('.label', 0)->plaintext,
);
if($this->getInput('include_description')) {
$issue_html = getSimpleHTMLDOMCached($uri, 3600)
or returnServerError('Unable to load issue description');
$issue_html = defaultLinkTo($issue_html, $uri);
$item['content'] = $issue_html->find('.comment .markdown', 0);
}
$this->items[] = $item;
}
}
protected function collectSingleIssueData($html) {
$comments = $html->find('.comments .comment')
or returnServerError('Unable to find comments');
foreach($comments as $comment) {
$this->items[] = array(
'uri' => $comment->find('a[href*="#issue"]', 0)->href,
'title' => $comment->find('span', 0)->plaintext,
'author' => $comment->find('.content a', 0)->plaintext,
'timestamp' => $comment->find('.time-since', 0)->title,
'content' => $comment->find('.markdown', 0),
);
}
$this->items = array_reverse($this->items);
}
protected function collectReleasesData($html) {
$releases = $html->find('#release-list li')
or returnServerError('Unable to find releases');
foreach($releases as $release) {
$this->items[] = array(
'uri' => $release->find('a', 0)->href,
'title' => 'Release ' . $release->find('h4', 0)->plaintext,
);
}
}
}

View File

@@ -1,208 +0,0 @@
<?php
class GooglePlusPostBridge extends BridgeAbstract{
private $title;
private $url;
const MAINTAINER = 'Grummfy, logmanoriginal';
const NAME = 'Google Plus Post Bridge';
const URI = 'https://plus.google.com';
const CACHE_TIMEOUT = 600; //10min
const DESCRIPTION = 'Returns user public post (without API).';
const PARAMETERS = array( array(
'username' => array(
'name' => 'username or Id',
'required' => true
),
'include_media' => array(
'name' => 'Include media',
'type' => 'checkbox',
'title' => 'Enable to include media in the feed content'
)
));
public function getIcon() {
return 'https://ssl.gstatic.com/images/branding/product/ico/google_plus_alldp.ico';
}
public function collectData(){
$username = $this->getInput('username');
// Usernames start with a + if it's not an ID
if(!is_numeric($username) && substr($username, 0, 1) !== '+') {
$username = '+' . $username;
}
$html = getSimpleHTMLDOM(static::URI . '/' . urlencode($username) . '/posts')
or returnServerError('No results for this query.');
$html = defaultLinkTo($html, static::URI);
$this->title = $html->find('meta[property=og:title]', 0)->getAttribute('content');
$this->url = $html->find('meta[property=og:url]', 0)->getAttribute('content');
foreach($html->find('div[jsname=WsjYwc]') as $post) {
$item = array();
$item['author'] = $post->find('div div div div a', 0)->innertext;
$item['uri'] = $post->find('div div div a', 1)->href;
$timestamp = $post->find('a.qXj2He span', 0);
if($timestamp) {
$item['timestamp'] = strtotime('+' . preg_replace(
'/[^0-9A-Za-z]/',
'',
$timestamp->getAttribute('aria-label')));
}
$message = $post->find('div[jsname=EjRJtf]', 0);
// Empty messages are not supported right now
if(!$message) {
continue;
}
$item['content'] = '<div style="float: left; padding: 0 10px 10px 0;"><a href="'
. $this->url
. '"><img align="top" alt="'
. $item['author']
. '" src="'
. $post->find('div img', 0)->src
. '" /></a></div><div>'
. trim(strip_tags($message, '<a><p><div><img>'))
. '</div>';
// Make title at least 50 characters long, but don't add '...' if it is shorter!
if(strlen($message->plaintext) > 50) {
$end = strpos($message->plaintext, ' ', 50) ?: strlen($message->plaintext);
} else {
$end = strlen($message->plaintext);
}
if(strlen(substr($message->plaintext, 0, $end)) === strlen($message->plaintext)) {
$item['title'] = $message->plaintext;
} else {
$item['title'] = substr($message->plaintext, 0, $end) . '...';
}
$media = $post->find('[jsname="MTOxpb"]', 0);
if($media) {
$item['enclosures'] = array();
foreach($media->find('img') as $img) {
$item['enclosures'][] = $this->fixImage($img)->src;
}
if($this->getInput('include_media') === true && count($item['enclosures'] > 0)) {
$item['content'] .= '<div style="clear: both;"><a href="'
. $item['enclosures'][0]
. '"><img src="'
. $item['enclosures'][0]
. '" /></a></div>';
}
}
// Add custom parameters (only useful for JSON or Plaintext)
$item['fullname'] = $item['author'];
$item['avatar'] = $post->find('div img', 0)->src;
$item['id'] = $post->find('div div div', 0)->getAttribute('id');
$item['content_simple'] = $message->plaintext;
$this->items[] = $item;
}
}
public function getName(){
return $this->title ?: 'Google Plus Post Bridge';
}
public function getURI(){
return $this->url ?: parent::getURI();
}
private function fixImage($img) {
// There are certain images like .gif which link to a static picture and
// get replaced dynamically via JS in the browser. If we want the "real"
// image we need to account for that.
$urlparts = parse_url($img->src);
if(array_key_exists('host', $urlparts)) {
// For some reason some URIs don't contain the scheme, assume https
if(!array_key_exists('scheme', $urlparts)) {
$urlparts['scheme'] = 'https';
}
$pathelements = explode('/', $urlparts['path']);
switch($urlparts['host']) {
case 'lh3.googleusercontent.com':
if(pathinfo(end($pathelements), PATHINFO_EXTENSION)) {
// The second to last element of the path specifies the
// image format. The URL is still valid if we remove it.
unset($pathelements[count($pathelements) - 2]);
} elseif(strrpos(end($pathelements), '=') !== false) {
// Some images go throug a proxy. For those images they
// add size information after an equal sign.
// Example: '=w530-h298-n'. Again this can safely be
// removed to get the original image.
$pathelements[count($pathelements) - 1] = substr(
end($pathelements),
0,
strrpos(end($pathelements), '=')
);
}
break;
}
$urlparts['path'] = implode('/', $pathelements);
}
$img->src = $this->build_url($urlparts);
return $img;
}
/**
* From: https://gist.github.com/Ellrion/f51ba0d40ae1d62eeae44fd1adf7b704
* slightly adjusted to work with PHP < 7.0
* @param array $parts
* @return string
*/
private function build_url(array $parts)
{
$scheme = isset($parts['scheme']) ? ($parts['scheme'] . '://') : '';
$host = isset($parts['host']) ? $parts['host'] : '';
$port = isset($parts['port']) ? (':' . $parts['port']) : '';
$user = isset($parts['user']) ? $parts['user'] : '';
$pass = isset($parts['pass']) ? (':' . $parts['pass']) : '';
$pass = ($user || $pass) ? ($pass . '@') : '';
$path = isset($parts['path']) ? $parts['path'] : '';
$query = isset($parts['query']) ? ('?' . $parts['query']) : '';
$fragment = isset($parts['fragment']) ? ('#' . $parts['fragment']) : '';
return implode('', [$scheme, $user, $pass, $host, $port, $path, $query, $fragment]);
}
}

View File

@@ -16,13 +16,13 @@ class HDWallpapersBridge extends BridgeAbstract {
),
'r' => array(
'name' => 'resolution',
'defaultValue' => '1920x1200',
'exampleValue' => '1920x1200, 1680x1050,…'
'defaultValue' => 'HD',
'exampleValue' => 'HD, 1920x1200, 1680x1050,…'
)
));
public function collectData(){
$category = $this->category;
$category = $this->getInput('c');
if(strrpos($category, 'wallpapers') !== strlen($category) - strlen('wallpapers')) {
$category .= '-desktop-wallpapers';
}
@@ -45,13 +45,12 @@ class HDWallpapersBridge extends BridgeAbstract {
$thumbnail = $element->find('img', 0);
$item = array();
// http://www.hdwallpapers.in/download/yosemite_reflections-1680x1050.jpg
$item['uri'] = self::URI
. '/download'
. str_replace('wallpapers.html', $this->getInput('r') . '.jpg', $element->href);
$item['timestamp'] = time();
$item['title'] = $element->find('p', 0)->text();
$item['title'] = $element->find('em1', 0)->text();
$item['content'] = $item['title']
. '<br><a href="'
. $item['uri']
@@ -60,6 +59,7 @@ class HDWallpapersBridge extends BridgeAbstract {
. $thumbnail->src
. '" /></a>';
$item['enclosures'] = array($item['uri']);
$this->items[] = $item;
$num++;

View File

@@ -0,0 +1,123 @@
<?php
class HaveIBeenPwnedBridge extends BridgeAbstract {
const NAME = 'Have I Been Pwned (HIBP) Bridge';
const URI = 'https://haveibeenpwned.com';
const DESCRIPTION = 'Returns list of Pwned websites';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(array(
'order' => array(
'name' => 'Order by',
'type' => 'list',
'values' => array(
'Breach date' => 'breachDate',
'Date added to HIBP' => 'dateAdded',
),
'defaultValue' => 'dateAdded',
)
));
const CACHE_TIMEOUT = 3600;
private $breachDateRegex = '/Breach date: ([0-9]{1,2} [A-Z-a-z]+ [0-9]{4})/';
private $dateAddedRegex = '/Date added to HIBP: ([0-9]{1,2} [A-Z-a-z]+ [0-9]{4})/';
private $accountsRegex = '/Compromised accounts: ([0-9,]+)/';
private $breaches = array();
public function collectData() {
$html = getSimpleHTMLDOM(self::URI . '/PwnedWebsites')
or returnServerError('Could not request: ' . self::URI . '/PwnedWebsites');
$breaches = array();
foreach($html->find('div.row') as $breach) {
$item = array();
if ($breach->class != 'row') {
continue;
}
preg_match($this->breachDateRegex, $breach->find('p', 1)->plaintext, $breachDate)
or returnServerError('Could not extract details');
preg_match($this->dateAddedRegex, $breach->find('p', 1)->plaintext, $dateAdded)
or returnServerError('Could not extract details');
preg_match($this->accountsRegex, $breach->find('p', 1)->plaintext, $accounts)
or returnServerError('Could not extract details');
$permalink = $breach->find('p', 1)->find('a', 0)->href;
// Remove permalink
$breach->find('p', 1)->find('a', 0)->outertext = '';
$item['title'] = html_entity_decode($breach->find('h3', 0)->plaintext, ENT_QUOTES)
. ' - ' . $accounts[1] . ' breached accounts';
$item['dateAdded'] = strtotime($dateAdded[1]);
$item['breachDate'] = strtotime($breachDate[1]);
$item['uri'] = self::URI . '/PwnedWebsites' . $permalink;
$item['content'] = '<p>' . $breach->find('p', 0)->innertext . '</p>';
$item['content'] .= '<p>' . $this->breachType($breach) . '</p>';
$item['content'] .= '<p>' . $breach->find('p', 1)->innertext . '</p>';
$this->breaches[] = $item;
}
$this->orderBreaches();
$this->createItems();
}
/**
* Extract data breach type(s)
*/
private function breachType($breach) {
$content = '';
if ($breach->find('h3 > i', 0)) {
foreach ($breach->find('h3 > i') as $i) {
$content .= $i->title . '.<br>';
}
}
return $content;
}
/**
* Order Breaches by date added or date breached
*/
private function orderBreaches() {
$sortBy = $this->getInput('order');
$sort = array();
foreach ($this->breaches as $key => $item) {
$sort[$key] = $item[$sortBy];
}
array_multisort($sort, SORT_DESC, $this->breaches);
}
/**
* Create items from breaches array
*/
private function createItems() {
foreach ($this->breaches as $breach) {
$item = array();
$item['title'] = $breach['title'];
$item['timestamp'] = $breach[$this->getInput('order')];
$item['uri'] = $breach['uri'];
$item['content'] = $breach['content'];
$this->items[] = $item;
}
}
}

75
bridges/HeiseBridge.php Normal file
View File

@@ -0,0 +1,75 @@
<?php
class HeiseBridge extends FeedExpander {
const MAINTAINER = 'Dreckiger-Dan';
const NAME = 'Heise Online Bridge';
const URI = 'https://heise.de/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns the full articles instead of only the intro';
const PARAMETERS = array(array(
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'Alle News'
=> 'https://www.heise.de/newsticker/heise-atom.xml',
'Top-News'
=> 'https://www.heise.de/newsticker/heise-top-atom.xml',
'Internet-Störungen'
=> 'https://www.heise.de/netze/netzwerk-tools/imonitor-internet-stoerungen/feed/aktuelle-meldungen/',
'Alle News von heise Developer'
=> 'https://www.heise.de/developer/rss/news-atom.xml'
)
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'required' => false,
'title' => 'Specify number of full articles to return',
'defaultValue' => 5
)
));
const LIMIT = 5;
public function collectData() {
$this->collectExpandableDatas(
$this->getInput('category'),
$this->getInput('limit') ?: static::LIMIT
);
}
protected function parseItem($feedItem) {
$item = parent::parseItem($feedItem);
$uri = $item['uri'];
do {
$article = getSimpleHTMLDOMCached($uri)
or returnServerError('Could not open article: ' . $uri);
$article = defaultLinkTo($article, $uri);
$item = $this->addArticleToItem($item, $article);
if($next = $article->find('.pagination a[rel="next"]', 0))
$uri = $next->href;
} while ($next);
return $item;
}
private function addArticleToItem($item, $article) {
if($author = $article->find('[itemprop="author"]', 0))
$item['author'] = $author->plaintext;
$content = $article->find('div[class*="article-content"]', 0);
foreach($content->find('p, h3, ul, table, pre, img') as $element) {
$item['content'] .= $element;
}
foreach($content->find('img') as $img) {
$item['enclosures'][] = $img->src;
}
return $item;
}
}

245
bridges/IndeedBridge.php Normal file
View File

@@ -0,0 +1,245 @@
<?php
class IndeedBridge extends BridgeAbstract {
const NAME = 'Indeed';
const URI = 'https://www.indeed.com/';
const DESCRIPTION = 'Returns reviews and comments for a company of your choice';
const MAINTAINER = 'logmanoriginal';
const CACHE_TIMEOUT = 14400; // 4 hours
const PARAMETERS = array(
array(
'c' => array(
'name' => 'Company',
'type' => 'text',
'required' => true,
'title' => 'Company name',
'exampleValue' => 'GitHub',
)
),
'global' => array(
'language' => array(
'name' => 'Language Code',
'type' => 'list',
'title' => 'Choose your language code',
'defaultValue' => 'en-US',
'values' => array(
'es-AR' => 'es-AR',
'de-AT' => 'de-AT',
'en-AU' => 'en-AU',
'nl-BE' => 'nl-BE',
'fr-BE' => 'fr-BE',
'pt-BR' => 'pt-BR',
'en-CA' => 'en-CA',
'fr-CA' => 'fr-CA',
'de-CH' => 'de-CH',
'fr-CH' => 'fr-CH',
'es-CL' => 'es-CL',
'zh-CN' => 'zh-CN',
'es-CO' => 'es-CO',
'de-DE' => 'de-DE',
'es-ES' => 'es-ES',
'fr-FR' => 'fr-FR',
'en-GB' => 'en-GB',
'en-HK' => 'en-HK',
'en-IE' => 'en-IE',
'en-IN' => 'en-IN',
'it-IT' => 'it-IT',
'ja-JP' => 'ja-JP',
'ko-KR' => 'ko-KR',
'es-MX' => 'es-MX',
'nl-NL' => 'nl-NL',
'pl-PL' => 'pl-PL',
'en-SG' => 'en-SG',
'en-US' => 'en-US',
'en-ZA' => 'en-ZA',
'en-AE' => 'en-AE',
'da-DK' => 'da-DK',
'in-ID' => 'in-ID',
'en-MY' => 'en-MY',
'es-PE' => 'es-PE',
'en-PH' => 'en-PH',
'en-PK' => 'en-PK',
'ro-RO' => 'ro-RO',
'ru-RU' => 'ru-RU',
'tr-TR' => 'tr-TR',
'zh-TW' => 'zh-TW',
'vi-VN' => 'vi-VN',
'en-VN' => 'en-VN',
'ar-EG' => 'ar-EG',
'fr-MA' => 'fr-MA',
'en-NG' => 'en-NG',
)
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'title' => 'Maximum number of items to return',
'exampleValue' => 20,
)
)
);
const SITES = array(
'es-AR' => 'https://ar.indeed.com/',
'de-AT' => 'https://at.indeed.com/',
'en-AU' => 'https://au.indeed.com/',
'nl-BE' => 'https://be.indeed.com/',
'fr-BE' => 'https://emplois.be.indeed.com/',
'pt-BR' => 'https://www.indeed.com.br/',
'en-CA' => 'https://ca.indeed.com/',
'fr-CA' => 'https://emplois.ca.indeed.com/',
'de-CH' => 'https://www.indeed.ch/',
'fr-CH' => 'https://emplois.indeed.ch/',
'es-CL' => 'https://www.indeed.cl/',
'zh-CN' => 'https://cn.indeed.com/',
'es-CO' => 'https://co.indeed.com/',
'de-DE' => 'https://de.indeed.com/',
'es-ES' => 'https://www.indeed.es/',
'fr-FR' => 'https://www.indeed.fr/',
'en-GB' => 'https://www.indeed.co.uk/',
'en-HK' => 'https://www.indeed.hk/',
'en-IE' => 'https://ie.indeed.com/',
'en-IN' => 'https://www.indeed.co.in/',
'it-IT' => 'https://it.indeed.com/',
'ja-JP' => 'https://jp.indeed.com/',
'ko-KR' => 'https://kr.indeed.com/',
'es-MX' => 'https://www.indeed.com.mx/',
'nl-NL' => 'https://www.indeed.nl/',
'pl-PL' => 'https://pl.indeed.com/',
'en-SG' => 'https://www.indeed.com.sg/',
'en-US' => 'https://www.indeed.com/',
'en-ZA' => 'https://www.indeed.co.za/',
'en-AE' => 'https://www.indeed.ae/',
'da-DK' => 'https://dk.indeed.com/',
'in-ID' => 'https://id.indeed.com/',
'en-MY' => 'https://www.indeed.com.my/',
'es-PE' => 'https://www.indeed.com.pe/',
'en-PH' => 'https://www.indeed.com.ph/',
'en-PK' => 'https://www.indeed.com.pk/',
'ro-RO' => 'https://ro.indeed.com/',
'ru-RU' => 'https://ru.indeed.com/',
'tr-TR' => 'https://tr.indeed.com/',
'zh-TW' => 'https://tw.indeed.com/',
'vi-VN' => 'https://vn.indeed.com/',
'en-VN' => 'https://jobs.vn.indeed.com/',
'ar-EG' => 'https://eg.indeed.com/',
'fr-MA' => 'https://ma.indeed.com/',
'en-NG' => 'https://ng.indeed.com/',
);
private $title;
public function collectData() {
$url = $this->getURI();
$limit = $this->getInput('limit') ?: 20;
do {
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request ' . $url);
$html = defaultLinkTo($html, $url);
$this->title = $html->find('h1', 0)->innertext;
// Use local translation of the word "Rating"
$rating_local = $html->find('a[data-id="rating_desc"]', 0)->plaintext;
foreach($html->find('#cmp-content [id^="cmp-review-"]') as $review) {
$item = array();
$rating = $review->find('.cmp-ratingNumber', 0)->plaintext;
$title = $review->find('.cmp-review-title > span', 0)->plaintext;
$comment = $this->beautifyComment($review->find('.cmp-review-content-container', 0));
$item['uri'] = $review->find('.cmp-review-share-popup-item-link--copylink', 0)->href;
$item['title'] = "{$rating_local} {$rating} / {$title}";
$item['timestamp'] = $review->find('.cmp-review-date-created', 0)->plaintext;
$item['author'] = $review->find('.cmp-reviewer', 0)->plaintext;
$item['content'] = $comment;
//$item['enclosures']
$item['categories'][] = $review->find('.cmp-reviewer-job-location', 0)->plaintext;
//$item['uid']
$this->items[] = $item;
if(count($this->items) >= $limit) {
break;
}
}
// Break if no more pages available.
if($next = $html->find('a[data-tn-element="next-page"]', 0)) {
$url = $next->href;
} else {
break;
}
} while(count($this->items) < $limit);
}
public function getURI() {
if($this->getInput('language')
&& $this->getInput('c')) {
return self::SITES[$this->getInput('language')]
. 'cmp/'
. urlencode($this->getInput('c'))
. '/reviews';
}
return parent::getURI();
}
public function getName() {
return $this->title ?: parent::getName();
}
public function detectParameters($url) {
/**
* Expected: https://<...>.indeed.<...>/cmp/<company>[/reviews][/...]
*
* Note that most users will be redirected to their localized version
* of the page, which adds the language code to the host. For example,
* "en.indeed.com" or "www.indeed.fr" (see link[rel="alternate"]). At
* least each of the sites have ".indeed." in the name.
*/
if(filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false
|| stristr($url, '.indeed.') === false) {
return null;
}
$url_components = parse_url($url);
$path_segments = array_values(array_filter(explode('/', $url_components['path'])));
if(count($path_segments) < 2 || $path_segments[0] !== 'cmp') {
return null;
}
$language = array_search('https://' . $url_components['host'] . '/', self::SITES);
if($language === false) {
return null;
}
$limit = self::PARAMETERS['global']['limit']['defaultValue'] ?: 20;
$company = $path_segments[1];
return array(
'c' => $company,
'language' => $language,
'limit' => $limit,
);
}
private function beautifyComment($comment) {
foreach($comment->find('.cmp-bold') as $bold) {
$bold->tag = 'strong';
$bold->removeClass('cmp-bold');
}
return $comment;
}
}

View File

@@ -89,7 +89,7 @@ class InstagramBridge extends BridgeAbstract {
if (isset($media->edge_media_to_caption->edges[0]->node->text)) {
$textContent = $media->edge_media_to_caption->edges[0]->node->text;
} else {
$textContent = basename($media->display_url);
$textContent = '(no text)';
}
$item['title'] = ($media->is_video ? '▶ ' : '') . trim($textContent);
@@ -103,10 +103,11 @@ class InstagramBridge extends BridgeAbstract {
$item['content'] = $data[0];
$item['enclosures'] = $data[1];
} else {
$mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l';
$item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
$item['content'] .= '<img src="' . htmlentities($media->display_url) . '" alt="' . $item['title'] . '" />';
$item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />';
$item['content'] .= '</a><br><br>' . nl2br(htmlentities($textContent));
$item['enclosures'] = array($media->display_url);
$item['enclosures'] = array($mediaURI);
}
$item['timestamp'] = $media->taken_at_timestamp;

View File

@@ -1,8 +1,7 @@
<?php
/**
* This class implements a bridge for http://www.instructables.com, supporting
* general feeds and feeds by category. Instructables doesn't support HTTPS as
* of now (23.06.2018), so all connections are insecure!
* general feeds and feeds by category.
*
* Remarks:
* - For some reason it is very important to have the category URI end with a
@@ -13,7 +12,7 @@
*/
class InstructablesBridge extends BridgeAbstract {
const NAME = 'Instructables Bridge';
const URI = 'http://www.instructables.com';
const URI = 'https://www.instructables.com';
const DESCRIPTION = 'Returns general feeds and feeds by category';
const MAINTAINER = 'logmanoriginal';
const PARAMETERS = array(
@@ -22,219 +21,201 @@ class InstructablesBridge extends BridgeAbstract {
'name' => 'Category',
'type' => 'list',
'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/',
'Circuits' => array(
'All' => '/circuits/',
'Apple' => '/circuits/apple/projects/',
'Arduino' => '/circuits/arduino/projects/',
'Art' => '/circuits/art/projects/',
'Assistive Tech' => '/circuits/assistive-tech/projects/',
'Audio' => '/circuits/audio/projects/',
'Cameras' => '/circuits/cameras/projects/',
'Clocks' => '/circuits/clocks/projects/',
'Computers' => '/circuits/computers/projects/',
'Electronics' => '/circuits/electronics/projects/',
'Gadgets' => '/circuits/gadgets/projects/',
'Lasers' => '/circuits/lasers/projects/',
'LEDs' => '/circuits/leds/projects/',
'Linux' => '/circuits/linux/projects/',
'Microcontrollers' => '/circuits/microcontrollers/projects/',
'Microsoft' => '/circuits/microsoft/projects/',
'Mobile' => '/circuits/mobile/projects/',
'Raspberry Pi' => '/circuits/raspberry-pi/projects/',
'Remote Control' => '/circuits/remote-control/projects/',
'Reuse' => '/circuits/reuse/projects/',
'Robots' => '/circuits/robots/projects/',
'Sensors' => '/circuits/sensors/projects/',
'Software' => '/circuits/software/projects/',
'Soldering' => '/circuits/soldering/projects/',
'Speakers' => '/circuits/speakers/projects/',
'Tools' => '/circuits/tools/projects/',
'USB' => '/circuits/usb/projects/',
'Wearables' => '/circuits/wearables/projects/',
'Websites' => '/circuits/websites/projects/',
'Wireless' => '/circuits/wireless/projects/',
),
'Workshop' => array(
'All' => '/workshop/',
'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/',
'3D Printing' => '/workshop/3d-printing/projects/',
'Cars' => '/workshop/cars/projects/',
'CNC' => '/workshop/cnc/projects/',
'Electric Vehicles' => '/workshop/electric-vehicles/projects/',
'Energy' => '/workshop/energy/projects/',
'Furniture' => '/workshop/furniture/projects/',
'Home Improvement' => '/workshop/home-improvement/projects/',
'Home Theater' => '/workshop/home-theater/projects/',
'Hydroponics' => '/workshop/hydroponics/projects/',
'Knives' => '/workshop/knives/projects/',
'Laser Cutting' => '/workshop/laser-cutting/projects/',
'Lighting' => '/workshop/lighting/projects/',
'Metalworking' => '/workshop/metalworking/projects/',
'Molds & Casting' => '/workshop/molds-and-casting/projects/',
'Motorcycles' => '/workshop/motorcycles/projects/',
'Organizing' => '/workshop/organizing/projects/',
'Pallets' => '/workshop/pallets/projects/',
'Repair' => '/workshop/repair/projects/',
'Science' => '/workshop/science/projects/',
'Shelves' => '/workshop/shelves/projects/',
'Solar' => '/workshop/solar/projects/',
'Tools' => '/workshop/tools/projects/',
'Woodworking' => '/workshop/woodworking/projects/',
'Workbenches' => '/workshop/workbenches/projects/',
),
'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/',
'Craft' => array(
'All' => '/craft/',
'Art' => '/craft/art/projects/',
'Books & Journals' => '/craft/books-and-journals/projects/',
'Cardboard' => '/craft/cardboard/projects/',
'Cards' => '/craft/cards/projects/',
'Clay' => '/craft/clay/projects/',
'Costumes & Cosplay' => '/craft/costumes-and-cosplay/projects/',
'Digital Graphics' => '/craft/digital-graphics/projects/',
'Duct Tape' => '/craft/duct-tape/projects/',
'Embroidery' => '/craft/embroidery/projects/',
'Fashion' => '/craft/fashion/projects/',
'Felt' => '/craft/felt/projects/',
'Fiber Arts' => '/craft/fiber-arts/projects/',
'Gift Wrapping' => '/craft/gift-wrapping/projects/',
'Jewelry' => '/craft/jewelry/projects/',
'Knitting & Crochet' => '/craft/knitting-and-crochet/projects/',
'Leather' => '/craft/leather/projects/',
'Mason Jars' => '/craft/mason-jars/projects/',
'No-Sew' => '/craft/no-sew/projects/',
'Paper' => '/craft/paper/projects/',
'Parties & Weddings' => '/craft/parties-and-weddings/projects/',
'Photography' => '/craft/photography/projects/',
'Printmaking' => '/craft/printmaking/projects/',
'Reuse' => '/craft/reuse/projects/',
'Sewing' => '/craft/sewing/projects/',
'Soapmaking' => '/craft/soapmaking/projects/',
'Wallets' => '/craft/wallets/projects/',
),
'Cooking' => array(
'All' => '/cooking/',
'Bacon' => '/cooking/bacon/projects/',
'BBQ & Grilling' => '/cooking/bbq-and-grilling/projects/',
'Beverages' => '/cooking/beverages/projects/',
'Bread' => '/cooking/bread/projects/',
'Breakfast' => '/cooking/breakfast/projects/',
'Cake' => '/cooking/cake/projects/',
'Candy' => '/cooking/candy/projects/',
'Canning & Preserving' => '/cooking/canning-and-preserving/projects/',
'Cocktails & Mocktails' => '/cooking/cocktails-and-mocktails/projects/',
'Coffee' => '/cooking/coffee/projects/',
'Cookies' => '/cooking/cookies/projects/',
'Cupcakes' => '/cooking/cupcakes/projects/',
'Dessert' => '/cooking/dessert/projects/',
'Homebrew' => '/cooking/homebrew/projects/',
'Main Course' => '/cooking/main-course/projects/',
'Pasta' => '/cooking/pasta/projects/',
'Pie' => '/cooking/pie/projects/',
'Pizza' => '/cooking/pizza/projects/',
'Salad' => '/cooking/salad/projects/',
'Sandwiches' => '/cooking/sandwiches/projects/',
'Snacks & Appetizers' => '/cooking/snacks-and-appetizers/projects/',
'Soups & Stews' => '/cooking/soups-and-stews/projects/',
'Vegetarian & Vegan' => '/cooking/vegetarian-and-vegan/projects/',
),
'Living' => array(
'All' => '/living/',
'Beauty' => '/living/beauty/projects/',
'Christmas' => '/living/christmas/projects/',
'Cleaning' => '/living/cleaning/projects/',
'Decorating' => '/living/decorating/projects/',
'Education' => '/living/education/projects/',
'Gardening' => '/living/gardening/projects/',
'Halloween' => '/living/halloween/projects/',
'Health' => '/living/health/projects/',
'Hiding Places' => '/living/hiding-places/projects/',
'Holidays' => '/living/holidays/projects/',
'Homesteading' => '/living/homesteading/projects/',
'Kids' => '/living/kids/projects/',
'Kitchen' => '/living/kitchen/projects/',
'LEGO & KNEX' => '/living/lego-and-knex/projects/',
'Life Hacks' => '/living/life-hacks/projects/',
'Music' => '/living/music/projects/',
'Office Supply Hacks' => '/living/office-supply-hacks/projects/',
'Organizing' => '/living/organizing/projects/',
'Pest Control' => '/living/pest-control/projects/',
'Pets' => '/living/pets/projects/',
'Pranks, Tricks, & Humor' => '/living/pranks-tricks-and-humor/projects/',
'Relationships' => '/living/relationships/projects/',
'Toys & Games' => '/living/toys-and-games/projects/',
'Travel' => '/living/travel/projects/',
'Video Games' => '/living/video-games/projects/',
),
'Outside' => array(
'All' => '/outside/',
'Bikes' => '/outside/bikes/',
'Survival' => '/outside/survival/',
'Backyard' => '/outside/backyard/',
'Beach' => '/outside/beach/',
'Birding' => '/outside/birding/',
'Boats' => '/outside/boats/',
'Camping' => '/outside/camping/',
'Climbing' => '/outside/climbing/',
'Fire' => '/outside/fire/',
'Fishing' => '/outside/fishing/',
'Hunting' => '/outside/hunting/',
'Kites' => '/outside/kites/',
'Knives' => '/outside/knives/',
'Knots' => '/outside/knots/',
'Paracord' => '/outside/paracord/',
'Rockets' => '/outside/rockets/',
'Skateboarding' => '/outside/skateboarding/',
'Snow' => '/outside/snow/',
'Water' => '/outside/water/',
'Backyard' => '/outside/backyard/projects/',
'Beach' => '/outside/beach/projects/',
'Bikes' => '/outside/bikes/projects/',
'Birding' => '/outside/birding/projects/',
'Boats' => '/outside/boats/projects/',
'Camping' => '/outside/camping/projects/',
'Climbing' => '/outside/climbing/projects/',
'Fire' => '/outside/fire/projects/',
'Fishing' => '/outside/fishing/projects/',
'Hunting' => '/outside/hunting/projects/',
'Kites' => '/outside/kites/projects/',
'Knots' => '/outside/knots/projects/',
'Launchers' => '/outside/launchers/projects/',
'Paracord' => '/outside/paracord/projects/',
'Rockets' => '/outside/rockets/projects/',
'Siege Engines' => '/outside/siege-engines/projects/',
'Skateboarding' => '/outside/skateboarding/projects/',
'Snow' => '/outside/snow/projects/',
'Sports' => '/outside/sports/projects/',
'Survival' => '/outside/survival/projects/',
'Water' => '/outside/water/projects/',
),
'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/',
'Makeymakey' => array(
'All' => '/makeymakey/',
'Makey Makey on Instructables' => '/makeymakey/',
),
'Teachers' => array(
'All' => '/teachers/',
'ELA' => '/teachers/ela/projects/',
'Math' => '/teachers/math/projects/',
'Science' => '/teachers/science/projects/',
'Social Studies' => '/teachers/social-studies/projects/',
'Engineering' => '/teachers/engineering/projects/',
'Coding' => '/teachers/coding/projects/',
'Electronics' => '/teachers/electronics/projects/',
'Robotics' => '/teachers/robotics/projects/',
'Arduino' => '/teachers/arduino/projects/',
'CNC' => '/teachers/cnc/projects/',
'Laser Cutting' => '/teachers/laser-cutting/projects/',
'3D Printing' => '/teachers/3d-printing/projects/',
'3D Design' => '/teachers/3d-design/projects/',
'Art' => '/teachers/art/projects/',
'Music' => '/teachers/music/projects/',
'Theatre' => '/teachers/theatre/projects/',
'Wood Shop' => '/teachers/wood-shop/projects/',
'Metal Shop' => '/teachers/metal-shop/projects/',
'Resources' => '/teachers/resources/projects/',
),
'Costumes' => array(
'All' => '/costumes/',
'Props' => '/costumes/props-and-accessories/',
'Animals' => '/costumes/animals/',
'Comics' => '/costumes/comics/',
'Fantasy' => '/costumes/fantasy/',
'For Kids' => '/costumes/for-kids/',
'For Pets' => '/costumes/for-pets/',
'Funny' => '/costumes/funny/',
'Games' => '/costumes/games/',
'Historic & Futuristic' => '/costumes/historic-and-futuristic/',
'Makeup' => '/costumes/makeup/',
'Masks' => '/costumes/masks/',
'Scary' => '/costumes/scary/',
'TV & Movies' => '/costumes/tv-and-movies/',
'Weapons & Armor' => '/costumes/weapons-and-armor/',
)
),
'title' => 'Select your category (required)',
'defaultValue' => 'Technology'
'defaultValue' => 'Circuits'
),
'filter' => array(
'name' => 'Filter',
@@ -252,65 +233,70 @@ class InstructablesBridge extends BridgeAbstract {
)
);
private $uri;
public function collectData() {
// Enable the following line to get the category list (dev mode)
// $this->listCategories();
$this->uri = static::URI;
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Error loading category ' . $this->getURI());
$html = defaultLinkTo($html, $this->getURI());
switch($this->queriedContext) {
case 'Category': $this->uri .= $this->getInput('category') . $this->getInput('filter');
}
$covers = $html->find('
.category-projects-list > div,
.category-landing-projects-list > div,
');
$html = getSimpleHTMLDOM($this->uri)
or returnServerError('Error loading category ' . $this->uri);
foreach($html->find('ul.explore-covers-list li') as $cover) {
foreach($covers as $cover) {
$item = array();
$item['uri'] = static::URI . $cover->find('a.cover-image', 0)->href;
$item['title'] = $cover->find('.title', 0)->innertext;
$item['uri'] = $cover->find('a.ible-title', 0)->href;
$item['title'] = $cover->find('a.ible-title', 0)->innertext;
$item['author'] = $this->getCategoryAuthor($cover);
$item['content'] = '<a href='
. $item['uri']
. '><img src='
. $cover->find('a.cover-image img', 0)->src
. $cover->find('img', 0)->getAttribute('data-src')
. '></a>';
$image = str_replace('.RECTANGLE1', '.LARGE', $cover->find('a.cover-image img', 0)->src);
$item['enclosures'] = [$image];
$item['enclosures'][] = str_replace(
'.RECTANGLE1',
'.LARGE',
$cover->find('img', 0)->getAttribute('data-src')
);
$this->items[] = $item;
}
}
public function getName() {
if(!is_null($this->getInput('category'))
&& !is_null($this->getInput('filter'))) {
foreach(self::PARAMETERS[$this->queriedContext]['category']['values'] as $key => $value) {
$subcategory = array_search($this->getInput('category'), $value);
switch($this->queriedContext) {
case 'Category': {
foreach(self::PARAMETERS[$this->queriedContext]['category']['values'] as $key => $value) {
$subcategory = array_search($this->getInput('category'), $value);
if($subcategory !== false)
break;
}
if($subcategory !== false)
break;
}
$filter = array_search(
$this->getInput('filter'),
self::PARAMETERS[$this->queriedContext]['filter']['values']
);
$filter = array_search(
$this->getInput('filter'),
self::PARAMETERS[$this->queriedContext]['filter']['values']
);
return $subcategory . ' (' . $filter . ') - ' . static::NAME;
return $subcategory . ' (' . $filter . ') - ' . static::NAME;
} break;
}
return parent::getName();
}
public function getURI() {
if(!is_null($this->getInput('category'))
&& !is_null($this->getInput('filter'))) {
return $this->uri;
switch($this->queriedContext) {
case 'Category': {
return self::URI
. $this->getInput('category')
. $this->getInput('filter');
} break;
}
return parent::getURI();
@@ -321,24 +307,32 @@ class InstructablesBridge extends BridgeAbstract {
* parameters list)
*/
private function listCategories(){
// Use arbitrary category to receive full list
$html = getSimpleHTMLDOM(self::URI . '/technology/');
foreach($html->find('.channel a') as $channel) {
$name = html_entity_decode(trim($channel->innertext));
// Use home page to acquire main categories
$html = getSimpleHTMLDOM(self::URI);
$html = defaultLinkTo($html, self::URI);
// Remove unwanted entities
$name = str_replace("'", '', $name);
$name = str_replace('&#39;', '', $name);
foreach($html->find('.home-content-explore-link') as $category) {
$uri = $channel->href;
// Use arbitrary category to receive full list
$html = getSimpleHTMLDOM($category->href);
$category = explode('/', $uri)[1];
foreach($html->find('.channel-thumbnail a') as $channel) {
$name = html_entity_decode(trim($channel->title));
if(!isset($categories)
|| !array_key_exists($category, $categories)
|| !in_array($uri, $categories[$category]))
$categories[$category][$name] = $uri;
// Remove unwanted entities
$name = str_replace("'", '', $name);
$name = str_replace('&#39;', '', $name);
$uri = $channel->href;
$category_name = explode('/', $uri)[1];
if(!isset($categories)
|| !array_key_exists($category_name, $categories)
|| !in_array($uri, $categories[$category_name]))
$categories[$category_name][$name] = $uri;
}
}
// Build PHP array manually
@@ -360,9 +354,9 @@ class InstructablesBridge extends BridgeAbstract {
*/
private function getCategoryAuthor($cover) {
return '<a href='
. static::URI . $cover->find('span.author a', 0)->href
. $cover->find('.ible-author a', 0)->href
. '>'
. $cover->find('span.author a', 0)->innertext
. $cover->find('.ible-author a', 0)->innertext
. '</a>';
}
}

View File

@@ -0,0 +1,293 @@
<?php
class InternetArchiveBridge extends BridgeAbstract {
const NAME = 'Internet Archive Bridge';
const URI = 'https://archive.org';
const DESCRIPTION = 'Returns newest uploads, posts and more from an account';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(
'Account' => array(
'username' => array(
'name' => 'Username',
'type' => 'text',
'required' => true,
'exampleValue' => '@verifiedjoseph',
),
'content' => array(
'name' => 'Content',
'type' => 'list',
'values' => array(
'Uploads' => 'uploads',
'Posts' => 'posts',
'Reviews' => 'reviews',
'Collections' => 'collections',
'Web Archives' => 'web-archive',
),
'defaultValue' => 'uploads',
)
)
);
const CACHE_TIMEOUT = 900; // 15 mins
private $skipClasses = array(
'item-ia mobile-header hidden-tiles',
'item-ia account-ia'
);
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request: ' . $this->getURI());
$html = defaultLinkTo($html, $this->getURI());
if ($this->getInput('content') !== 'posts') {
$detailsDivNumber = 0;
foreach ($html->find('div.results > div[data-id]') as $index => $result) {
$item = array();
if (in_array($result->class, $this->skipClasses)) {
continue;
}
switch($result->class) {
case 'item-ia':
switch($this->getInput('content')) {
case 'reviews':
$item = $this->processReview($result);
break;
case 'uploads':
$item = $this->processUpload($result);
break;
}
break;
case 'item-ia url-item':
$item = $this->processWebArchives($result);
break;
case 'item-ia collection-ia':
$item = $this->processCollection($result);
break;
}
if ($this->getInput('content') !== 'reviews') {
$hiddenDetails = $this->processHiddenDetails($html, $detailsDivNumber, $item);
$this->items[] = array_merge($item, $hiddenDetails);
} else {
$this->items[] = $item;
}
$detailsDivNumber++;
}
}
if ($this->getInput('content') === 'posts') {
$this->items = $this->processPosts($html);
}
}
public function getURI() {
if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) {
return self::URI . '/details/' . $this->processUsername() . '&tab=' . $this->getInput('content');
}
return parent::getURI();
}
public function getName() {
if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) {
$contentValues = array_flip(self::PARAMETERS['Account']['content']['values']);
return $contentValues[$this->getInput('content')] . ' - '
. $this->processUsername() . ' - Internet Archive';
}
return parent::getName();
}
private function processUsername() {
if (substr($this->getInput('username'), 0, 1) !== '@') {
return '@' . $this->getInput('username');
}
return $this->getInput('username');
}
private function processUpload($result) {
$item = array();
$collection = $result->find('a.stealth', 0);
$collectionLink = self::URI . $collection->href;
$collectionTitle = $collection->find('div.item-parent-ttl', 0)->plaintext;
$item['title'] = trim($result->find('div.ttl', 0)->innertext);
$item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
$item['uri'] = self::URI . $result->find('div.item-ttl.C.C2 > a', 0)->href;
if ($result->find('div.by.C.C4', 0)->children(2)) {
$item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext;
}
$item['content'] = <<<EOD
<p>Media Type: {$result->attr['data-mediatype']}<br>
Collection: <a href="{$collectionLink}">{$collectionTitle}</a></p>
EOD;
$item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
return $item;
}
private function processReview($result) {
$item = array();
$item['title'] = trim($result->find('div.ttl', 0)->innertext);
$item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
$item['uri'] = $result->find('div.review-title', 0)->children(0)->href;
if ($result->find('div.by.C.C4', 0)->children(2)) {
$item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext;
}
$item['content'] = <<<EOD
<p><strong>Subject: {$result->find('div.review-title', 0)->plaintext}</strong></p>
<p>{$result->find('div.hidden-lists.review' , 0)->children(1)->plaintext}</p>
EOD;
$item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
return $item;
}
private function processWebArchives($result) {
$item = array();
$item['title'] = trim($result->find('div.ttl', 0)->plaintext);
$item['timestamp'] = strtotime($result->find('div.hidden-lists', 0)->children(0)->plaintext);
$item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href;
$item['content'] = <<<EOD
{$this->processUsername()} archived <a href="{$item['uri']}">{$result->find('div.ttl', 0)->plaintext}</a>
EOD;
$item['enclosures'][] = $result->find('img.item-img', 0)->source;
return $item;
}
private function processCollection($result) {
$item = array();
$title = trim($result->find('div.collection-title.C.C2', 0)->children(0)->plaintext);
$itemCount = strtolower(trim($result->find('div.num-items.topinblock', 0)->plaintext));
$item['title'] = $title . ' (' . $itemCount . ')';
$item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
$item['uri'] = $result->find('div.collection-title.C.C2 > a', 0)->href;
$item['content'] = '';
if ($result->find('img.item-img', 0)) {
$item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source;
}
return $item;
}
private function processHiddenDetails($html, $detailsDivNumber, $item) {
$description = '';
if ($html->find('div.details-ia.hidden-tiles', $detailsDivNumber)) {
$detailsDiv = $html->find('div.details-ia.hidden-tiles', $detailsDivNumber);
if ($detailsDiv->find('div.C234', 0)->children(0)) {
$description = $detailsDiv->find('div.C234', 0)->children(0)->plaintext;
$detailsDiv->find('div.C234', 0)->children(0)->innertext = '';
}
$topics = trim($detailsDiv->find('div.C234', 0)->plaintext);
if (!empty($topics)) {
$topics = trim($detailsDiv->find('div.C234', 0)->plaintext);
$topics = trim(substr($topics, 7));
$item['categories'] = explode(',', $topics);
}
$item['content'] = '<p>' . $description . '</p>' . $item['content'];
}
return $item;
}
private function processPosts($html) {
$items = array();
foreach ($html->find('table.forumTable > tr') as $index => $tr) {
$item = array();
if ($index === 0) {
continue;
}
$item['title'] = $tr->find('td', 0)->plaintext;
$item['timestamp'] = strtotime($tr->find('td', 4)->children(0)->plaintext);
$item['uri'] = $tr->find('td', 0)->children(0)->href;
$formLink = <<<EOD
<a href="{$tr->find('td', 2)->children(0)->href}">{$tr->find('td', 2)->children(0)->plaintext}</a>
EOD;
$postDate = $tr->find('td', 4)->children(0)->plaintext;
$postPageHtml = getSimpleHTMLDOMCached($item['uri'], 3600)
or returnServerError('Could not request: ' . $item['uri']);
$postPageHtml = defaultLinkTo($postPageHtml, $this->getURI());
$post = $postPageHtml->find('div.box.well.well-sm', 0);
$parentLink = '';
$replyLink = <<<EOD
<a href="{$post->find('a', 0)->href}">Reply</a>
EOD;
if ($post->find('a', 1)->innertext = 'See parent post') {
$parentLink = <<<EOD
<a href="{$post->find('a', 1)->href}">View parent post</a>
EOD;
}
$post->find('h1', 0)->outertext = '';
$post->find('h2', 0)->outertext = '';
$item['content'] = <<<EOD
<p>{$post->innertext}</p>{$replyLink} - {$parentLink} - Posted in {$formLink} on {$postDate}
EOD;
$items[] = $item;
if (count($items) >= 10) {
break;
}
}
return $items;
}
}

128
bridges/IvooxBridge.php Normal file
View File

@@ -0,0 +1,128 @@
<?php
/**
* IvooxRssBridge
* Returns the latest search result
* TODO: support podcast episodes list
*/
class IvooxBridge extends BridgeAbstract {
const NAME = 'Ivoox Bridge';
const URI = 'https://www.ivoox.com/';
const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the 10 newest episodes by keyword search';
const MAINTAINER = 'xurxof'; // based on YoutubeBridge by mitsukarenai
const PARAMETERS = array(
'Search result' => array(
's' => array(
'name' => 'keyword',
'exampleValue' => 'test'
)
)
);
private function ivBridgeAddItem(
$episode_link,
$podcast_name,
$episode_title,
$author_name,
$episode_description,
$publication_date,
$episode_duration) {
$item = array();
$item['title'] = htmlspecialchars_decode($podcast_name . ': ' . $episode_title);
$item['author'] = $author_name;
$item['timestamp'] = $publication_date;
$item['uri'] = $episode_link;
$item['content'] = '<a href="' . $episode_link . '">' . $podcast_name . ': ' . $episode_title
. '</a><br />Duration: ' . $episode_duration
. '<br />Description:<br />' . $episode_description;
$this->items[] = $item;
}
private function ivBridgeParseHtmlListing($html) {
$limit = 4;
$count = 0;
foreach($html->find('div.flip-container') as $flipper) {
$linkcount = 0;
if(!empty($flipper->find( 'div.modulo-type-banner' ))) {
// ad
continue;
}
if($count < $limit) {
foreach($flipper->find('div.header-modulo') as $element) {
foreach($element->find('a') as $link) {
if ($linkcount == 0) {
$episode_link = $link->href;
$episode_title = $link->title;
} elseif ($linkcount == 1) {
$author_link = $link->href;
$author_name = $link->title;
} elseif ($linkcount == 2) {
$podcast_link = $link->href;
$podcast_name = $link->title;
}
$linkcount++;
}
}
$episode_description = $flipper->find('button.btn-link', 0)->getAttribute('data-content');
$episode_duration = $flipper->find('p.time', 0)->innertext;
$publication_date = $flipper->find('li.date', 0)->getAttribute('title');
// alternative date_parse_from_format
// or DateTime::createFromFormat('G:i - d \d\e M \d\e Y', $publication);
// TODO: month name translations, due function doesn't support locale
$a = strptime($publication_date, '%H:%M - %d de %b. de %Y'); // obsolete function, uses c libraries
$publication_date = mktime(0, 0, 0, $a['tm_mon'] + 1, $a['tm_mday'], $a['tm_year'] + 1900);
$this->ivBridgeAddItem(
$episode_link,
$podcast_name,
$episode_title,
$author_name,
$episode_description,
$publication_date,
$episode_duration
);
$count++;
}
}
}
public function collectData() {
// store locale, change to spanish
$originalLocales = explode(';', setlocale(LC_ALL, 0));
setlocale(LC_ALL, 'es_ES.utf8');
$xml = '';
$html = '';
$url_feed = '';
if($this->getInput('s')) { /* Search modes */
$this->request = str_replace(' ', '-', $this->getInput('s'));
$url_feed = self::URI . urlencode($this->request) . '_sb_f_1.html?o=uploaddate';
} else {
returnClientError('Not valid mode at IvooxBridge');
}
$dom = getSimpleHTMLDOM($url_feed)
or returnServerError('Could not request ' . $url_feed);
$this->ivBridgeParseHtmlListing($dom);
// restore locale
foreach($originalLocales as $localeSetting) {
if(strpos($localeSetting, '=') !== false) {
list($category, $locale) = explode('=', $localeSetting);
} else {
$category = LC_ALL;
$locale = $localeSetting;
}
setlocale($category, $locale);
}
}
}

View File

@@ -24,6 +24,16 @@ class KununuBridge extends BridgeAbstract {
'type' => 'checkbox',
'exampleValue' => 'checked',
'title' => 'Activate to load full article'
),
'include_ratings' => array(
'name' => 'Include ratings',
'type' => 'checkbox',
'title' => 'Activate to include ratings in the feed'
),
'include_benefits' => array(
'name' => 'Include benefits',
'type' => 'checkbox',
'title' => 'Activate to include benefits in the feed'
)
),
array(
@@ -116,7 +126,7 @@ class KununuBridge extends BridgeAbstract {
$item = array();
$item['author'] = $this->extractArticleAuthorPosition($article);
$item['timestamp'] = strtotime($date);
$item['timestamp'] = strtotime($date->content);
$item['title'] = $rating->getAttribute('aria-label')
. ' : '
. strip_tags($summary->innertext);
@@ -175,7 +185,32 @@ class KununuBridge extends BridgeAbstract {
$description = $article->find('[itemprop=reviewBody]', 0)
or returnServerError('Cannot find article description!');
return $description->innertext;
$retVal = $description->innertext;
if($this->getInput('include_ratings')
&& ($ratings = $article->find('.review-ratings .rating-group'))) {
$retVal .= (empty($retVal) ? '' : '<hr>') . '<table>';
foreach($ratings as $rating) {
$retVal .= <<<EOD
<tr>
<td>{$rating->find('.rating-title', 0)->plaintext}
<td>{$rating->find('.rating-badge', 0)->plaintext}
</tr>
EOD;
}
$retVal .= '</table>';
}
if($this->getInput('include_benefits')
&& ($benefits = $article->find('benefit'))) {
$retVal .= (empty($retVal) ? '' : '<hr>') . '<ul>';
foreach($benefits as $benefit) {
$retVal .= "<li>{$benefit->plaintext}</li>";
}
$retVal .= '</ul>';
}
return $retVal;
}
/**

View File

@@ -0,0 +1,89 @@
<?php
class MastodonBridge extends FeedExpander {
const MAINTAINER = 'husim0';
const NAME = 'Mastodon Bridge';
const CACHE_TIMEOUT = 900; // 15mn
const DESCRIPTION = 'Returns toots';
const URI = 'https://mastodon.social';
const PARAMETERS = array(array(
'canusername' => array(
'name' => 'Canonical username (ex : @sebsauvage@framapiaf.org)',
'required' => true,
),
'norep' => array(
'name' => 'Without replies',
'type' => 'checkbox',
'title' => 'Only return initial toots'
),
'noboost' => array(
'name' => 'Without boosts',
'required' => false,
'type' => 'checkbox',
'title' => 'Hide boosts'
)
));
public function getName(){
switch($this->queriedContext) {
case 'By username':
return $this->getInput('canusername');
default: return parent::getName();
}
}
protected function parseItem($newItem){
$item = parent::parseItem($newItem);
$content = str_get_html($item['content']);
$title = str_get_html($item['title']);
$item['title'] = $content->plaintext;
if(strlen($item['title']) > 75) {
$item['title'] = substr($item['title'], 0, strpos(wordwrap($item['title'], 75), "\n")) . '...';
}
if(strpos($title, 'shared a status by') !== false) {
if($this->getInput('noboost')) {
return null;
}
preg_match('/shared a status by (\S{0,})/', $title, $matches);
$item['title'] = 'Boost ' . $matches[1] . ' ' . $item['title'];
$item['author'] = $matches[1];
} else {
$item['author'] = $this->getInput('canusername');
}
// Check if it's a initial toot or a response
if($this->getInput('norep') && preg_match('/^@.+/', trim($content->plaintext))) {
return null;
}
return $item;
}
private function getInstance(){
preg_match('/^@[a-zA-Z0-9_]+@(.+)/', $this->getInput('canusername'), $matches);
return $matches[1];
}
private function getUsername(){
preg_match('/^@([a-zA-Z_0-9_]+)@.+/', $this->getInput('canusername'), $matches);
return $matches[1];
}
public function getURI(){
if($this->getInput('canusername'))
return 'https://' . $this->getInstance() . '/users/' . $this->getUsername() . '.atom';
return parent::getURI();
}
public function collectData(){
return $this->collectExpandableDatas($this->getURI());
}
}

View File

@@ -0,0 +1,60 @@
<?php
class MediapartBridge extends FeedExpander {
const MAINTAINER = 'killruana';
const NAME = 'Mediapart Bridge';
const URI = 'https://www.mediapart.fr/';
const PARAMETERS = array(
array(
'single_page_mode' => array(
'name' => 'Single page article',
'type' => 'checkbox',
'title' => 'Display long articles on a single page',
'defaultValue' => 'checked'
),
'mpsessid' => array(
'name' => 'MPSESSID',
'type' => 'text',
'title' => 'Value of the session cookie MPSESSID'
)
)
);
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Returns the newest articles.';
public function collectData() {
$url = self::URI . 'articles/feed';
$this->collectExpandableDatas($url);
}
protected function parseItem($newsItem) {
$item = parent::parseItem($newsItem);
// Enable single page mode?
if ($this->getInput('single_page_mode') === true) {
$item['uri'] .= '?onglet=full';
}
// If a session cookie is defined, get the full article
$mpsessid = $this->getInput('mpsessid');
if (!empty($mpsessid)) {
// Set the session cookie
$opt = array();
$opt[CURLOPT_COOKIE] = 'MPSESSID=' . $mpsessid;
// Get the page
$articlePage = getSimpleHTMLDOM(
$newsItem->link . '?onglet=full',
array(),
$opt);
// Extract the article content
$content = $articlePage->find('div.content-article', 0)->innertext;
$content = sanitize($content);
$content = defaultLinkTo($content, static::URI);
$item['content'] .= $content;
}
return $item;
}
}

View File

@@ -0,0 +1,153 @@
<?php
class MozillaBugTrackerBridge extends BridgeAbstract {
const NAME = 'Mozilla Bug Tracker';
const URI = 'https://bugzilla.mozilla.org';
const DESCRIPTION = 'Returns feeds for bug comments';
const MAINTAINER = 'AntoineTurmel';
const PARAMETERS = array(
'Bug comments' => array(
'id' => array(
'name' => 'Bug tracking ID',
'type' => 'number',
'required' => true,
'title' => 'Insert bug tracking ID',
'exampleValue' => 121241
),
'limit' => array(
'name' => 'Number of comments to return',
'type' => 'number',
'required' => false,
'title' => 'Specify number of comments to return',
'defaultValue' => -1
),
'sorting' => array(
'name' => 'Sorting',
'type' => 'list',
'required' => false,
'title' => 'Defines the sorting order of the comments returned',
'defaultValue' => 'of',
'values' => array(
'Oldest first' => 'of',
'Latest first' => 'lf'
)
)
)
);
private $bugid = '';
private $bugdesc = '';
public function getIcon() {
return self::URI . '/extensions/BMO/web/images/favicon.ico';
}
public function collectData(){
$limit = $this->getInput('limit');
$sorting = $this->getInput('sorting');
// We use the print preview page for simplicity
$html = getSimpleHTMLDOMCached($this->getURI() . '&format=multiple',
86400,
null,
null,
true,
true,
DEFAULT_TARGET_CHARSET,
false, // Do NOT remove line breaks
DEFAULT_BR_TEXT,
DEFAULT_SPAN_TEXT);
if($html === false)
returnServerError('Failed to load page!');
// Store header information into private members
$this->bugid = $html->find('#bugzilla-body', 0)->find('a', 0)->innertext;
$this->bugdesc = $html->find('table.bugfields', 0)->find('tr', 0)->find('td', 0)->innertext;
// Get and limit comments
$comments = $html->find('.bz_comment_table div.bz_comment');
if($limit > 0 && count($comments) > $limit) {
$comments = array_slice($comments, count($comments) - $limit, $limit);
}
// Order comments
switch($sorting) {
case 'lf': $comments = array_reverse($comments, true);
case 'of':
default: // Nothing to do, keep original order
}
foreach($comments as $comment) {
$comment = $this->inlineStyles($comment);
$item = array();
$item['uri'] = $this->getURI() . '#' . $comment->id;
$item['author'] = $comment->find('span.bz_comment_user', 0)->innertext;
$item['title'] = $comment->find('span.bz_comment_number', 0)->find('a', 0)->innertext;
$item['timestamp'] = strtotime($comment->find('span.bz_comment_time', 0)->innertext);
$item['content'] = $comment->find('pre.bz_comment_text', 0)->innertext;
// Fix line breaks (they use LF)
$item['content'] = str_replace("\n", '<br>', $item['content']);
// Fix relative URIs
$item['content'] = $this->replaceRelativeURI($item['content']);
$this->items[] = $item;
}
}
public function getURI(){
switch($this->queriedContext) {
case 'Bug comments':
return parent::getURI()
. '/show_bug.cgi?id='
. $this->getInput('id');
break;
default: return parent::getURI();
}
}
public function getName(){
switch($this->queriedContext) {
case 'Bug comments':
return 'Bug '
. $this->bugid
. ' tracker for '
. $this->bugdesc
. ' - '
. parent::getName();
break;
default: return parent::getName();
}
}
/**
* Replaces all relative URIs with absolute ones
*
* @param string $content The source string
* @return string Returns the source string with all relative URIs replaced
* by absolute ones.
*/
private function replaceRelativeURI($content){
return preg_replace('/href="(?!http)/', 'href="' . self::URI . '/', $content);
}
/**
* Adds styles as attributes to tags with known classes
*
* @param object $html A simplehtmldom object
* @return object Returns the original object with styles added as
* attributes.
*/
private function inlineStyles($html){
foreach($html->find('.bz_obsolete') as $element) {
$element->style = 'text-decoration:line-through;';
}
return $html;
}
}

View File

@@ -0,0 +1,194 @@
<?php
class NationalGeographicBridge extends BridgeAbstract {
const CONTEXT_BY_TOPIC = 'By Topic';
const PARAMETER_TOPIC = 'topic';
const PARAMETER_FULL_ARTICLE = 'full';
const TOPIC_MAGAZINE = 'Magazine';
const TOPIC_LATEST_STORIES = 'Latest Stories';
const NAME = 'National Geographic';
const URI = 'https://www.nationalgeographic.com/';
const DESCRIPTION = 'Fetches the latest articles from the National Geographic Magazine';
const MAINTAINER = 'logmanoriginal';
const PARAMETERS = array(
self::CONTEXT_BY_TOPIC => array(
self::PARAMETER_TOPIC => array(
'name' => 'Topic',
'type' => 'list',
'values' => array(
self::TOPIC_MAGAZINE => 'magazine',
self::TOPIC_LATEST_STORIES => 'latest-stories'
),
'title' => 'Select your topic',
'defaultValue' => 'Magazine'
)
),
'global' => array(
self::PARAMETER_FULL_ARTICLE => array(
'name' => 'Full Article',
'type' => 'checkbox',
'title' => 'Enable to load full articles (takes longer)'
)
)
);
private $topicName = '';
public function getURI() {
switch ($this->queriedContext) {
case self::CONTEXT_BY_TOPIC: {
return self::URI . $this->getInput(self::PARAMETER_TOPIC);
} break;
default: {
return parent::getURI();
}
}
}
public function collectData() {
$this->topicName = $this->getTopicName($this->getInput(self::PARAMETER_TOPIC));
switch($this->topicName) {
case self::TOPIC_MAGAZINE: {
return $this->collectMagazine();
} break;
case self::TOPIC_LATEST_STORIES: {
return $this->collectLatestStories();
} break;
default: {
returnServerError('Unknown topic: "' . $this->topicName . '"');
}
}
}
public function getName() {
switch ($this->queriedContext) {
case self::CONTEXT_BY_TOPIC: {
return static::NAME . ': ' . $this->topicName;
} break;
default: {
return parent::getName();
}
}
}
private function getTopicName($topic) {
return array_search($topic, static::PARAMETERS[self::CONTEXT_BY_TOPIC][self::PARAMETER_TOPIC]['values']);
}
private function collectMagazine() {
$uri = $this->getURI();
$html = getSimpleHTMLDOM($uri)
or returnServerError('Could not request ' . $uri);
$script = $html->find('#lead-component script')[0];
$json = json_decode($script->innertext, true);
// This is probably going to break in the future, fix it then :)
foreach($json['body']['0']['multilayout_promo_beta']['stories'] as $story) {
$this->addStory($story);
}
}
private function collectLatestStories() {
$uri = self::URI . 'latest-stories/_jcr_content/content/hubfeed.promo-hub-feed-all-stories.json';
$json_raw = getContents($uri)
or returnServerError('Could not request ' . $uri);
foreach(json_decode($json_raw, true) as $story) {
$this->addStory($story);
}
}
private function addStory($story) {
$title = 'Unknown title';
$content = '';
foreach($story['components'] as $component) {
switch($component['content_type']) {
case 'title': {
$title = $component['title']['text'];
} break;
case 'dek': {
$content = $component['dek']['text'];
} break;
}
}
$item = array();
$item['uri'] = $story['uri'];
$item['title'] = $title;
// if full article is requested!
if ($this->getInput(self::PARAMETER_FULL_ARTICLE))
$item['content'] = $this->getFullArticle($item['uri']);
else
$item['content'] = $content;
if (isset($story['promo_image'])) {
switch($story['promo_image']['content_type']) {
case 'image': {
$item['enclosures'][] = $story['promo_image']['image']['uri'];
} break;
}
}
if (isset($story['lead_media'])) {
$media = $story['lead_media'];
switch($media['content_type']) {
case 'image': {
// Don't add if promo_image was added
if (empty($item['enclosures']))
$item['enclosures'][] = $media['image']['uri'];
} break;
case 'image_gallery': {
foreach($media['image_gallery']['images'] as $image) {
$item['enclosures'][] = $image['uri'];
}
} break;
}
}
$this->items[] = $item;
}
private function getFullArticle($uri) {
$html = getSimpleHTMLDOMCached($uri)
or returnServerError('Could not load ' . $uri);
$html = defaultLinkTo($html, $uri);
$content = '';
foreach($html->find('
.content > .smartbody.text,
.content > .section.image script[type="text/json"],
.content > .section.image span[itemprop="caption"],
.content > .section.inline script[type="text/json"]
') as $element) {
if ($element->tag === 'script') {
$json = json_decode($element->innertext, true);
if (isset($json['src'])) {
$content .= '<img src="' . $json['src'] . '" width="100%" alt="' . $json['alt'] . '">';
} elseif (isset($json['galleryType']) && isset($json['endpoint'])) {
$doc = getContents($json['endpoint'])
or returnServerError('Could not load ' . $json['endpoint']);
$json = json_decode($doc, true);
foreach($json['items'] as $item) {
$content .= '<p>' . $item['caption'] . '</p>';
$content .= '<img src="' . $item['url'] . '" width="100%" alt="' . $item['caption'] . '">';
}
}
} else {
$content .= $element->outertext;
}
}
return $content;
}
}

View File

@@ -6,6 +6,16 @@ class PikabuBridge extends BridgeAbstract {
const DESCRIPTION = 'Выводит посты по тегу';
const MAINTAINER = 'em92';
const PARAMETERS_FILTER = array(
'name' => 'Фильтр',
'type' => 'list',
'values' => array(
'Горячее' => 'hot',
'Свежее' => 'new',
),
'defaultValue' => 'hot'
);
const PARAMETERS = array(
'По тегу' => array(
'tag' => array(
@@ -13,21 +23,29 @@ class PikabuBridge extends BridgeAbstract {
'exampleValue' => 'it',
'required' => true
),
'filter' => array(
'name' => 'Фильтр',
'type' => 'list',
'values' => array(
'Горячее' => 'hot',
'Свежее' => 'new',
),
'defaultValue' => 'hot'
)
'filter' => self::PARAMETERS_FILTER
),
'По сообществу' => array(
'community' => array(
'name' => 'Сообщество',
'exampleValue' => 'linux',
'required' => true
),
'filter' => self::PARAMETERS_FILTER
)
);
protected $title = null;
public function getURI() {
if ($this->getInput('tag')) {
return self::URI . '/tag/' . rawurlencode($this->getInput('tag')) . '/' . rawurlencode($this->getInput('filter'));
} else if ($this->getInput('community')) {
$uri = self::URI . '/community/' . rawurlencode($this->getInput('community'));
if ($this->getInput('filter') != 'hot') {
$uri .= '/' . rawurlencode($this->getInput('filter'));
}
return $uri;
} else {
return parent::getURI();
}
@@ -38,10 +56,10 @@ class PikabuBridge extends BridgeAbstract {
}
public function getName() {
if (is_string($this->getInput('tag'))) {
return $this->getInput('tag') . ' - ' . parent::getName();
} else {
if (is_null($this->title)) {
return parent::getName();
} else {
return $this->title . ' - ' . parent::getName();
}
}
@@ -52,6 +70,8 @@ class PikabuBridge extends BridgeAbstract {
$text_html = iconv('windows-1251', 'utf-8', $text_html);
$html = str_get_html($text_html);
$this->title = $html->find('title', 0)->innertext;
foreach($html->find('article.story') as $post) {
$time = $post->find('time.story__datetime', 0);
if (is_null($time)) continue;
@@ -67,6 +87,11 @@ class PikabuBridge extends BridgeAbstract {
}
}
foreach($post->find('[data-type=gifx]') as $el) {
$src = $el->getAttribute('data-source');
$el->outertext = '<img src="' . $src . '">';
}
foreach($post->find('img') as $img) {
$src = $img->getAttribute('src');
if (!$src) {

View File

@@ -16,12 +16,6 @@ class PinterestBridge extends FeedExpander {
'name' => 'board',
'required' => true
)
),
'From search' => array(
'q' => array(
'name' => 'Keyword',
'required' => true
)
)
);
@@ -29,17 +23,9 @@ class PinterestBridge extends FeedExpander {
return 'https://s.pinimg.com/webapp/style/images/favicon-9f8f9adf.png';
}
public function collectData(){
switch($this->queriedContext) {
case 'By username and board':
$this->collectExpandableDatas($this->getURI() . '.rss');
$this->fixLowRes();
break;
case 'From search':
default:
$html = getSimpleHTMLDOMCached($this->getURI());
$this->getSearchResults($html);
}
public function collectData() {
$this->collectExpandableDatas($this->getURI() . '.rss');
$this->fixLowRes();
}
private function fixLowRes() {
@@ -55,71 +41,21 @@ class PinterestBridge extends FeedExpander {
}
private function getSearchResults($html){
$json = json_decode($html->find('#jsInit1', 0)->innertext, true);
$results = $json['resourceDataCache'][0]['data']['results'];
public function getURI() {
foreach($results as $result) {
$item = array();
$item['uri'] = self::URI . $result['board']['url'];
// Some use regular titles, others provide 'advanced' infos, a few
// provide even less info. Thus we attempt multiple options.
$item['title'] = trim($result['title']);
if($item['title'] === '')
$item['title'] = trim($result['rich_summary']['display_name']);
if($item['title'] === '')
$item['title'] = trim($result['grid_description']);
$item['timestamp'] = strtotime($result['created_at']);
$item['username'] = $result['pinner']['username'];
$item['fullname'] = $result['pinner']['full_name'];
$item['avatar'] = $result['pinner']['image_small_url'];
$item['author'] = $item['username'] . ' (' . $item['fullname'] . ')';
$item['content'] = '<img align="left" style="margin: 2px 4px;" src="'
. htmlentities($item['avatar'])
. '" /><p><strong>'
. $item['username']
. '</strong><br>'
. $item['fullname']
. '</p><br><img src="'
. $result['images']['736x']['url']
. '" alt="" /><br><p>'
. $result['description']
. '</p>';
$item['enclosures'] = array($result['images']['orig']['url']);
$this->items[] = $item;
if ($this->queriedContext === 'By username and board') {
return self::URI . '/' . urlencode($this->getInput('u')) . '/' . urlencode($this->getInput('b'));
}
return parent::getURI();
}
public function getURI(){
switch($this->queriedContext) {
case 'By username and board':
$uri = self::URI . '/' . urlencode($this->getInput('u')) . '/' . urlencode($this->getInput('b'));// . '.rss';
break;
case 'From search':
$uri = self::URI . '/search/?q=' . urlencode($this->getInput('q'));
break;
default: return parent::getURI();
}
return $uri;
}
public function getName() {
public function getName(){
switch($this->queriedContext) {
case 'By username and board':
$specific = $this->getInput('u') . ' - ' . $this->getInput('b');
break;
case 'From search':
$specific = $this->getInput('q');
break;
default: return parent::getName();
if ($this->queriedContext === 'By username and board') {
return $this->getInput('u') . ' - ' . $this->getInput('b') . ' - ' . self::NAME;
}
return $specific . ' - ' . self::NAME;
return parent::getName();
}
}

132
bridges/QPlayBridge.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
class QPlayBridge extends BridgeAbstract {
const NAME = 'Q Play';
const URI = 'https://www.qplay.pt';
const DESCRIPTION = 'Entretenimento e humor em Português';
const MAINTAINER = 'somini';
const PARAMETERS = array(
'Program' => array(
'program' => array(
'name' => 'Program Name',
'type' => 'text',
'required' => true,
),
),
'Catalog' => array(
'all_pages' => array(
'name' => 'All Pages',
'type' => 'checkbox',
'defaultValue' => false,
),
),
);
public function getIcon() {
# This should be the favicon served on `self::URI`
return 'https://s3.amazonaws.com/unode1/assets/4957/r3T9Lm9LTLmpAEX6FlSA_apple-touch-icon.png';
}
public function getURI() {
switch ($this->queriedContext) {
case 'Program':
return self::URI . '/programs/' . $this->getInput('program');
case 'Catalog':
return self::URI . '/catalog';
}
return parent::getURI();
}
public function getName() {
switch ($this->queriedContext) {
case 'Program':
$html = getSimpleHTMLDOMCached($this->getURI())
or returnServerError('Could not load content');
return $html->find('h1.program--title', 0)->innertext;
case 'Catalog':
return self::NAME . ' | Programas';
}
return parent::getName();
}
/* This uses the uscreen platform, other sites can adapt this. https://www.uscreen.tv/ */
public function collectData() {
switch ($this->queriedContext) {
case 'Program':
$program = $this->getInput('program');
$html = getSimpleHTMLDOMCached($this->getURI())
or returnServerError('Could not load content');
foreach($html->find('.cce--thumbnails-video-chapter') as $element) {
$cid = $element->getAttribute('data-id');
$item['title'] = $element->find('.cce--chapter-title', 0)->innertext;
$item['content'] = $element->find('.cce--thumbnails-image-block', 0)
. $element->find('.cce--chapter-body', 0)->innertext;
$item['uri'] = $this->getURI() . '?cid=' . $cid;
/* TODO: Suport login credentials? */
/* # Get direct video URL */
/* $json_source = getContents(self::URI . '/chapters/' . $cid, array('Cookie: _uscreen2_session=???;')) */
/* or returnServerError('Could not request chapter JSON'); */
/* $json = json_decode($json_source); */
/* $item['enclosures'] = [$json->fallback]; */
$this->items[] = $item;
}
break;
case 'Catalog':
$json_raw = getContents($this->getCatalogURI(1))
or returnServerError('Could not load catalog content');
$json = json_decode($json_raw);
$total_pages = $json->total_pages;
foreach($this->parseCatalogPage($json) as $item) {
$this->items[] = $item;
}
if ($this->getInput('all_pages') === true) {
foreach(range(2, $total_pages) as $page) {
$json_raw = getContents($this->getCatalogURI($page))
or returnServerError('Could not load catalog content (all pages)');
$json = json_decode($json_raw);
foreach($this->parseCatalogPage($json) as $item) {
$this->items[] = $item;
}
}
}
break;
}
}
private function getCatalogURI($page) {
return self::URI . '/catalog.json?page=' . $page;
}
private function parseCatalogPage($json) {
$items = array();
foreach($json->records as $record) {
$item = array();
$item['title'] = $record->title;
$item['content'] = $record->description
. '<div>Duration: ' . $record->duration . '</div>';
$item['timestamp'] = strtotime($record->release_date);
$item['uri'] = self::URI . $record->url;
$item['enclosures'] = array(
$record->main_poster,
);
$items[] = $item;
}
return $items;
}
}

View File

@@ -1,34 +1,87 @@
<?php
class RadioMelodieBridge extends BridgeAbstract {
const NAME = 'Radio Melodie Actu';
const URI = 'https://www.radiomelodie.com/';
const URI = 'https://www.radiomelodie.com';
const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie';
const MAINTAINER = 'sysadminstory';
public function getIcon() {
return self::URI . 'img/favicon.png';
return self::URI . '/img/favicon.png';
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI . 'actu')
$html = getSimpleHTMLDOM(self::URI . '/actu/')
or returnServerError('Could not request Radio Melodie.');
$list = $html->find('div[class=actuitem]');
$list = $html->find('div[class=displayList]', 0)->children();
foreach($list as $element) {
$item = array();
if($element->tag == 'a') {
$articleURL = self::URI . $element->href;
$article = getSimpleHTMLDOM($articleURL);
$textDOM = $article->find('article', 0);
// Get picture URL
$pictureHTML = $element->find('div[class=picture]');
preg_match(
'/background-image:url\((.*)\);/',
$pictureHTML[0]->getAttribute('style'),
$pictures);
$pictureURL = $pictures[1];
// Initialise arrays
$item = array();
$audio = array();
$picture = array();
$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;
// Get the Main picture URL
$picture[] = $this->rewriteImage($article->find('div[id=pictureTitleSupport]', 0)->find('img', 0)->src);
$audioHTML = $article->find('audio');
// Add the audio element to the enclosure
foreach($audioHTML as $audioElement) {
$audioURL = $audioElement->src;
$audio[] = $audioURL;
}
// Rewrite pictures URL
$imgs = $textDOM->find('img[src^="http://www.radiomelodie.com/image.php]');
foreach($imgs as $img) {
$img->src = $this->rewriteImage($img->src);
$article->save();
}
// Remove Google Ads
$ads = $article->find('div[class=adInline]');
foreach($ads as $ad) {
$ad->outertext = '';
$article->save();
}
// Remove Radio Melodie Logo
$logoHTML = $article->find('div[id=logoArticleRM]', 0);
$logoHTML->outertext = '';
$article->save();
$author = $article->find('p[class=AuthorName]', 0)->plaintext;
$item['enclosures'] = array_merge($picture, $audio);
$item['author'] = $author;
$item['uri'] = $articleURL;
$item['title'] = $article->find('meta[property=og:title]', 0)->content;
$date = $article->find('p[class*=date]', 0)->plaintext;
// Header Image
$header = '<img src="' . $picture[0] . '"/>';
// Remove the Date and Author part
$textDOM->find('div[class=AuthorDate]', 0)->outertext = '';
$article->save();
$text = $textDOM->innertext;
$item['content'] = '<h1>' . $item['title'] . '</h1>' . $date . '<br/>' . $header . $text;
$this->items[] = $item;
}
}
}
/*
* Function to rewrite image URL to use the real Image URL and not the resized one (which is very slow)
*/
private function rewriteImage($url)
{
$parts = explode('?', $url);
parse_str(html_entity_decode($parts[1]), $params);
return self::URI . '/' . $params['image'];
}
}

View File

@@ -6,98 +6,60 @@ class RoadAndTrackBridge extends BridgeAbstract {
const CACHE_TIMEOUT = 86400; // 24h
const DESCRIPTION = 'Returns the latest news from Road & Track.';
const PARAMETERS = array(
array(
'new-cars' => array(
'name' => 'New Cars',
'type' => 'checkbox',
'exampleValue' => 'checked',
'title' => 'Activate to load New Cars articles'
),
'motorsports' => array(
'name' => 'Motorsports',
'type' => 'checkbox',
'exampleValue' => 'checked',
'title' => 'Activate to load Motorsports articles'
),
'car-culture' => array(
'name' => 'Car Culture',
'type' => 'checkbox',
'exampleValue' => 'checked',
'title' => 'Activate to load Car Culture articles'
),
'car-shows' => array(
'name' => 'Car shows',
'type' => 'checkbox',
'exampleValue' => 'checked',
'title' => 'Activate to load Car shows articles'
)
)
);
const SIG_URL = 'https://cloud.mazdigital.com/feeds/production/comboapp/204/api/v3/';
public function collectData() {
//Magic
$signVal = '?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9jbG91ZC5tYXpkaWd';
$signVal .= 'pdGFsLmNvbS9mZWVkcy9wcm9kdWN0aW9uL2NvbWJvYXBwLzIwNC8qIiwiQ29uZGl0aW9uIj';
$signVal .= 'p7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNTUyNTU5MDUzfSwiSXBBZGRyZ';
$signVal .= 'XNzIjp7IkFXUzpTb3VyY2VJcCI6IjAuMC4wLjAvMCJ9fX1dfQ__&Signature=jgS~Jccjs';
$signVal .= 'lXMMywWesmwDpUbHvEmrADRP7iBRzT~OiP-O~zI-8TtQzqTP7GUrpB9~v69CvhO7-JVtw94';
$signVal .= 'VC3N6lQrwsxTTIhpS57YGeV~MbZx~P653yUV7jb3jpJE2yUawfXnEkD-XzOIn8-caMo~14i';
$signVal .= 'KuWV9KNDkTJaRgOMy0rrVpWqiuBjCu5s5B8Ylt2qwcpOvHjXSqG9IY5c7GUIXKsk8yXzGFi';
$signVal .= 'yzy8hfuGgdx0n7fgl7c4-EoDgQaz~U76g0epejPxV5Csj16rCCfAqBU5kZJnACZ1vvOvRcV';
$signVal .= 'Wiu8KUuUuCS04SPmJ73Y5XoY8~uXRScxZG1kAFTIAhT4nYVlg__&Key-Pair-Id=APKAIZB';
$signVal .= 'QNNSW4WGIFP4Q';
$page = getSimpleHTMLDOM(self::URI);
$newsElements = array();
if($this->getInput('new-cars')) {
$newsElements = array_merge($newsElements,
json_decode(getContents(self::SIG_URL . '7591/item_feed' . $signVal))
);
}
if($this->getInput('motorsports')) {
$newsElements = array_merge($newsElements,
json_decode(getContents(self::SIG_URL . '7590/item_feed' . $signVal))
);
}
if($this->getInput('car-culture')) {
$newsElements = array_merge($newsElements,
json_decode(getContents(self::SIG_URL . '7588/item_feed' . $signVal))
);
}
if($this->getInput('car-shows')) {
$newsElements = array_merge($newsElements,
json_decode(getContents(self::SIG_URL . '7589/item_feed' . $signVal))
);
}
usort($newsElements, function($a, $b) {
return $b->published - $a->published;
});
//Process the first element
$firstArticleLink = $page->find('.custom-promo-title', 0)->href;
$this->items[] = $this->fetchArticle($firstArticleLink);
$limit = 19;
foreach($newsElements as $element) {
$item = array();
$item['uri'] = $element->sourceUrl;
$item['timestamp'] = $element->published;
$item['enclosures'] = array($element->cover->url);
$item['title'] = $element->title;
$item['content'] = $this->getArticleContent($element);
$this->items[] = $item;
if($limit > 0) {
$limit--;
} else {
break;
}
foreach($page->find('.full-item-title') as $article) {
$this->items[] = $this->fetchArticle($article->href);
$limit -= 1;
if($limit == 0) break;
}
}
private function fixImages($content) {
$enclosures = [];
foreach($content->find('img') as $image) {
$image->src = explode('?', $image->getAttribute('data-src'))[0];
$enclosures[] = $image->src;
}
foreach($content->find('.embed-image-wrap, .content-lede-image-wrap') as $imgContainer) {
$imgContainer->style = '';
}
return $enclosures;
}
private function fetchArticle($articleLink) {
$articleLink = self::URI . $articleLink;
$article = getSimpleHTMLDOM($articleLink);
$item = array();
$item['title'] = $article->find('.content-hed', 0)->innertext;
$item['author'] = $article->find('.byline-name', 0)->innertext;
$item['timestamp'] = strtotime($article->find('.content-info-date', 0)->getAttribute('datetime'));
$content = $article->find('.content-container', 0);
if($content->find('.content-rail', 0) !== null)
$content->find('.content-rail', 0)->innertext = '';
$enclosures = $this->fixImages($content);
$item['enclosures'] = $enclosures;
$item['content'] = $content;
return $item;
}
private function getArticleContent($article) {
return getContents($article->contentUrl);

View File

@@ -9,7 +9,7 @@ class Rue89Bridge extends BridgeAbstract {
public function collectData() {
$jsonArticles = getContents('https://appdata.nouvelobs.com/rue89/feed.json')
or die('Unable to query Rue89 !');
or returnServerError('Unable to query Rue89 !');
$articles = json_decode($jsonArticles)->items;
foreach($articles as $article) {
$this->items[] = $this->getArticle($article);
@@ -19,7 +19,8 @@ class Rue89Bridge extends BridgeAbstract {
private function getArticle($articleInfo) {
$articleJson = getContents($articleInfo->json_url) or die('Unable to get article !');
$articleJson = getContents($articleInfo->json_url)
or returnServerError('Unable to get article !');
$article = json_decode($articleJson);
$item = array();
$item['title'] = $article->title;

63
bridges/SIMARBridge.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
class SIMARBridge extends BridgeAbstract {
const NAME = 'SIMAR';
const URI = 'http://www.simar-louresodivelas.pt/';
const DESCRIPTION = 'Verificar estado da rede SIMAR';
const MAINTAINER = 'somini';
const PARAMETERS = array(
'Público' => array(
'interventions' => array(
'type' => 'checkbox',
'name' => 'Incluir Intervenções?',
'defaultValue' => 'checked',
)
)
);
public function collectData() {
$html = getSimpleHTMLDOM(self::getURI())
or returnServerError('Could not load content');
$e_home = $html->find('#home', 0)
or returnServerError('Invalid site structure');
foreach($e_home->find('span') as $element) {
$item = array();
$item['title'] = 'Rotura: ' . $element->plaintext;
$item['content'] = $element->innertext;
$item['uid'] = 'urn:sha1:' . hash('sha1', $item['content']);
$this->items[] = $item;
}
if ($this->getInput('interventions')) {
$e_main1 = $html->find('#menu1', 0)
or returnServerError('Invalid site structure');
foreach ($e_main1->find('a') as $element) {
$item = array();
$item['title'] = 'Intervenção: ' . $element->plaintext;
$item['uri'] = self::getURI() . $element->href;
$item['content'] = $element->innertext;
/* Try to get the actual contents for this kind of item */
$item_html = getSimpleHTMLDOMCached($item['uri']);
if ($item_html) {
$e_item = $item_html->find('.auto-style59', 0);
foreach($e_item->find('p') as $paragraph) {
/* Remove empty paragraphs */
if (preg_match('/^(\W|&nbsp;)+$/', $paragraph->innertext) == 1) {
$paragraph->outertext = '';
}
}
if ($e_item) {
$item['content'] = $e_item->innertext;
}
}
$this->items[] = $item;
}
}
}
}

View File

@@ -1,11 +0,0 @@
<?php
require_once('MoebooruBridge.php');
class SakugabooruBridge extends MoebooruBridge {
const MAINTAINER = 'mitsukarenai';
const NAME = 'Sakugabooru';
const URI = 'http://sakuga.yshi.org/';
const DESCRIPTION = 'Returns images from given page';
}

View File

@@ -2,70 +2,152 @@
class ShanaprojectBridge extends BridgeAbstract {
const MAINTAINER = 'logmanoriginal';
const NAME = 'Shanaproject Bridge';
const URI = 'http://www.shanaproject.com';
const URI = 'https://www.shanaproject.com';
const DESCRIPTION = 'Returns a list of anime from the current Season Anime List';
const PARAMETERS = array(
array(
'min_episodes' => array(
'name' => 'Minimum Episodes',
'type' => 'number',
'title' => 'Minimum number of episodes before including in feed',
'defaultValue' => 0,
),
'min_total_episodes' => array(
'name' => 'Minimum Total Episodes',
'type' => 'number',
'title' => 'Minimum total number of episodes before including in feed',
'defaultValue' => 0,
),
'require_banner' => array(
'name' => 'Require Banner',
'type' => 'checkbox',
'title' => 'Only include anime with custom banner image',
'defaultValue' => false,
),
),
);
private $uri;
public function getURI() {
return isset($this->uri) ? $this->uri : parent::getURI();
}
public function collectData(){
$html = $this->loadSeasonAnimeList();
$animes = $html->find('div.header_display_box_info')
or returnServerError('Could not find anime headers!');
$min_episodes = $this->getInput('min_episodes') ?: 0;
$min_total_episodes = $this->getInput('min_total_episodes') ?: 0;
foreach($animes as $anime) {
list(
$episodes_released,
/* of */,
$episodes_total
) = explode(' ', $this->extractAnimeEpisodeInformation($anime));
// Skip if not enough episodes yet
if ($episodes_released < $min_episodes) {
continue;
}
// Skip if too many episodes in total
if ($episodes_total !== '?' && $episodes_total < $min_total_episodes) {
continue;
}
// Skip if https://static.shanaproject.com/no-art.jpg
if ($this->getInput('require_banner')
&& strpos($this->extractAnimeBackgroundImage($anime), 'no-art') !== false) {
continue;
}
$this->items[] = array(
'title' => $this->extractAnimeTitle($anime),
'author' => $this->extractAnimeAuthor($anime),
'uri' => $this->extractAnimeUri($anime),
'timestamp' => $this->extractAnimeTimestamp($anime),
'content' => $this->buildAnimeContent($anime),
);
}
}
// Returns an html object for the Season Anime List (latest season)
private function loadSeasonAnimeList(){
// First we need to find the URI to the latest season from the
// 'seasons' page searching for 'Season Anime List'
$html = getSimpleHTMLDOM($this->getURI() . '/seasons');
if(!$html)
returnServerError('Could not load \'seasons\' page!');
$season = $html->find('div.follows_menu/a', 1);
if(!$season)
returnServerError('Could not find \'Season Anime List\'!');
$html = getSimpleHTMLDOM(self::URI . '/seasons')
or returnServerError('Could not load \'seasons\' page!');
$html = getSimpleHTMLDOM($this->getURI() . $season->href);
if(!$html)
returnServerError(
$html = defaultLinkTo($html, self::URI . '/seasons');
$season = $html->find('div.follows_menu > a', 1)
or returnServerError('Could not find \'Season Anime List\'!');
$html = getSimpleHTMLDOM($season->href)
or returnServerError(
'Could not load \'Season Anime List\' from \''
. $season->innertext
. '\'!'
);
$this->uri = $season->href;
$html = defaultLinkTo($html, $season->href);
return $html;
}
// Extracts the anime title
private function extractAnimeTitle($anime){
$title = $anime->find('a', 0);
if(!$title)
returnServerError('Could not find anime title!');
$title = $anime->find('a', 0)
or returnServerError('Could not find anime title!');
return trim($title->innertext);
}
// Extracts the anime URI
private function extractAnimeUri($anime){
$uri = $anime->find('a', 0);
if(!$uri)
returnServerError('Could not find anime URI!');
return $this->getURI() . $uri->href;
$uri = $anime->find('a', 0)
or returnServerError('Could not find anime URI!');
return $uri->href;
}
// Extracts the anime release date (timestamp)
private function extractAnimeTimestamp($anime){
$timestamp = $anime->find('span.header_info_block', 1);
if(!$timestamp)
if(!$timestamp) {
return null;
}
return strtotime($timestamp->innertext);
}
// Extracts the anime studio name (author)
private function extractAnimeAuthor($anime){
$author = $anime->find('span.header_info_block', 2);
if(!$author)
return; // Sometimes the studio is unknown, so leave empty
if(!$author) {
return null; // Sometimes the studio is unknown, so leave empty
}
return trim($author->innertext);
}
// Extracts the episode information (x of y released)
private function extractAnimeEpisodeInformation($anime){
$episode = $anime->find('div.header_info_episode', 0);
if(!$episode)
returnServerError('Could not find anime episode information!');
return preg_replace('/\r|\n/', ' ', $episode->plaintext);
$episode = $anime->find('div.header_info_episode', 0)
or returnServerError('Could not find anime episode information!');
$retVal = preg_replace('/\r|\n/', ' ', $episode->plaintext);
$retVal = preg_replace('/\s+/', ' ', $retVal);
return $retVal;
}
// Extracts the background image
@@ -73,15 +155,16 @@ class ShanaprojectBridge extends BridgeAbstract {
// Getting the picture is a little bit tricky as it is part of the style.
// Luckily the style is part of the parent div :)
if(preg_match('/url\(\/\/([^\)]+)\)/i', $anime->parent->style, $matches))
if(preg_match('/url\(\/\/([^\)]+)\)/i', $anime->parent->style, $matches)) {
return $matches[1];
}
returnServerError('Could not extract background image!');
}
// Builds an URI to search for a specific anime (subber is left empty)
private function buildAnimeSearchUri($anime){
return $this->getURI()
return self::URI
. '/search/?title='
. urlencode($this->extractAnimeTitle($anime))
. '&subber=';
@@ -102,22 +185,4 @@ class ShanaprojectBridge extends BridgeAbstract {
. $this->buildAnimeSearchUri($anime)
. '">Search episodes</a></p>';
}
public function collectData(){
$html = $this->loadSeasonAnimeList();
$animes = $html->find('div.header_display_box_info');
if(!$animes)
returnServerError('Could not find anime headers!');
foreach($animes as $anime) {
$item = array();
$item['title'] = $this->extractAnimeTitle($anime);
$item['author'] = $this->extractAnimeAuthor($anime);
$item['uri'] = $this->extractAnimeUri($anime);
$item['timestamp'] = $this->extractAnimeTimestamp($anime);
$item['content'] = $this->buildAnimeContent($anime);
$this->items[] = $item;
}
}
}

View File

@@ -16,6 +16,8 @@ class SoundCloudBridge extends BridgeAbstract {
const CLIENT_ID = 'W0KEWWILAjDiRH89X0jpwzuq6rbSK08R';
private $feedIcon = null;
public function collectData(){
$res = json_decode(getContents(
@@ -25,6 +27,8 @@ class SoundCloudBridge extends BridgeAbstract {
. self::CLIENT_ID
)) or returnServerError('No results for this query');
$this->feedIcon = $res->avatar_url;
$tracks = json_decode(getContents(
'https://api.soundcloud.com/users/'
. urlencode($res->id)
@@ -56,6 +60,14 @@ class SoundCloudBridge extends BridgeAbstract {
}
public function getIcon(){
if ($this->feedIcon) {
return $this->feedIcon;
}
return parent::getIcon();
}
public function getName(){
if(!is_null($this->getInput('u'))) {
return self::NAME . ' - ' . $this->getInput('u');

View File

@@ -0,0 +1,64 @@
<?php
class SplCenterBridge extends FeedExpander {
const NAME = 'Southern Poverty Law Center Bridge';
const URI = 'https://www.splcenter.org';
const DESCRIPTION = 'Returns the newest posts from the Southern Poverty Law Center';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(array(
'content' => array(
'name' => 'Content',
'type' => 'list',
'values' => array(
'News' => 'news',
'Hatewatch' => 'hatewatch',
),
'defaultValue' => 'news',
)
)
);
const CACHE_TIMEOUT = 3600; // 1 hour
protected function parseItem($item) {
$item = parent::parseItem($item);
$articleHtml = getSimpleHTMLDOMCached($item['uri'])
or returnServerError('Could not request: ' . $item['uri']);
foreach ($articleHtml->find('.file') as $index => $media) {
$articleHtml->find('div.file', $index)->outertext = '<em>' . $media->outertext . '</em>';
}
$item['content'] = $articleHtml->find('div#group-content-container', 0)->innertext;
$item['enclosures'][] = $articleHtml->find('meta[name="twitter:image"]', 0)->content;
return $item;
}
public function collectData() {
$this->collectExpandableDatas($this->getURI() . '/rss.xml');
}
public function getURI() {
if (!is_null($this->getInput('content'))) {
return self::URI . '/' . $this->getInput('content');
}
return parent::getURI();
}
public function getName() {
if (!is_null($this->getInput('content'))) {
$parameters = $this->getParameters();
$contentValues = array_flip($parameters[0]['content']['values']);
return $contentValues[$this->getInput('content')] . ' - Southern Poverty Law Center';
}
return parent::getName();
}
}

View File

@@ -8,44 +8,12 @@ class SteamBridge extends BridgeAbstract {
const MAINTAINER = 'jacknumber';
const PARAMETERS = array(
'Wishlist' => array(
'username' => array(
'name' => 'Username',
'userid' => array(
'name' => 'Steamid64 (find it on steamid.io)',
'title' => 'User ID (17 digits). Find your user ID with steamid.io or steamidfinder.com',
'required' => true,
),
'currency' => array(
'name' => 'Currency',
'type' => 'list',
'values' => array(
// source: http://steam.steamlytics.xyz/currencies
'USD' => 'us',
'GBP' => 'gb',
'EUR' => 'fr',
'CHF' => 'ch',
'RUB' => 'ru',
'BRL' => 'br',
'JPY' => 'jp',
'SEK' => 'se',
'IDR' => 'id',
'MYR' => 'my',
'PHP' => 'ph',
'SGD' => 'sg',
'THB' => 'th',
'KRW' => 'kr',
'TRY' => 'tr',
'MXN' => 'mx',
'CAD' => 'ca',
'NZD' => 'nz',
'CNY' => 'cn',
'INR' => 'in',
'CLP' => 'cl',
'PEN' => 'pe',
'COP' => 'co',
'ZAR' => 'za',
'HKD' => 'hk',
'TWD' => 'tw',
'SRD' => 'sr',
'AED' => 'ae',
),
'exampleValue' => '76561198821231205',
'pattern' => '[0-9]{17}',
),
'only_discount' => array(
'name' => 'Only discount',
@@ -56,27 +24,15 @@ class SteamBridge extends BridgeAbstract {
public function collectData(){
$username = $this->getInput('username');
$params = array(
'cc' => $this->getInput('currency')
);
$userid = $this->getInput('userid');
$url = self::URI . 'wishlist/id/' . $username . '?' . http_build_query($params);
$targetVariable = 'g_rgAppInfo';
$sourceUrl = self::URI . 'wishlist/profiles/' . $userid . '/wishlistdata?p=0';
$sort = array();
$html = '';
$html = getSimpleHTMLDOM($url)
or returnServerError("Could not request Steam Wishlist. Tried:\n - $url");
$json = getContents($sourceUrl)
or returnServerError('Could not get content from wishlistdata (' . $sourceUrl . ')');
$jsContent = $html->find('.responsive_page_template_content script', 0)->innertext;
if(preg_match('/var ' . $targetVariable . ' = (.*?);/s', $jsContent, $matches)) {
$appsData = json_decode($matches[1]);
} else {
returnServerError("Could not parse JS variable ($targetVariable) in page content.");
}
$appsData = json_decode($json);
foreach($appsData as $id => $element) {
@@ -87,6 +43,8 @@ class SteamBridge extends BridgeAbstract {
if($element->subs) {
$appIsBuyable = 1;
$priceBlock = str_get_html($element->subs[0]->discount_block);
$appPrice = str_replace('--', '00', $priceBlock->find('.discount_final_price', 0)->plaintext);
if($element->subs[0]->discount_pct) {
@@ -94,8 +52,6 @@ class SteamBridge extends BridgeAbstract {
$discountBlock = str_get_html($element->subs[0]->discount_block);
$appDiscountValue = $discountBlock->find('.discount_pct', 0)->plaintext;
$appOldPrice = $discountBlock->find('.discount_original_price', 0)->plaintext;
$appNewPrice = $discountBlock->find('.discount_final_price', 0)->plaintext;
$appPrice = $appNewPrice;
} else {
@@ -103,7 +59,6 @@ class SteamBridge extends BridgeAbstract {
continue;
}
$appPrice = $element->subs[0]->price / 100;
}
} else {
@@ -117,11 +72,14 @@ class SteamBridge extends BridgeAbstract {
}
}
$coverUrl = str_replace('_292x136', '', strtok($element->capsule, '?'));
$picturesPath = pathinfo($coverUrl)['dirname'] . '/';
$item = array();
$item['uri'] = "http://store.steampowered.com/app/$id/";
$item['title'] = $element->name;
$item['type'] = $appType;
$item['cover'] = str_replace('_292x136', '', $element->capsule);
$item['cover'] = $coverUrl;
$item['timestamp'] = $element->added;
$item['isBuyable'] = $appIsBuyable;
$item['hasDiscount'] = $appHasDiscount;
@@ -129,22 +87,29 @@ class SteamBridge extends BridgeAbstract {
$item['priority'] = $element->priority;
if($appIsBuyable) {
$item['price'] = floatval(str_replace(',', '.', $appPrice));
$item['content'] = $appPrice;
}
if($appIsFree) {
$item['content'] = 'Free';
}
if($appHasDiscount) {
$item['discount']['value'] = $appDiscountValue;
$item['discount']['oldPrice'] = floatval(str_replace(',', '.', $appOldPrice));
$item['discount']['newPrice'] = floatval(str_replace(',', '.', $appNewPrice));
$item['discount']['oldPrice'] = $appOldPrice;
$item['content'] = '<s>' . $appOldPrice . '</s> <b>' . $appPrice . '</b> (' . $appDiscountValue . ')';
}
$item['enclosures'] = array();
$item['enclosures'][] = str_replace('_292x136', '', $element->capsule);
$item['enclosures'][] = $coverUrl;
foreach($element->screenshots as $screenshot) {
$item['enclosures'][] = substr($element->capsule, 0, -31) . $screenshot;
foreach($element->screenshots as $screenshotFileName) {
$item['enclosures'][] = $picturesPath . $screenshotFileName;
}
$sort[$id] = $element->priority;

View File

@@ -0,0 +1,191 @@
<?php
class SteamCommunityBridge extends BridgeAbstract {
const NAME = 'Steam Community';
const URI = 'https://www.steamcommunity.com';
const DESCRIPTION = 'Get the latest community updates for a game on Steam.';
const MAINTAINER = 'thefranke';
const CACHE_TIMEOUT = 3600; // 1h
const PARAMETERS = array(
array(
'i' => array(
'name' => 'App ID',
'required' => true
),
'category' => array(
'name' => 'category',
'type' => 'list',
'exampleValue' => 'Artwork',
'title' => 'Select a category',
'values' => array(
'Artwork' => 'images',
'Screenshots' => 'screenshots',
'Videos' => 'videos',
'Workshop' => 'workshop'
)
)
)
);
public function getIcon() {
return self::URI . '/favicon.ico';
}
protected function getMainPage() {
$category = $this->getInput('category');
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not fetch Steam data.');
return $html;
}
public function getName() {
$category = $this->getInput('category');
if (is_null('i') || is_null($category)) {
return self::NAME;
}
$html = $this->getMainPage();
$titleItem = $html->find('div.apphub_AppName', 0);
if (!$titleItem)
return self::NAME;
return $titleItem->innertext . ' (' . ucwords($category) . ')';
}
public function getURI() {
if ($this->getInput('category') === 'workshop')
return self::URI . '/workshop/browse/?appid='
. $this->getInput('i') . '&browsesort=mostrecent';
return self::URI . '/app/'
. $this->getInput('i') . '/'
. $this->getInput('category')
. '/?p=1&browsefilter=mostrecent';
}
private function collectMedia() {
$category = $this->getInput('category');
$html = $this->getMainPage();
$cards = $html->find('div.apphub_Card');
foreach($cards as $card) {
$uri = $card->getAttribute('data-modal-content-url');
$htmlCard = getSimpleHTMLDOMCached($uri);
$author = $card->find('div.apphub_CardContentAuthorName', 0)->innertext;
$author = strip_tags($author);
$title = $author . '\'s screenshot';
if ($category != 'screenshots')
$title = $htmlCard->find('div.workshopItemTitle', 0)->innertext;
$date = $htmlCard->find('div.detailsStatRight', 0)->innertext;
// create item
$item = array();
$item['title'] = $title;
$item['uri'] = $uri;
$item['timestamp'] = strtotime($date);
$item['author'] = $author;
$item['categories'] = $category;
$media = $htmlCard->getElementById('ActualMedia');
$mediaURI = $media->getAttribute('src');
$downloadURI = $mediaURI;
if ($category == 'videos') {
preg_match('/.*\/embed\/(.*)\?/', $mediaURI, $result);
$youtubeID = $result[1];
$mediaURI = 'https://img.youtube.com/vi/' . $youtubeID . '/hqdefault.jpg';
$downloadURI = 'https://www.youtube.com/watch?v=' . $youtubeID;
}
$desc = '';
if ($category == 'screenshots') {
$descItem = $htmlCard->find('div.screenshotDescription', 0);
if ($descItem)
$desc = $descItem->innertext;
}
if ($category == 'images') {
$descItem = $htmlCard->find('div.nonScreenshotDescription', 0);
if ($descItem)
$desc = $descItem->innertext;
$downloadURI = $htmlCard->find('a.downloadImage', 0)->href;
}
$item['content'] = '<p><a href="' . $downloadURI . '"><img src="' . $mediaURI . '"/></a></p>';
$item['content'] .= '<p>' . $desc . '</p>';
$this->items[] = $item;
if (count($this->items) >= 10)
break;
}
}
private function collectWorkshop() {
$category = $this->getInput('category');
$html = $this->getMainPage();
$workShopItems = $html->find('div.workshopItem');
foreach($workShopItems as $workShopItem) {
$author = $workShopItem->find('div.workshopItemAuthorName', 0)->find('a', 0);
$author = $author->innertext;
$fileRating = $workShopItem->find('img.fileRating', 0);
$uri = $workShopItem->find('a.ugc', 0)->getAttribute('href');
$htmlItem = getSimpleHTMLDOMCached($uri);
$title = $htmlItem->find('div.workshopItemTitle', 0)->innertext;
$date = $htmlItem->find('div.detailsStatRight', 0)->innertext;
$description = $htmlItem->find('div.workshopItemDescription', 0)->innertext;
$previewImage = $htmlItem->find('#previewImage', 0);
$htmlTags = $htmlItem->find('div.workshopTags');
$tags = '';
foreach($htmlTags as $htmlTag) {
if ($tags !== '')
$tags .= ',';
$tags .= $htmlTag->find('a', 0)->innertext;
}
// create item
$item = array();
$item['title'] = $title;
$item['uri'] = $uri;
$item['timestamp'] = strtotime($date);
$item['author'] = $author;
$item['categories'] = $category;
$item['content'] = '<p><a href="' . $uri . '">'
. $previewImage . '</a></p><p>' . $fileRating
. '</p><p>' . $description . '</p>';
$this->items[] = $item;
if (count($this->items) >= 10)
break;
}
}
public function collectData() {
if ($this->getInput('category') === 'workshop')
$this->collectWorkshop();
else
$this->collectMedia();
}
}

View File

@@ -0,0 +1,80 @@
<?php
class StockFilingsBridge extends FeedExpander {
const MAINTAINER = 'captn3m0';
const NAME = 'SEC Stock filings';
const URI = 'https://www.sec.gov/edgar/searchedgar/companysearch.html';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Tracks SEC Filings for a single company';
const SEARCH_URL = 'https://www.sec.gov/cgi-bin/browse-edgar?owner=exclude&action=getcompany&CIK=';
const WEBSITE_ROOT = 'https://www.sec.gov';
const PARAMETERS = array(
array(
'ticker' => array(
'name' => 'cik',
'required' => true,
'exampleValue' => 'AMD',
// https://stackoverflow.com/a/12827734
'pattern' => '[A-Za-z0-9]+',
),
));
public function getIcon() {
return 'https://www.sec.gov/favicon.ico';
}
/**
* Generates search URL
*/
private function getSearchUrl() {
return self::SEARCH_URL . $this->getInput('ticker');
}
/**
* Returns the Company Name
*/
private function getRssFeed($html) {
$links = $html->find('#contentDiv a');
foreach ($links as $link) {
$href = $link->href;
if (substr($href, 0, 4) !== 'http') {
$href = self::WEBSITE_ROOT . $href;
}
parse_str(html_entity_decode(parse_url($href, PHP_URL_QUERY)), $query);
if (isset($query['output']) and ($query['output'] == 'atom')) {
return $href;
}
}
return false;
}
/**
* Return \simple_html_dom object
* for the entire html of the product page
*/
private function getHtml() {
$uri = $this->getSearchUrl();
return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request SEC.');
}
/**
* Scrape the SEC Stock Filings RSS Feed URL
* and redirect there
*/
public function collectData() {
$html = $this->getHtml();
$rssFeedUrl = $this->getRssFeed($html);
if ($rssFeedUrl) {
parent::collectExpandableDatas($rssFeedUrl);
} else {
returnClientError('Could not find RSS Feed URL. Are you sure you used a valid CIK?');
}
}
}

301
bridges/TelegramBridge.php Normal file
View File

@@ -0,0 +1,301 @@
<?php
class TelegramBridge extends BridgeAbstract {
const NAME = 'Telegram Bridge';
const URI = 'https://t.me';
const DESCRIPTION = 'Returns newest posts from a public Telegram channel';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(array(
'username' => array(
'name' => 'Username',
'type' => 'text',
'required' => true,
'exampleValue' => '@telegram',
)
)
);
const CACHE_TIMEOUT = 900; // 15 mins
private $feedName = '';
private $enclosures = array();
private $itemTitle = '';
private $backgroundImageRegex = "/background-image:url\('(.*)'\)/";
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request: ' . $this->getURI());
$channelTitle = htmlspecialchars_decode(
$html->find('div.tgme_channel_info_header_title span', 0)->plaintext,
ENT_QUOTES
);
$this->feedName = $channelTitle . ' (@' . $this->processUsername() . ')';
foreach($html->find('div.tgme_widget_message_wrap.js-widget_message_wrap') as $index => $messageDiv) {
$this->itemTitle = '';
$this->enclosures = array();
$item = array();
$item['uri'] = $this->processUri($messageDiv);
$item['content'] = html_entity_decode($this->processContent($messageDiv), ENT_QUOTES);
$item['title'] = html_entity_decode($this->itemTitle, ENT_QUOTES);
$item['timestamp'] = $this->processDate($messageDiv);
$item['enclosures'] = $this->enclosures;
$author = trim($messageDiv->find('a.tgme_widget_message_owner_name', 0)->plaintext);
$item['author'] = html_entity_decode($author, ENT_QUOTES);
$this->items[] = $item;
}
$this->items = array_reverse($this->items);
}
public function getURI() {
if (!is_null($this->getInput('username'))) {
return self::URI . '/s/' . $this->processUsername();
}
return parent::getURI();
}
public function getName() {
if (!empty($this->feedName)) {
return $this->feedName . ' - Telegram';
}
return parent::getName();
}
private function processUsername() {
if (substr($this->getInput('username'), 0, 1) === '@') {
return substr($this->getInput('username'), 1);
}
return $this->getInput('username');
}
private function processUri($messageDiv) {
return $messageDiv->find('a.tgme_widget_message_date', 0)->href;
}
private function processContent($messageDiv) {
$message = '';
if ($messageDiv->find('div.tgme_widget_message_forwarded_from', 0)) {
$message = $messageDiv->find('div.tgme_widget_message_forwarded_from', 0)->innertext . '<br><br>';
}
if ($messageDiv->find('a.tgme_widget_message_reply', 0)) {
$message = $this->processReply($messageDiv);
}
if ($messageDiv->find('div.tgme_widget_message_sticker_wrap', 0)) {
$message .= $this->processSticker($messageDiv);
}
if ($messageDiv->find('div.tgme_widget_message_poll', 0)) {
$message .= $this->processPoll($messageDiv);
}
if ($messageDiv->find('video', 0)) {
$message .= $this->processVideo($messageDiv);
}
if ($messageDiv->find('a.tgme_widget_message_photo_wrap', 0)) {
$message .= $this->processPhoto($messageDiv);
}
if ($messageDiv->find('a.not_supported', 0)) {
$message .= $this->processNotSupported($messageDiv);
}
if ($messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)) {
$message .= $messageDiv->find('div.tgme_widget_message_text.js-message_text', 0);
$this->itemTitle = $this->ellipsisTitle(
$messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)->plaintext
);
}
if ($messageDiv->find('a.tgme_widget_message_link_preview', 0)) {
$message .= $this->processLinkPreview($messageDiv);
}
return $message;
}
private function processReply($messageDiv) {
$reply = $messageDiv->find('a.tgme_widget_message_reply', 0);
return <<<EOD
<blockquote>{$reply->find('span.tgme_widget_message_author_name', 0)->plaintext}<br>
{$reply->find('div.tgme_widget_message_text', 0)->innertext}
<a href="{$reply->href}">{$reply->href}</a></blockquote><hr>
EOD;
}
private function processSticker($messageDiv) {
if (empty($this->itemTitle)) {
$this->itemTitle = '@' . $this->processUsername() . ' posted a sticker';
}
$stickerDiv = $messageDiv->find('div.tgme_widget_message_sticker_wrap', 0);
preg_match($this->backgroundImageRegex, $stickerDiv->find('i', 0)->style, $sticker);
$this->enclosures[] = $sticker[1];
return <<<EOD
<a href="{$stickerDiv->children(0)->herf}"><img src="{$sticker[1]}"></a>
EOD;
}
private function processPoll($messageDiv) {
$poll = $messageDiv->find('div.tgme_widget_message_poll', 0);
$title = $poll->find('div.tgme_widget_message_poll_question', 0)->plaintext;
$type = $poll->find('div.tgme_widget_message_poll_type', 0)->plaintext;
if (empty($this->itemTitle)) {
$this->itemTitle = $title;
}
$pollOptions = '<ul>';
foreach ($poll->find('div.tgme_widget_message_poll_option') as $option) {
$pollOptions .= '<li>' . $option->children(0)->plaintext . ' - ' .
$option->find('div.tgme_widget_message_poll_option_text', 0)->plaintext . '</li>';
}
$pollOptions .= '</ul>';
return <<<EOD
{$title}<br><small>$type</small><br>{$pollOptions}
EOD;
}
private function processLinkPreview($messageDiv) {
$image = '';
$title = '';
$site = '';
$description = '';
$preview = $messageDiv->find('a.tgme_widget_message_link_preview', 0);
if (trim($preview->innertext) === '') {
return '';
}
if($preview->find('i', 0) &&
preg_match($this->backgroundImageRegex, $preview->find('i', 0)->style, $photo)) {
$image = '<img src="' . $photo[1] . '"/>';
$this->enclosures[] = $photo[1];
}
if ($preview->find('div.link_preview_title', 0)) {
$title = $preview->find('div.link_preview_title', 0)->plaintext;
}
if ($preview->find('div.link_preview_site_name', 0)) {
$site = $preview->find('div.link_preview_site_name', 0)->plaintext;
}
if ($preview->find('div.link_preview_description', 0)) {
$description = $preview->find('div.link_preview_description', 0)->plaintext;
}
return <<<EOD
<blockquote><a href="{$preview->href}">$image</a><br><a href="{$preview->href}">
{$title} - {$site}</a><br>{$description}</blockquote>
EOD;
}
private function processVideo($messageDiv) {
if (empty($this->itemTitle)) {
$this->itemTitle = '@' . $this->processUsername() . ' posted a video';
}
if ($messageDiv->find('i.tgme_widget_message_video_thumb')) {
preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo);
} elseif ($messageDiv->find('i.link_preview_video_thumb')) {
preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo);
}
$this->enclosures[] = $photo[1];
return <<<EOD
<video controls="" poster="{$photo[1]}" preload="none">
<source src="{$messageDiv->find('video', 0)->src}" type="video/mp4">
</video>
EOD;
}
private function processPhoto($messageDiv) {
if (empty($this->itemTitle)) {
$this->itemTitle = '@' . $this->processUsername() . ' posted a photo';
}
$photos = '';
foreach ($messageDiv->find('a.tgme_widget_message_photo_wrap') as $photoWrap) {
preg_match($this->backgroundImageRegex, $photoWrap->style, $photo);
$this->enclosures[] = $photo[1];
$photos .= <<<EOD
<a href="{$photoWrap->href}"><img src="{$photo[1]}"/></a><br>
EOD;
}
return $photos;
}
private function processNotSupported($messageDiv) {
if (empty($this->itemTitle)) {
$this->itemTitle = '@' . $this->processUsername() . ' posted a video';
}
if ($messageDiv->find('i.tgme_widget_message_video_thumb')) {
preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo);
} elseif ($messageDiv->find('i.link_preview_video_thumb')) {
preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo);
}
$this->enclosures[] = $photo[1];
return <<<EOD
<a href="{$messageDiv->find('a.not_supported', 0)->href}">
{$messageDiv->find('div.message_media_not_supported_label', 0)->innertext}<br><br>
{$messageDiv->find('span.message_media_view_in_telegram', 0)->innertext}<br><br>
<img src="{$photo[1]}"/></a>
EOD;
}
private function processDate($messageDiv) {
$messageMeta = $messageDiv->find('span.tgme_widget_message_meta', 0);
return $messageMeta->find('time', 0)->datetime;
}
private function ellipsisTitle($text) {
$length = 100;
if (strlen($text) > $length) {
$text = explode('<br>', wordwrap($text, $length, '<br>'));
return $text[0] . '...';
}
return $text;
}
}

View File

@@ -28,7 +28,31 @@ class TwitterBridge extends BridgeAbstract {
'name' => 'Keyword or #hashtag',
'required' => true,
'exampleValue' => 'rss-bridge, #rss-bridge',
'title' => 'Insert a keyword or hashtag'
'title' => <<<EOD
* To search for multiple words (must contain all of these words), put a space between them.
Example: `rss-bridge release`.
* To search for multiple words (contains any of these words), put "OR" between them.
Example: `rss-bridge OR rssbridge`.
* To search for an exact phrase (including whitespace), put double-quotes around them.
Example: `"rss-bridge release"`
* If you want to search for anything **but** a specific word, put a hyphen before it.
Example: `rss-bridge -release` (ignores "release")
* Of course, this also works for hashtags.
Example: `#rss-bridge OR #rssbridge`
* And you can combine them in any shape or form you like.
Example: `#rss-bridge OR #rssbridge -release`
EOD
)
),
'By username' => array(
@@ -165,7 +189,7 @@ class TwitterBridge extends BridgeAbstract {
// Skip retweets?
if($this->getInput('noretweet')
&& strcasecmp($tweet->getAttribute('data-screen-name'), $this->getInput('u'))) {
&& $tweet->find('div.context span.js-retweet-text a', 0)) {
continue;
}
@@ -189,8 +213,8 @@ class TwitterBridge extends BridgeAbstract {
$item['fullname'] = htmlspecialchars_decode($tweet->getAttribute('data-name'), ENT_QUOTES);
// get author
$item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
if(strcasecmp($tweet->getAttribute('data-screen-name'), $this->getInput('u'))) {
$item['author'] .= ' RT: @' . $this->getInput('u');
if($rt = $tweet->find('div.context span.js-retweet-text a', 0)) {
$item['author'] .= ' RT: @' . $rt->plaintext;
}
// get avatar link
$item['avatar'] = $tweet->find('img', 0)->src;
@@ -245,22 +269,26 @@ EOD;
// Add embeded image to content
$image_html = '';
$image = $this->getImageURI($tweet);
if(!$this->getInput('noimg') && !is_null($image)) {
// Set image scaling
$image_orig = $this->getInput('noimgscaling') ? $image : $image . ':orig';
$image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb';
$images = $this->getImageURI($tweet);
if(!$this->getInput('noimg') && !is_null($images)) {
// add enclosures
$item['enclosures'] = array($image_orig);
foreach ($images as $image) {
$image_html = <<<EOD
// Set image scaling
$image_orig = $this->getInput('noimgscaling') ? $image : $image . ':orig';
$image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb';
// add enclosures
$item['enclosures'][] = $image_orig;
$image_html .= <<<EOD
<a href="{$image_orig}">
<img
style="align:top; max-width:558px; border:1px solid black;"
src="{$image_thumb}" />
</a>
EOD;
}
}
// add content
@@ -291,22 +319,27 @@ EOD;
// Add embeded image to content
$quotedImage_html = '';
$quotedImage = $this->getQuotedImageURI($tweet);
if(!$this->getInput('noimg') && !is_null($quotedImage)) {
// Set image scaling
$quotedImage_orig = $this->getInput('noimgscaling') ? $quotedImage : $quotedImage . ':orig';
$quotedImage_thumb = $this->getInput('noimgscaling') ? $quotedImage : $quotedImage . ':thumb';
$quotedImages = $this->getQuotedImageURI($tweet);
// add enclosures
$item['enclosures'] = array($quotedImage_orig);
if(!$this->getInput('noimg') && !is_null($quotedImages)) {
$quotedImage_html = <<<EOD
<a href="{$quotedImage_orig}">
foreach ($quotedImages as $image) {
// Set image scaling
$image_orig = $this->getInput('noimgscaling') ? $image : $image . ':orig';
$image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb';
// add enclosures
$item['enclosures'][] = $image_orig;
$quotedImage_html .= <<<EOD
<a href="{$image_orig}">
<img
style="align:top; max-width:558px; border:1px solid black;"
src="{$quotedImage_thumb}" />
src="{$image_thumb}" />
</a>
EOD;
}
}
$item['content'] = <<<EOD
@@ -360,9 +393,18 @@ EOD;
private function getImageURI($tweet){
// Find media in tweet
$images = array();
$container = $tweet->find('div.AdaptiveMedia-container', 0);
if($container && $container->find('img', 0)) {
return $container->find('img', 0)->src;
foreach ($container->find('img') as $img) {
$images[] = $img->src;
}
}
if (!empty($images)) {
return $images;
}
return null;
@@ -370,9 +412,18 @@ EOD;
private function getQuotedImageURI($tweet){
// Find media in tweet
$images = array();
$container = $tweet->find('div.QuoteMedia-container', 0);
if($container && $container->find('img', 0)) {
return $container->find('img', 0)->src;
foreach ($container->find('img') as $img) {
$images[] = $img->src;
}
}
if (!empty($images)) {
return $images;
}
return null;

175
bridges/VimeoBridge.php Normal file
View File

@@ -0,0 +1,175 @@
<?php
class VimeoBridge extends BridgeAbstract {
const NAME = 'Vimeo Bridge';
const URI = 'https://vimeo.com/';
const DESCRIPTION = 'Returns search results from Vimeo';
const MAINTAINER = 'logmanoriginal';
const PARAMETERS = array(
array(
'q' => array(
'name' => 'Search Query',
'type' => 'text',
'required' => true
),
'type' => array(
'name' => 'Show results for',
'type' => 'list',
'defaultValue' => 'Videos',
'values' => array(
'Videos' => 'search',
'On Demand' => 'search/ondemand',
'People' => 'search/people',
'Channels' => 'search/channels',
'Groups' => 'search/groups'
)
)
)
);
public function getURI() {
if(($query = $this->getInput('q'))
&& ($type = $this->getInput('type'))) {
return self::URI . $type . '/sort:latest?q=' . $query;
}
return parent::getURI();
}
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI(),
$header = array(),
$opts = array(),
$lowercase = true,
$forceTagsClosed = true,
$target_charset = DEFAULT_TARGET_CHARSET,
$stripRN = false, // We want to keep newline characters
$defaultBRText = DEFAULT_BR_TEXT,
$defaultSpanText = DEFAULT_SPAN_TEXT)
or returnServerError('Could not request ' . $this->getURI());
$json = null; // Holds the JSON data
/**
* Search results are included as JSON formatted string inside a script
* tag that has the variable 'vimeo.config'. The data is condensed into
* a single line of code, so we can just search for the newline.
*
* Everything after "vimeo.config = _extend((vimeo.config || {}), " is
* the JSON formatted string.
*/
foreach($html->find('script') as $script) {
foreach(explode("\n", $script) as $line) {
$line = trim($line);
if(strpos($line, 'vimeo.config') !== 0)
continue;
// 45 = strlen("vimeo.config = _extend((vimeo.config || {}), ");
// 47 = 45 + 2, because we don't want the final ");"
$json = json_decode(substr($line, 45, strlen($line) - 47));
}
}
if(is_null($json)) {
returnClientError('No results for this query!');
}
foreach($json->api->initial_json->data as $element) {
switch($element->type) {
case 'clip': $this->addClip($element); break;
case 'ondemand': $this->addOnDemand($element); break;
case 'people': $this->addPeople($element); break;
case 'channel': $this->addChannel($element); break;
case 'group': $this->addGroup($element); break;
default: returnServerError('Unknown type: ' . $element->type);
}
}
}
private function addClip($element) {
$item = array();
$item['uri'] = $element->clip->link;
$item['title'] = $element->clip->name;
$item['author'] = $element->clip->user->name;
$item['timestamp'] = strtotime($element->clip->created_time);
$item['enclosures'] = array(
end($element->clip->pictures->sizes)->link
);
$item['content'] = "<img src={$item['enclosures'][0]} />";
$this->items[] = $item;
}
private function addOnDemand($element) {
$item = array();
$item['uri'] = $element->ondemand->link;
$item['title'] = $element->ondemand->name;
// Only for films
if(isset($element->ondemand->film))
$item['timestamp'] = strtotime($element->ondemand->film->release_time);
$item['enclosures'] = array(
end($element->ondemand->pictures->sizes)->link
);
$item['content'] = "<img src={$item['enclosures'][0]} />";
$this->items[] = $item;
}
private function addPeople($element) {
$item = array();
$item['uri'] = $element->people->link;
$item['title'] = $element->people->name;
$item['enclosures'] = array(
end($element->people->pictures->sizes)->link
);
$item['content'] = "<img src={$item['enclosures'][0]} />";
$this->items[] = $item;
}
private function addChannel($element) {
$item = array();
$item['uri'] = $element->channel->link;
$item['title'] = $element->channel->name;
$item['enclosures'] = array(
end($element->channel->pictures->sizes)->link
);
$item['content'] = "<img src={$item['enclosures'][0]} />";
$this->items[] = $item;
}
private function addGroup($element) {
$item = array();
$item['uri'] = $element->group->link;
$item['title'] = $element->group->name;
$item['enclosures'] = array(
end($element->group->pictures->sizes)->link
);
$item['content'] = "<img src={$item['enclosures'][0]} />";
$this->items[] = $item;
}
}

View File

@@ -13,6 +13,10 @@ class VkBridge extends BridgeAbstract
'u' => array(
'name' => 'Group or user name',
'required' => true
),
'hide_reposts' => array(
'name' => 'Hide reposts',
'type' => 'checkbox',
)
)
);
@@ -48,7 +52,7 @@ class VkBridge extends BridgeAbstract
$text_html = $this->getContents()
or returnServerError('No results for group or user name "' . $this->getInput('u') . '".');
$text_html = iconv('windows-1251', 'utf-8', $text_html);
$text_html = iconv('windows-1251', 'utf-8//ignore', $text_html);
// makes album link generating work correctly
$text_html = str_replace('"class="page_album_link">', '" class="page_album_link">', $text_html);
$html = str_get_html($text_html);
@@ -234,6 +238,9 @@ class VkBridge extends BridgeAbstract
}
if (is_object($post->find('div.copy_quote', 0))) {
if ($this->getInput('hide_reposts') === true) {
continue;
}
$copy_quote = $post->find('div.copy_quote', 0);
if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) {
$copy_post_header->outertext = '';

View File

@@ -71,16 +71,4 @@ class WordPressPluginUpdateBridge extends BridgeAbstract {
return parent::getName();
}
private function getCachedDate($url){
Debug::log('getting pubdate from url ' . $url . '');
// Initialize cache
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$cache->setPath(PATH_CACHE . 'pages/');
$params = [$url];
$cache->setParameters($params);
// Get cachefile timestamp
$time = $cache->getTime();
return ($time !== false ? $time : time());
}
}

View File

@@ -3,7 +3,7 @@ class WorldOfTanksBridge extends FeedExpander {
const MAINTAINER = 'Riduidel';
const NAME = 'World of Tanks';
const URI = 'http://worldoftanks.eu/';
const URI = 'https://worldoftanks.eu/';
const DESCRIPTION = 'News about the tank slaughter game.';
const PARAMETERS = array( array(
@@ -22,6 +22,8 @@ class WorldOfTanksBridge extends FeedExpander {
)
));
const POSSIBLE_ARTICLES = array('article', 'rich-article');
public function collectData() {
$this->collectExpandableDatas(sprintf('https://worldoftanks.eu/%s/rss/news/', $this->getInput('lang')));
}
@@ -40,13 +42,17 @@ class WorldOfTanksBridge extends FeedExpander {
private function loadFullArticle($uri){
$html = getSimpleHTMLDOMCached($uri);
$content = $html->find('article', 0);
foreach(self::POSSIBLE_ARTICLES as $article_class) {
$content = $html->find('article', 0);
// Remove the scripts, please
foreach($content->find('script') as $script) {
$script->outertext = '';
if($content !== null) {
// Remove the scripts, please
foreach($content->find('script') as $script) {
$script->outertext = '';
}
return $content->innertext;
}
}
return $content->innertext;
return null;
}
}

View File

@@ -118,7 +118,7 @@ class XenForoBridge extends BridgeAbstract {
// Notice: The DOM structure changes depending on the XenForo version used
if($mainContent = $html->find('div.mainContent', 0)) {
$this->version = self::XENFORO_VERSION_1;
} elseif ($mainContent = $html->find('div[class="p-body"]', 0)) {
} elseif ($mainContent = $html->find('div[class~="p-body"]', 0)) {
$this->version = self::XENFORO_VERSION_2;
} else {
returnServerError('This forum is currently not supported!');
@@ -127,7 +127,7 @@ class XenForoBridge extends BridgeAbstract {
switch($this->version) {
case self::XENFORO_VERSION_1:
$titleBar = $mainContent->find('div.titleBar h1', 0)
$titleBar = $mainContent->find('div.titleBar > h1', 0)
or returnServerError('Error finding title bar!');
$this->title = $titleBar->plaintext;
@@ -140,7 +140,7 @@ class XenForoBridge extends BridgeAbstract {
case self::XENFORO_VERSION_2:
$titleBar = $mainContent->find('div[class="p-title"] h1', 0)
$titleBar = $mainContent->find('div[class~="p-title"] h1', 0)
or returnServerError('Error finding title bar!');
$this->title = $titleBar->plaintext;
@@ -166,7 +166,7 @@ class XenForoBridge extends BridgeAbstract {
$lang = $html->find('html', 0)->lang;
// Posts are contained in an "ol"
$messageList = $html->find('#messageList li')
$messageList = $html->find('#messageList > li')
or returnServerError('Error finding message list!');
foreach($messageList as $post) {
@@ -179,7 +179,7 @@ class XenForoBridge extends BridgeAbstract {
$item['uri'] = $url . '#' . $post->getAttribute('id');
$content = $post->find('.messageContent article', 0);
$content = $post->find('.messageContent > article', 0);
// Add some style to quotes
foreach($content->find('.bbCodeQuote') as $quote) {
@@ -255,7 +255,7 @@ class XenForoBridge extends BridgeAbstract {
$lang = $html->find('html', 0)->lang;
$messageList = $html->find('div[class="block-body"] article')
$messageList = $html->find('div[class~="block-body"] article')
or returnServerError('Error finding message list!');
foreach($messageList as $post) {
@@ -268,13 +268,17 @@ class XenForoBridge extends BridgeAbstract {
$item['uri'] = $url . '#' . $post->getAttribute('id');
$title = $post->find('div[class="message-content"] article', 0)->plaintext;
$title = $post->find('div[class~="message-content"] article', 0)->plaintext;
$end = strpos($title, ' ', 70);
$item['title'] = substr($title, 0, $end);
$item['timestamp'] = $this->fixDate($post->find('time', 0)->title, $lang);
if ($post->find('time[datetime]', 0)) {
$item['timestamp'] = $post->find('time[datetime]', 0)->datetime;
} else {
$item['timestamp'] = $this->fixDate($post->find('time', 0)->title, $lang);
}
$item['author'] = $post->getAttribute('data-author');
$item['content'] = $post->find('div[class="message-content"] article', 0);
$item['content'] = $post->find('div[class~="message-content"] article', 0);
// Bridge specific properties
$item['id'] = $post->getAttribute('id');
@@ -305,7 +309,7 @@ class XenForoBridge extends BridgeAbstract {
// Load at least the last page
do {
$pageurl = $hosturl . str_replace($sentinel, $lastpage, $baseurl);
$pageurl = str_replace($sentinel, $lastpage, $baseurl);
// We can optimize performance by caching all but the last page
if($page != $lastpage) {
@@ -339,7 +343,7 @@ class XenForoBridge extends BridgeAbstract {
}
// Manually extract baseurl and inject sentinel
$baseurl = $pageNav->find('li a', -1)->href;
$baseurl = $pageNav->find('li > a', -1)->href;
$baseurl = str_replace('page-' . $lastpage, 'page-{{sentinel}}', $baseurl);
$sentinel = '{{sentinel}}';
@@ -353,7 +357,7 @@ class XenForoBridge extends BridgeAbstract {
// Load at least the last page
do {
$pageurl = $hosturl . str_replace($sentinel, $lastpage, $baseurl);
$pageurl = str_replace($sentinel, $lastpage, $baseurl);
// We can optimize performance by caching all but the last page
if($page != $lastpage) {
@@ -364,9 +368,9 @@ class XenForoBridge extends BridgeAbstract {
or returnServerError('Error loading contents from ' . $pageurl . '!');
}
$html = defaultLinkTo($html, $this->hosturl);
$html = defaultLinkTo($html, $hosturl);
$this->extractThreadPostsV2($html, $this->pageurl);
$this->extractThreadPostsV2($html, $pageurl);
$page--;

View File

@@ -65,7 +65,7 @@ class YoutubeBridge extends BridgeAbstract {
private $feedName = '';
private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time){
$html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid");
$html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid", true);
// Skip unavailable videos
if(!strpos($html->innertext, 'IS_UNAVAILABLE_PAGE')) {
@@ -127,7 +127,6 @@ class YoutubeBridge extends BridgeAbstract {
}
private function ytBridgeParseHtmlListing($html, $element_selector, $title_selector, $add_parsed_items = true) {
$limit = $add_parsed_items ? 10 : INF;
$count = 0;
$duration_min = $this->getInput('duration_min') ?: -1;
@@ -141,40 +140,38 @@ class YoutubeBridge extends BridgeAbstract {
}
foreach($html->find($element_selector) as $element) {
if($count < $limit) {
$author = '';
$desc = '';
$time = 0;
$vid = str_replace('/watch?v=', '', $element->find('a', 0)->href);
$vid = substr($vid, 0, strpos($vid, '&') ?: strlen($vid));
$title = trim($this->ytBridgeFixTitle($element->find($title_selector, 0)->plaintext));
$author = '';
$desc = '';
$time = 0;
$vid = str_replace('/watch?v=', '', $element->find('a', 0)->href);
$vid = substr($vid, 0, strpos($vid, '&') ?: strlen($vid));
$title = trim($this->ytBridgeFixTitle($element->find($title_selector, 0)->plaintext));
if (strpos($vid, 'googleads') !== false
|| $title == '[Private video]'
|| $title == '[Deleted video]'
) {
continue;
}
// The duration comes in one of the formats:
// hh:mm:ss / mm:ss / m:ss
// 01:03:30 / 15:06 / 1:24
$durationText = trim($element->find('div.timestamp span', 0)->plaintext);
$durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText);
sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds);
$duration = $hours * 3600 + $minutes * 60 + $seconds;
if($duration < $duration_min || $duration > $duration_max) {
continue;
}
if ($add_parsed_items) {
$this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
}
$count++;
if (strpos($vid, 'googleads') !== false
|| $title == '[Private video]'
|| $title == '[Deleted video]'
) {
continue;
}
// The duration comes in one of the formats:
// hh:mm:ss / mm:ss / m:ss
// 01:03:30 / 15:06 / 1:24
$durationText = trim($element->find('div.timestamp span', 0)->plaintext);
$durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText);
sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds);
$duration = $hours * 3600 + $minutes * 60 + $seconds;
if($duration < $duration_min || $duration > $duration_max) {
continue;
}
if ($add_parsed_items) {
$this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
}
$count++;
}
return $count;
}
@@ -184,18 +181,38 @@ class YoutubeBridge extends BridgeAbstract {
return html_entity_decode($title, ENT_QUOTES, 'UTF-8');
}
private function ytGetSimpleHTMLDOM($url){
private function ytGetSimpleHTMLDOM($url, $cached = false){
$header = array(
'Accept-Language: en-US'
);
$opts = array();
$lowercase = true;
$forceTagsClosed = true;
$target_charset = DEFAULT_TARGET_CHARSET;
$stripRN = false;
$defaultBRText = DEFAULT_BR_TEXT;
$defaultSpanText = DEFAULT_SPAN_TEXT;
if ($cached) {
return getSimpleHTMLDOMCached($url,
86400,
$header,
$opts,
$lowercase,
$forceTagsClosed,
$target_charset,
$stripRN,
$defaultBRText,
$defaultSpanText);
}
return getSimpleHTMLDOM($url,
$header = array(
'Accept-Language: en-US'
),
$opts = array(),
$lowercase = true,
$forceTagsClosed = true,
$target_charset = DEFAULT_TARGET_CHARSET,
$stripRN = false,
$defaultBRText = DEFAULT_BR_TEXT,
$defaultSpanText = DEFAULT_SPAN_TEXT);
$header,
$opts,
$lowercase,
$forceTagsClosed,
$target_charset,
$stripRN,
$defaultBRText,
$defaultSpanText);
}
public function collectData(){

0
cache/pages/.gitkeep vendored Normal file
View File

0
cache/server/.gitkeep vendored Normal file
View File

View File

@@ -3,20 +3,21 @@
* Cache with file system
*/
class FileCache implements CacheInterface {
protected $path;
protected $param;
protected $key;
public function loadData(){
if(file_exists($this->getCacheFile())) {
return unserialize(file_get_contents($this->getCacheFile()));
}
return null;
}
public function saveData($datas){
public function saveData($data){
// Notice: We use plain serialize() here to reduce memory footprint on
// large input data.
$writeStream = file_put_contents($this->getCacheFile(), serialize($datas));
$writeStream = file_put_contents($this->getCacheFile(), serialize($data));
if($writeStream === false) {
throw new \Exception('Cannot write the cache... Do you have the right permissions ?');
@@ -29,13 +30,14 @@ class FileCache implements CacheInterface {
$cacheFile = $this->getCacheFile();
clearstatcache(false, $cacheFile);
if(file_exists($cacheFile)) {
return filemtime($cacheFile);
$time = filemtime($cacheFile);
return ($time !== false) ? $time : null;
}
return false;
return null;
}
public function purgeCache($duration){
public function purgeCache($seconds){
$cachePath = $this->getPath();
if(file_exists($cachePath)) {
$cacheIterator = new RecursiveIteratorIterator(
@@ -47,7 +49,7 @@ class FileCache implements CacheInterface {
if(in_array($cacheFile->getBasename(), array('.', '..', '.gitkeep')))
continue;
elseif($cacheFile->isFile()) {
if(filemtime($cacheFile->getPathname()) < time() - $duration)
if(filemtime($cacheFile->getPathname()) < time() - $seconds)
unlink($cacheFile->getPathname());
}
}
@@ -55,34 +57,34 @@ class FileCache implements CacheInterface {
}
/**
* Set cache path
* Set scope
* @return self
*/
public function setPath($path){
if(is_null($path) || !is_string($path)) {
throw new \Exception('The given path is invalid!');
public function setScope($scope){
if(is_null($scope) || !is_string($scope)) {
throw new \Exception('The given scope is invalid!');
}
$this->path = $path;
// Make sure path ends with '/' or '\'
$lastchar = substr($this->path, -1, 1);
if($lastchar !== '/' && $lastchar !== '\\')
$this->path .= '/';
if(!is_dir($this->path))
mkdir($this->path, 0755, true);
$this->path = PATH_CACHE . trim($scope, " \t\n\r\0\x0B\\\/") . '/';
return $this;
}
/**
* Set HTTP GET parameters
* Set key
* @return self
*/
public function setParameters(array $param){
$this->param = array_map('strtolower', $param);
public function setKey($key){
if (!empty($key) && is_array($key)) {
$key = array_map('strtolower', $key);
}
$key = json_encode($key);
if (!is_string($key)) {
throw new \Exception('The given key is invalid!');
}
$this->key = $key;
return $this;
}
@@ -90,9 +92,15 @@ class FileCache implements CacheInterface {
* Return cache path (and create if not exist)
* @return string Cache path
*/
protected function getPath(){
private function getPath(){
if(is_null($this->path)) {
throw new \Exception('Call "setPath" first!');
throw new \Exception('Call "setScope" first!');
}
if(!is_dir($this->path)) {
if (mkdir($this->path, 0755, true) !== true) {
throw new \Exception('Unable to create ' . $this->path);
}
}
return $this->path;
@@ -102,7 +110,7 @@ class FileCache implements CacheInterface {
* Get the file name use for cache store
* @return string Path to the file cache
*/
protected function getCacheFile(){
private function getCacheFile(){
return $this->getPath() . $this->getCacheName();
}
@@ -110,13 +118,11 @@ class FileCache implements CacheInterface {
* Determines file name for store the cache
* return string
*/
protected function getCacheName(){
if(is_null($this->param)) {
throw new \Exception('Call "setParameters" first!');
private function getCacheName(){
if(is_null($this->key)) {
throw new \Exception('Call "setKey" first!');
}
// Change character when making incompatible changes to prevent loading
// errors due to incompatible file contents \|/
return hash('md5', http_build_query($this->param) . 'A') . '.cache';
return hash('md5', $this->key) . '.cache';
}
}

115
caches/MemcachedCache.php Normal file
View File

@@ -0,0 +1,115 @@
<?php
class MemcachedCache implements CacheInterface {
private $scope;
private $key;
private $conn;
private $expiration = 0;
private $time = false;
private $data = null;
public function __construct() {
if (!extension_loaded('memcached')) {
returnServerError('"memcached" extension not loaded. Please check "php.ini"');
}
$host = Configuration::getConfig(get_called_class(), 'host');
$port = Configuration::getConfig(get_called_class(), 'port');
if (empty($host) && empty($port)) {
returnServerError('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG);
} else if (empty($host)) {
returnServerError('"host" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
} else if (empty($port)) {
returnServerError('"port" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
} else if (!ctype_digit($port)) {
returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
}
$port = intval($port);
if ($port < 1 || $port > 65535) {
returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
}
$conn = new Memcached();
$conn->addServer($host, $port) or returnServerError('Could not connect to memcached server');
$this->conn = $conn;
}
public function loadData(){
if ($this->data) return $this->data;
$result = $this->conn->get($this->getCacheKey());
if ($result === false) {
return false;
}
$this->time = $result['time'];
$this->data = $result['data'];
return $result['data'];
}
public function saveData($datas){
$time = time();
$object_to_save = array(
'data' => $datas,
'time' => $time,
);
$result = $this->conn->set($this->getCacheKey(), $object_to_save, $this->expiration);
if($result === false) {
returnServerError('Cannot write the cache to memcached server');
}
$this->time = $time;
return $this;
}
public function getTime(){
if ($this->time === false) {
$this->loadData();
}
return $this->time;
}
public function purgeCache($duration){
// Note: does not purges cache right now
// Just sets cache expiration and leave cache purging for memcached itself
$this->expiration = $duration;
}
/**
* Set scope
* @return self
*/
public function setScope($scope){
$this->scope = $scope;
return $this;
}
/**
* Set key
* @return self
*/
public function setKey($key){
if (!empty($key) && is_array($key)) {
$key = array_map('strtolower', $key);
}
$key = json_encode($key);
if (!is_string($key)) {
throw new \Exception('The given key is invalid!');
}
$this->key = $key;
return $this;
}
private function getCacheKey(){
if(is_null($this->key)) {
returnServerError('Call "setKey" first!');
}
return 'rss_bridge_cache_' . hash('md5', $this->scope . $this->key . 'A');
}
}

View File

@@ -3,16 +3,25 @@
* Cache based on SQLite 3 <https://www.sqlite.org>
*/
class SQLiteCache implements CacheInterface {
protected $path;
protected $param;
protected $scope;
protected $key;
private $db = null;
public function __construct() {
if (!extension_loaded('sqlite3'))
if (!extension_loaded('sqlite3')) {
die('"sqlite3" extension not loaded. Please check "php.ini"');
}
$file = PATH_CACHE . 'cache.sqlite';
$file = Configuration::getConfig(get_called_class(), 'file');
if (empty($file)) {
die('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG);
}
if (dirname($file) == '.') {
$file = PATH_CACHE . $file;
} elseif (!is_dir(dirname($file))) {
die('Invalid configuration for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
}
if (!is_file($file)) {
$this->db = new SQLite3($file);
@@ -39,10 +48,10 @@ class SQLiteCache implements CacheInterface {
return null;
}
public function saveData($datas){
public function saveData($data){
$Qupdate = $this->db->prepare('INSERT OR REPLACE INTO storage (key, value, updated) VALUES (:key, :value, :updated)');
$Qupdate->bindValue(':key', $this->getCacheKey());
$Qupdate->bindValue(':value', serialize($datas));
$Qupdate->bindValue(':value', serialize($data));
$Qupdate->bindValue(':updated', time());
$Qupdate->execute();
@@ -60,40 +69,53 @@ class SQLiteCache implements CacheInterface {
}
}
return false;
return null;
}
public function purgeCache($duration){
public function purgeCache($seconds){
$Qdelete = $this->db->prepare('DELETE FROM storage WHERE updated < :expired');
$Qdelete->bindValue(':expired', time() - $duration);
$Qdelete->bindValue(':expired', time() - $seconds);
$Qdelete->execute();
}
/**
* Set cache path
* Set scope
* @return self
*/
public function setPath($path){
$this->path = $path;
public function setScope($scope){
if(is_null($scope) || !is_string($scope)) {
throw new \Exception('The given scope is invalid!');
}
$this->scope = $scope;
return $this;
}
/**
* Set HTTP GET parameters
* Set key
* @return self
*/
public function setParameters(array $param){
$this->param = array_map('strtolower', $param);
public function setKey($key){
if (!empty($key) && is_array($key)) {
$key = array_map('strtolower', $key);
}
$key = json_encode($key);
if (!is_string($key)) {
throw new \Exception('The given key is invalid!');
}
$this->key = $key;
return $this;
}
////////////////////////////////////////////////////////////////////////////
protected function getCacheKey(){
if(is_null($this->param)) {
throw new \Exception('Call "setParameters" first!');
private function getCacheKey(){
if(is_null($this->key)) {
throw new \Exception('Call "setKey" first!');
}
return hash('sha1', $this->path . http_build_query($this->param), true);
return hash('sha1', $this->scope . $this->key, true);
}
}

12
composer.json Normal file
View File

@@ -0,0 +1,12 @@
{
"require": {
"php": ">=5.6",
"ext-mbstring": "*",
"ext-sqlite3": "*",
"ext-curl": "*",
"ext-openssl": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"ext-json": "*"
}
}

26
composer.lock generated Normal file
View File

@@ -0,0 +1,26 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ef341ee18f28c7bd5832e188fe157734",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.6",
"ext-mbstring": "*",
"ext-sqlite3": "*",
"ext-curl": "*",
"ext-openssl": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"ext-json": "*"
},
"platform-dev": []
}

View File

@@ -4,6 +4,14 @@
; file, it will be replaced on the next update of RSS-Bridge! You can specify
; your own configuration in 'config.ini.php' (copy this file).
[system]
; Defines the timezone used by RSS-Bridge
; Find a list of supported timezones at
; https://www.php.net/manual/en/timezones.php
; timezone = "UTC" (default)
timezone = "UTC"
[cache]
; Defines the cache type used by RSS-Bridge
@@ -52,3 +60,12 @@ username = ""
; The password for authentication. Insert this password when prompted for login.
; Use a strong password to prevent others from guessing your login!
password = ""
; --- Cache specific configuration ---------------------------------------------
[SQLiteCache]
file = "cache.sqlite"
[MemcachedCache]
host = "localhost"
port = 11211

View File

@@ -4,8 +4,21 @@ class HtmlFormat extends FormatAbstract {
$extraInfos = $this->getExtraInfos();
$title = htmlspecialchars($extraInfos['name']);
$uri = htmlspecialchars($extraInfos['uri']);
$atomquery = str_replace('format=Html', 'format=Atom', htmlentities($_SERVER['QUERY_STRING']));
$mrssquery = str_replace('format=Html', 'format=Mrss', htmlentities($_SERVER['QUERY_STRING']));
// Dynamically build buttons for all formats (except HTML)
$formatFac = new FormatFactory();
$formatFac->setWorkingDir(PATH_LIB_FORMATS);
$buttons = '';
foreach($formatFac->getFormatNames() as $format) {
if(strcasecmp($format, 'HTML') === 0) {
continue;
}
$query = str_replace('format=Html', 'format=' . $format, htmlentities($_SERVER['QUERY_STRING']));
$buttons .= $this->buildButton($format, $query) . PHP_EOL;
}
$entries = '';
foreach($this->getItems() as $item) {
@@ -82,18 +95,17 @@ EOD;
<html>
<head>
<meta charset="{$charset}">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{$title}</title>
<link href="static/HtmlFormat.css" rel="stylesheet">
<link rel="alternate" type="application/atom+xml" title="Atom" href="./?{$atomquery}" />
<link rel="alternate" type="application/rss+xml" title="RSS" href="/?{$mrssquery}" />
<link rel="icon" type="image/png" href="static/favicon.png">
<meta name="robots" content="noindex, follow">
</head>
<body>
<h1 class="pagetitle"><a href="{$uri}" target="_blank">{$title}</a></h1>
<div class="buttons">
<a href="./#bridge-{$_GET['bridge']}"><button class="backbutton">← back to rss-bridge</button></a>
<a href="./?{$atomquery}"><button class="rss-feed">RSS feed (ATOM)</button></a>
<a href="./?{$mrssquery}"><button class="rss-feed">RSS feed (MRSS)</button></a>
{$buttons}
</div>
{$entries}
</body>
@@ -113,4 +125,10 @@ EOD;
return parent::display();
}
private function buildButton($format, $query) {
return <<<EOD
<a href="./?{$query}"><button class="rss-feed">{$format}</button></a>
EOD;
}
}

View File

@@ -6,8 +6,6 @@ Configuration::loadConfiguration();
Authentication::showPromptIfNeeded();
date_default_timezone_set('UTC');
/*
Move the CLI arguments to the $_GET array, in order to be able to use
rss-bridge from the command line
@@ -29,28 +27,8 @@ define('USER_AGENT',
ini_set('user_agent', USER_AGENT);
// default whitelist
$whitelist_default = array(
'BandcampBridge',
'CryptomeBridge',
'DansTonChatBridge',
'DuckDuckGoBridge',
'FacebookBridge',
'FlickrBridge',
'GooglePlusPostBridge',
'GoogleSearchBridge',
'IdenticaBridge',
'InstagramBridge',
'OpenClassroomsBridge',
'PinterestBridge',
'ScmbBridge',
'TwitterBridge',
'WikipediaBridge',
'YoutubeBridge');
try {
Bridge::setWhitelist($whitelist_default);
$actionFac = new \ActionFactory();
$actionFac->setWorkingDir(PATH_LIB_ACTIONS);

View File

@@ -194,6 +194,11 @@ abstract class BridgeAbstract implements BridgeInterface {
*/
public function setDatas(array $inputs){
if(isset($inputs['context'])) { // Context hinting (optional)
$this->queriedContext = $inputs['context'];
unset($inputs['context']);
}
if(empty(static::PARAMETERS)) {
if(!empty($inputs)) {
@@ -218,8 +223,11 @@ abstract class BridgeAbstract implements BridgeInterface {
);
}
// Guess the paramter context from input data
$this->queriedContext = $validator->getQueriedContext($inputs, static::PARAMETERS);
// Guess the context from input data
if(empty($this->queriedContext)) {
$this->queriedContext = $validator->getQueriedContext($inputs, static::PARAMETERS);
}
if(is_null($this->queriedContext)) {
returnClientError('Required parameter(s) missing');
} elseif($this->queriedContext === false) {

View File

@@ -48,13 +48,19 @@ final class BridgeCard {
* @param bool $isHttps If disabled, adds a warning to the form
* @return string The form header
*/
private static function getFormHeader($bridgeName, $isHttps = false) {
private static function getFormHeader($bridgeName, $isHttps = false, $parameterName = '') {
$form = <<<EOD
<form method="GET" action="?">
<input type="hidden" name="action" value="display" />
<input type="hidden" name="bridge" value="{$bridgeName}" />
EOD;
if(!empty($parameterName)) {
$form .= <<<EOD
<input type="hidden" name="context" value="{$parameterName}" />
EOD;
}
if(!$isHttps) {
$form .= '<div class="secure-warning">Warning :
This bridge is not fetching its content through a secure connection</div>';
@@ -80,7 +86,7 @@ This bridge is not fetching its content through a secure connection</div>';
$isHttps = false,
$parameterName = '',
$parameters = array()) {
$form = self::getFormHeader($bridgeName, $isHttps);
$form = self::getFormHeader($bridgeName, $isHttps, $parameterName);
if(count($parameters) > 0) {
@@ -299,7 +305,10 @@ This bridge is not fetching its content through a secure connection</div>';
*/
static function displayBridgeCard($bridgeName, $formats, $isActive = true){
$bridge = Bridge::create($bridgeName);
$bridgeFac = new \BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
$bridge = $bridgeFac->create($bridgeName);
if($bridge == false)
return '';

View File

@@ -35,17 +35,7 @@
* $bridge = Bridge::create('GitHubIssue');
* ```
*/
class Bridge {
/**
* Holds a path to the working directory.
*
* Do not access this property directly!
* Use {@see Bridge::setWorkingDir()} and {@see Bridge::getWorkingDir()} instead.
*
* @var string|null
*/
protected static $workingDir = null;
class BridgeFactory extends FactoryAbstract {
/**
* Holds a list of whitelisted bridges.
@@ -55,18 +45,7 @@ class Bridge {
*
* @var array
*/
protected static $whitelist = array();
/**
* Throws an exception when trying to create a new instance of this class.
* Use {@see Bridge::create()} to instanciate a new bridge from the working
* directory.
*
* @throws \LogicException if called.
*/
public function __construct(){
throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create bridge objects!');
}
protected $whitelist = array();
/**
* Creates a new bridge object from the working directory.
@@ -77,13 +56,13 @@ class Bridge {
* @param string $name Name of the bridge object.
* @return object|bool The bridge object or false if the class is not instantiable.
*/
public static function create($name){
if(!self::isBridgeName($name)) {
public function create($name){
if(!$this->isBridgeName($name)) {
throw new \InvalidArgumentException('Bridge name invalid!');
}
$name = self::sanitizeBridgeName($name) . 'Bridge';
$filePath = self::getWorkingDir() . $name . '.php';
$name = $this->sanitizeBridgeName($name) . 'Bridge';
$filePath = $this->getWorkingDir() . $name . '.php';
if(!file_exists($filePath)) {
throw new \Exception('Bridge file ' . $filePath . ' does not exist!');
@@ -98,48 +77,6 @@ class Bridge {
return false;
}
/**
* Sets the working directory.
*
* @param string $dir Path to the directory containing bridges.
* @throws \LogicException if the provided path is not a valid string.
* @throws \Exception if the provided path does not exist.
* @throws \InvalidArgumentException if $dir is not a directory.
* @return void
*/
public static function setWorkingDir($dir){
self::$workingDir = null;
if(!is_string($dir)) {
throw new \InvalidArgumentException('Working directory is not a valid string!');
}
if(!file_exists($dir)) {
throw new \Exception('Working directory does not exist!');
}
if(!is_dir($dir)) {
throw new \InvalidArgumentException('Working directory is not a directory!');
}
self::$workingDir = realpath($dir) . '/';
}
/**
* Returns the working directory.
* The working directory must be specified with {@see Bridge::setWorkingDir()}!
*
* @throws \LogicException if the working directory is not set.
* @return string The current working directory.
*/
public static function getWorkingDir(){
if(is_null(self::$workingDir)) {
throw new \LogicException('Working directory is not set!');
}
return self::$workingDir;
}
/**
* Returns true if the provided name is a valid bridge name.
*
@@ -149,7 +86,7 @@ class Bridge {
* @param string $name The bridge name.
* @return bool true if the name is a valid bridge name, false otherwise.
*/
public static function isBridgeName($name){
public function isBridgeName($name){
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
@@ -160,12 +97,12 @@ class Bridge {
*
* @return array List of bridge names
*/
public static function getBridgeNames(){
public function getBridgeNames(){
static $bridgeNames = array(); // Initialized on first call
if(empty($bridgeNames)) {
$files = scandir(self::getWorkingDir());
$files = scandir($this->getWorkingDir());
if($files !== false) {
foreach($files as $file) {
@@ -185,14 +122,15 @@ class Bridge {
* @param string $name Name of the bridge.
* @return bool True if the bridge is whitelisted.
*/
public static function isWhitelisted($name){
return in_array(self::sanitizeBridgeName($name), self::getWhitelist());
public function isWhitelisted($name){
return in_array($this->sanitizeBridgeName($name), $this->getWhitelist());
}
/**
* Returns the whitelist.
*
* On first call this function reads the whitelist from {@see WHITELIST}.
* On first call this function reads the whitelist from {@see WHITELIST} if
* the file exists, {@see WHITELIST_DEFAULT} otherwise.
* * Each line in the file specifies one bridge on the whitelist.
* * An empty file disables all bridges.
* * If the file only only contains `*`, all bridges are whitelisted.
@@ -204,30 +142,32 @@ class Bridge {
*
* @return array Array of whitelisted bridges
*/
public static function getWhitelist() {
public function getWhitelist() {
static $firstCall = true; // Initialized on first call
if($firstCall) {
// Create initial whitelist or load from disk
if (!file_exists(WHITELIST) && !empty(self::$whitelist)) {
file_put_contents(WHITELIST, implode("\n", self::$whitelist));
} elseif(file_exists(WHITELIST)) {
if(file_exists(WHITELIST)) {
$contents = trim(file_get_contents(WHITELIST));
} elseif(file_exists(WHITELIST_DEFAULT)) {
$contents = trim(file_get_contents(WHITELIST_DEFAULT));
} else {
$contents = '';
}
if($contents === '*') { // Whitelist all bridges
self::$whitelist = self::getBridgeNames();
} else {
self::$whitelist = array_map('self::sanitizeBridgeName', explode("\n", $contents));
if($contents === '*') { // Whitelist all bridges
$this->whitelist = $this->getBridgeNames();
} else {
//$this->$whitelist = array_map('$this->sanitizeBridgeName', explode("\n", $contents));
foreach(explode("\n", $contents) as $bridgeName) {
$this->whitelist[] = $this->sanitizeBridgeName($bridgeName);
}
}
}
return self::$whitelist;
return $this->whitelist;
}
@@ -245,8 +185,8 @@ class Bridge {
* @param array $default The whitelist as array of bridge names.
* @return void
*/
public static function setWhitelist($default = array()) {
self::$whitelist = array_map('self::sanitizeBridgeName', $default);
public function setWhitelist($default = array()) {
$this->whitelist = array_map('$this->sanitizeBridgeName', $default);
}
/**
@@ -266,7 +206,7 @@ class Bridge {
* @return string|null The sanitized bridge name if the provided name is
* valid, null otherwise.
*/
protected static function sanitizeBridgeName($name) {
protected function sanitizeBridgeName($name) {
if(is_string($name)) {
@@ -280,10 +220,16 @@ class Bridge {
$name = $matches[1];
}
// Improve performance for correctly written bridge names
if(in_array($name, $this->getBridgeNames())) {
$index = array_search($name, $this->getBridgeNames());
return $this->getBridgeNames()[$index];
}
// The name is valid if a corresponding bridge file is found on disk
if(in_array(strtolower($name), array_map('strtolower', self::getBridgeNames()))) {
$index = array_search(strtolower($name), array_map('strtolower', self::getBridgeNames()));
return self::getBridgeNames()[$index];
if(in_array(strtolower($name), array_map('strtolower', $this->getBridgeNames()))) {
$index = array_search(strtolower($name), array_map('strtolower', $this->getBridgeNames()));
return $this->getBridgeNames()[$index];
}
Debug::log('Invalid bridge name specified: "' . $name . '"!');

View File

@@ -33,6 +33,7 @@ final class BridgeList {
<meta name="description" content="RSS-Bridge" />
<title>RSS-Bridge</title>
<link href="static/style.css" rel="stylesheet">
<link rel="icon" type="image/png" href="static/favicon.png">
<script src="static/search.js"></script>
<script src="static/select.js"></script>
<noscript>
@@ -61,14 +62,19 @@ EOD;
$totalActiveBridges = 0;
$inactiveBridges = '';
$bridgeList = Bridge::getBridgeNames();
$formats = Format::getFormatNames();
$bridgeFac = new \BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
$bridgeList = $bridgeFac->getBridgeNames();
$formatFac = new FormatFactory();
$formatFac->setWorkingDir(PATH_LIB_FORMATS);
$formats = $formatFac->getFormatNames();
$totalBridges = count($bridgeList);
foreach($bridgeList as $bridgeName) {
if(Bridge::isWhitelisted($bridgeName)) {
if($bridgeFac->isWhitelisted($bridgeName)) {
$body .= BridgeCard::displayBridgeCard($bridgeName, $formats);
$totalActiveBridges++;
@@ -111,8 +117,7 @@ EOD;
return <<<EOD
<header>
<h1>RSS-Bridge</h1>
<h2>Reconnecting the Web</h2>
<div class="logo"></div>
{$warning}
</header>
EOD;
@@ -130,7 +135,7 @@ EOD;
<section class="searchbar">
<h3>Search</h3>
<input type="text" name="searchfield"
id="searchfield" placeholder="Enter the bridge you want to search for"
id="searchfield" placeholder="Insert URL or bridge name"
onchange="search()" onkeyup="search()" value="{$query}">
</section>
EOD;

View File

@@ -31,29 +31,7 @@
* $cache = Cache::create('FileCache');
* ```
*/
class Cache {
/**
* Holds a path to the working directory.
*
* Do not access this property directly!
* Use {@see Cache::setWorkingDir()} and {@see Cache::getWorkingDir()} instead.
*
* @var string|null
*/
protected static $workingDir = null;
/**
* Throws an exception when trying to create a new instance of this class.
* Use {@see Cache::create()} to create a new cache object from the working
* directory.
*
* @throws \LogicException if called.
*/
public function __construct(){
throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create cache objects!');
}
class CacheFactory extends FactoryAbstract {
/**
* Creates a new cache object from the working directory.
*
@@ -63,14 +41,14 @@ class Cache {
* @param string $name Name of the cache object.
* @return object|bool The cache object or false if the class is not instantiable.
*/
public static function create($name){
$name = self::sanitizeCacheName($name) . 'Cache';
public function create($name){
$name = $this->sanitizeCacheName($name) . 'Cache';
if(!self::isCacheName($name)) {
if(!$this->isCacheName($name)) {
throw new \InvalidArgumentException('Cache name invalid!');
}
$filePath = self::getWorkingDir() . $name . '.php';
$filePath = $this->getWorkingDir() . $name . '.php';
if(!file_exists($filePath)) {
throw new \Exception('Cache file ' . $filePath . ' does not exist!');
@@ -85,48 +63,6 @@ class Cache {
return false;
}
/**
* Sets the working directory.
*
* @param string $dir Path to a directory containing cache classes
* @throws \InvalidArgumentException if $dir is not a string.
* @throws \Exception if the working directory doesn't exist.
* @throws \InvalidArgumentException if $dir is not a directory.
* @return void
*/
public static function setWorkingDir($dir){
self::$workingDir = null;
if(!is_string($dir)) {
throw new \InvalidArgumentException('Working directory is not a valid string!');
}
if(!file_exists($dir)) {
throw new \Exception('Working directory does not exist!');
}
if(!is_dir($dir)) {
throw new \InvalidArgumentException('Working directory is not a directory!');
}
self::$workingDir = realpath($dir) . '/';
}
/**
* Returns the working directory.
* The working directory must be set with {@see Cache::setWorkingDir()}!
*
* @throws \LogicException if the working directory is not set.
* @return string The current working directory.
*/
public static function getWorkingDir(){
if(is_null(self::$workingDir)) {
throw new \LogicException('Working directory is not set!');
}
return self::$workingDir;
}
/**
* Returns true if the provided name is a valid cache name.
*
@@ -136,7 +72,7 @@ class Cache {
* @param string $name The cache name.
* @return bool true if the name is a valid cache name, false otherwise.
*/
public static function isCacheName($name){
public function isCacheName($name){
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
@@ -147,12 +83,12 @@ class Cache {
*
* @return array List of cache names
*/
public static function getCacheNames(){
public function getCacheNames(){
static $cacheNames = array(); // Initialized on first call
if(empty($cacheNames)) {
$files = scandir(self::getWorkingDir());
$files = scandir($this->getWorkingDir());
if($files !== false) {
foreach($files as $file) {
@@ -183,7 +119,7 @@ class Cache {
* @return string|null The sanitized cache name if the provided name is
* valid, null otherwise.
*/
protected static function sanitizeCacheName($name) {
protected function sanitizeCacheName($name) {
if(is_string($name)) {
@@ -198,9 +134,9 @@ class Cache {
}
// The name is valid if a corresponding file is found on disk
if(in_array(strtolower($name), array_map('strtolower', self::getCacheNames()))) {
$index = array_search(strtolower($name), array_map('strtolower', self::getCacheNames()));
return self::getCacheNames()[$index];
if(in_array(strtolower($name), array_map('strtolower', $this->getCacheNames()))) {
$index = array_search(strtolower($name), array_map('strtolower', $this->getCacheNames()));
return $this->getCacheNames()[$index];
}
Debug::log('Invalid cache name specified: "' . $name . '"!');

View File

@@ -13,38 +13,54 @@
/**
* The cache interface
*
* @todo Add missing function to the interface
* @todo Explain parameters and return values in more detail
* @todo Return self more often (to allow call chaining)
*/
interface CacheInterface {
/**
* Set scope of the current cache
*
* If $scope is an empty string, the cache is set to a global context.
*
* @param string $scope The scope the data is related to
*/
public function setScope($scope);
/**
* Set key to assign the current data
*
* Since $key can be anything, the cache implementation must ensure to
* assign the related data reliably; most commonly by serializing and
* hashing the key in an appropriate way.
*
* @param array $key The key the data is related to
*/
public function setKey($key);
/**
* Loads data from cache
*
* @return mixed The cache data
* @return mixed The cached data or null
*/
public function loadData();
/**
* Stores data to the cache
*
* @param mixed $datas The data to store
* @param mixed $data The data to store
* @return self The cache object
*/
public function saveData($datas);
public function saveData($data);
/**
* Returns the timestamp for the curent cache file
* Returns the timestamp for the curent cache data
*
* @return int Timestamp
* @return int Timestamp or null
*/
public function getTime();
/**
* Removes any data that is older than the specified duration from cache
* Removes any data that is older than the specified age from cache
*
* @param int $duration The cache duration in seconds
* @param int $seconds The cache age in seconds
*/
public function purgeCache($duration);
public function purgeCache($seconds);
}

View File

@@ -28,7 +28,7 @@ final class Configuration {
*
* @todo Replace this property by a constant.
*/
public static $VERSION = '2019-03-17';
public static $VERSION = '2019-07-06';
/**
* Holds the configuration data.
@@ -80,35 +80,31 @@ final class Configuration {
// Check PHP version
if(version_compare(PHP_VERSION, '5.6.0') === -1)
die('RSS-Bridge requires at least PHP version 5.6.0!');
self::reportError('RSS-Bridge requires at least PHP version 5.6.0!');
// extensions check
if(!extension_loaded('openssl'))
die('"openssl" extension not loaded. Please check "php.ini"');
self::reportError('"openssl" extension not loaded. Please check "php.ini"');
if(!extension_loaded('libxml'))
die('"libxml" extension not loaded. Please check "php.ini"');
self::reportError('"libxml" extension not loaded. Please check "php.ini"');
if(!extension_loaded('mbstring'))
die('"mbstring" extension not loaded. Please check "php.ini"');
self::reportError('"mbstring" extension not loaded. Please check "php.ini"');
if(!extension_loaded('simplexml'))
die('"simplexml" extension not loaded. Please check "php.ini"');
self::reportError('"simplexml" extension not loaded. Please check "php.ini"');
// Allow RSS-Bridge to run without curl module in CLI mode without root certificates
if(!extension_loaded('curl') && !(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo'))))
die('"curl" extension not loaded. Please check "php.ini"');
self::reportError('"curl" extension not loaded. Please check "php.ini"');
if(!extension_loaded('json'))
die('"json" extension not loaded. Please check "php.ini"');
self::reportError('"json" extension not loaded. Please check "php.ini"');
// Check cache folder permissions (write permissions required)
if(!is_writable(PATH_CACHE))
die('RSS-Bridge does not have write permissions for ' . PATH_CACHE . '!');
// Check whitelist file permissions
if(!file_exists(WHITELIST) && !is_writable(dirname(WHITELIST)))
die('RSS-Bridge does not have write permissions for ' . WHITELIST . '!');
self::reportError('RSS-Bridge does not have write permissions for ' . PATH_CACHE . '!');
}
@@ -118,15 +114,13 @@ final class Configuration {
* Returns an error message and aborts execution if the configuration is invalid.
*
* The RSS-Bridge configuration is split into two files:
* - `config.default.ini.php`: The default configuration file that ships with
* every release of RSS-Bridge (do not modify this file!).
* - `config.ini.php`: The local configuration file that can be modified by
* server administrators.
* - {@see FILE_CONFIG_DEFAULT} The default configuration file that ships
* with every release of RSS-Bridge (do not modify this file!).
* - {@see FILE_CONFIG} The local configuration file that can be modified
* by server administrators.
*
* The files must be located at {@see PATH_ROOT}
*
* RSS-Bridge will first load `config.default.ini.php` into memory and then
* replace parameters with the contents of `config.ini.php`. That way new
* RSS-Bridge will first load {@see FILE_CONFIG_DEFAULT} into memory and then
* replace parameters with the contents of {@see FILE_CONFIG}. That way new
* parameters are automatically initialized with default values and custom
* configurations can be reduced to the minimum set of parametes necessary
* (only the ones that changed).
@@ -140,16 +134,16 @@ final class Configuration {
*/
public static function loadConfiguration() {
if(!file_exists(PATH_ROOT . 'config.default.ini.php'))
die('The default configuration file "config.default.ini.php" is missing!');
if(!file_exists(FILE_CONFIG_DEFAULT))
self::reportError('The default configuration file is missing at ' . FILE_CONFIG_DEFAULT);
Configuration::$config = parse_ini_file(PATH_ROOT . 'config.default.ini.php', true, INI_SCANNER_TYPED);
Configuration::$config = parse_ini_file(FILE_CONFIG_DEFAULT, true, INI_SCANNER_TYPED);
if(!Configuration::$config)
die('Error parsing config.default.ini.php');
self::reportError('Error parsing ' . FILE_CONFIG_DEFAULT);
if(file_exists(PATH_ROOT . 'config.ini.php')) {
if(file_exists(FILE_CONFIG)) {
// Replace default configuration with custom settings
foreach(parse_ini_file(PATH_ROOT . 'config.ini.php', true, INI_SCANNER_TYPED) as $header => $section) {
foreach(parse_ini_file(FILE_CONFIG, true, INI_SCANNER_TYPED) as $header => $section) {
foreach($section as $key => $value) {
// Skip unknown sections and keys
if(array_key_exists($header, Configuration::$config) && array_key_exists($key, Configuration::$config[$header])) {
@@ -159,8 +153,14 @@ final class Configuration {
}
}
if(!is_string(self::getConfig('system', 'timezone'))
|| !in_array(self::getConfig('system', 'timezone'), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
self::reportConfigurationError('system', 'timezone');
date_default_timezone_set(self::getConfig('system', 'timezone'));
if(!is_string(self::getConfig('proxy', 'url')))
die('Parameter [proxy] => "url" is not a valid string! Please check "config.ini.php"!');
self::reportConfigurationError('proxy', 'url', 'Is not a valid string');
if(!empty(self::getConfig('proxy', 'url'))) {
/** URL of the proxy server */
@@ -168,38 +168,38 @@ final class Configuration {
}
if(!is_bool(self::getConfig('proxy', 'by_bridge')))
die('Parameter [proxy] => "by_bridge" is not a valid Boolean! Please check "config.ini.php"!');
self::reportConfigurationError('proxy', 'by_bridge', 'Is not a valid Boolean');
/** True if proxy usage can be enabled selectively for each bridge */
define('PROXY_BYBRIDGE', self::getConfig('proxy', 'by_bridge'));
if(!is_string(self::getConfig('proxy', 'name')))
die('Parameter [proxy] => "name" is not a valid string! Please check "config.ini.php"!');
self::reportConfigurationError('proxy', 'name', 'Is not a valid string');
/** Name of the proxy server */
define('PROXY_NAME', self::getConfig('proxy', 'name'));
if(!is_string(self::getConfig('cache', 'type')))
die('Parameter [cache] => "type" is not a valid string! Please check "config.ini.php"!');
self::reportConfigurationError('cache', 'type', 'Is not a valid string');
if(!is_bool(self::getConfig('cache', 'custom_timeout')))
die('Parameter [cache] => "custom_timeout" is not a valid Boolean! Please check "config.ini.php"!');
self::reportConfigurationError('cache', 'custom_timeout', 'Is not a valid Boolean');
/** True if the cache timeout can be specified by the user */
define('CUSTOM_CACHE_TIMEOUT', self::getConfig('cache', 'custom_timeout'));
if(!is_bool(self::getConfig('authentication', 'enable')))
die('Parameter [authentication] => "enable" is not a valid Boolean! Please check "config.ini.php"!');
self::reportConfigurationError('authentication', 'enable', 'Is not a valid Boolean');
if(!is_string(self::getConfig('authentication', 'username')))
die('Parameter [authentication] => "username" is not a valid string! Please check "config.ini.php"!');
self::reportConfigurationError('authentication', 'username', 'Is not a valid string');
if(!is_string(self::getConfig('authentication', 'password')))
die('Parameter [authentication] => "password" is not a valid string! Please check "config.ini.php"!');
self::reportConfigurationError('authentication', 'password', 'Is not a valid string');
if(!empty(self::getConfig('admin', 'email'))
&& !filter_var(self::getConfig('admin', 'email'), FILTER_VALIDATE_EMAIL))
die('Parameter [admin] => "email" is not a valid email address! Please check "config.ini.php"!');
self::reportConfigurationError('admin', 'email', 'Is not a valid email address');
}
@@ -246,4 +246,46 @@ final class Configuration {
return Configuration::$VERSION;
}
/**
* Reports an configuration error for the specified section and key to the
* user and ends execution
*
* @param string $section The section name
* @param string $key The configuration key
* @param string $message An optional message to the user
*
* @return void
*/
private static function reportConfigurationError($section, $key, $message = '') {
$report = "Parameter [{$section}] => \"{$key}\" is invalid!" . PHP_EOL;
if(file_exists(FILE_CONFIG)) {
$report .= 'Please check your configuration file at ' . FILE_CONFIG . PHP_EOL;
} elseif(!file_exists(FILE_CONFIG_DEFAULT)) {
$report .= 'The default configuration file is missing at ' . FILE_CONFIG_DEFAULT . PHP_EOL;
} else {
$report .= 'The default configuration file is broken.' . PHP_EOL
. 'Restore the original file from ' . REPOSITORY . PHP_EOL;
}
$report .= $message;
self::reportError($report);
}
/**
* Reports an error message to the user and ends execution
*
* @param string $message The error message
*
* @return void
*/
private static function reportError($message) {
header('Content-Type: text/plain', true, 500);
die('Configuration error' . PHP_EOL . $message);
}
}

View File

@@ -11,6 +11,15 @@
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Builds a GitHub search query to find open bugs for the current bridge
*/
function buildGitHubSearchQuery($bridgeName){
return REPOSITORY
. 'issues?q='
. urlencode('is:issue is:open ' . $bridgeName);
}
/**
* Returns an URL that automatically populates a new issue on GitHub based
* on the information provided
@@ -83,7 +92,8 @@ function buildBridgeException($e, $bridge){
. '`';
$body_html = nl2br($body);
$link = buildGitHubIssueQuery($title, $body, 'bug report', $bridge->getMaintainer());
$link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer());
$searchQuery = buildGitHubSearchQuery($bridge::NAME);
$header = buildHeader($e, $bridge);
$message = <<<EOD
@@ -91,7 +101,7 @@ function buildBridgeException($e, $bridge){
remote website's content!<br>
{$body_html}
EOD;
$section = buildSection($e, $bridge, $message, $link);
$section = buildSection($e, $bridge, $message, $link, $searchQuery);
return $section;
}
@@ -119,11 +129,12 @@ function buildTransformException($e, $bridge){
. (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '')
. '`';
$link = buildGitHubIssueQuery($title, $body, 'bug report', $bridge->getMaintainer());
$link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer());
$searchQuery = buildGitHubSearchQuery($bridge::NAME);
$header = buildHeader($e, $bridge);
$message = "RSS-Bridge was unable to transform the contents returned by
<strong>{$bridge->getName()}</strong>!";
$section = buildSection($e, $bridge, $message, $link);
$section = buildSection($e, $bridge, $message, $link, $searchQuery);
return buildPage($title, $header, $section);
}
@@ -154,11 +165,12 @@ EOD;
* @param object $bridge The bridge object
* @param string $message The message to display
* @param string $link The link to include in the anchor
* @param string $searchQuery A GitHub search query for the current bridge
* @return string The HTML section
*
* @todo This function belongs inside a class
*/
function buildSection($e, $bridge, $message, $link){
function buildSection($e, $bridge, $message, $link, $searchQuery){
return <<<EOD
<section>
<p class="exception-message">{$message}</p>
@@ -166,9 +178,13 @@ function buildSection($e, $bridge, $message, $link){
<ul class="advice">
<li>Press Return to check your input parameters</li>
<li>Press F5 to retry</li>
<li>Check if this issue was already reported on <a href="{$searchQuery}">GitHub</a> (give it a thumbs-up)</li>
<li>Open a <a href="{$link}">GitHub Issue</a> if this error persists</li>
</ul>
</div>
<a href="{$searchQuery}" title="Opens GitHub to search for similar issues">
<button>Search GitHub Issues</button>
</a>
<a href="{$link}" title="After clicking this button you can review
the issue before submitting it"><button>Open GitHub Issue</button></a>
<p class="maintainer">{$bridge->getMaintainer()}</p>

View File

@@ -76,7 +76,7 @@ class FeedItem {
* $item['uri'] = 'https://www.github.com/rss-bridge/rss-bridge/';
* $item['title'] = 'Title';
* $item['timestamp'] = strtotime('now');
* $item['autor'] = 'Unknown author';
* $item['author'] = 'Unknown author';
* $item['content'] = 'Hello World!';
* $item['enclosures'] = array('https://github.com/favicon.ico');
* $item['categories'] = array('php', 'rss-bridge', 'awesome');
@@ -347,7 +347,7 @@ class FeedItem {
$enclosure,
FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) {
Debug::log('Each enclosure must contain a scheme, host and path!');
} else {
} elseif(!in_array($enclosure, $this->enclosures)) {
$this->enclosures[] = $enclosure;
}
}
@@ -418,6 +418,9 @@ class FeedItem {
if(!is_string($uid)) {
Debug::log('Unique id must be a string!');
} elseif (preg_match('/^[a-f0-9]{40}$/', $uid)) {
// keep id if it already is a SHA-1 hash
$this->uid = $uid;
} else {
$this->uid = sha1($uid);
}

View File

@@ -31,29 +31,7 @@
* $format = Format::create('Atom');
* ```
*/
class Format {
/**
* Holds a path to the working directory.
*
* Do not access this property directly!
* Use {@see Format::setWorkingDir()} and {@see Format::getWorkingDir()} instead.
*
* @var string|null
*/
protected static $workingDir = null;
/**
* Throws an exception when trying to create a new instance of this class.
* Use {@see Format::create()} to create a new format object from the working
* directory.
*
* @throws \LogicException if called.
*/
public function __construct(){
throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create cache objects!');
}
class FormatFactory extends FactoryAbstract {
/**
* Creates a new format object from the working directory.
*
@@ -63,13 +41,13 @@ class Format {
* @param string $name Name of the format object.
* @return object|bool The format object or false if the class is not instantiable.
*/
public static function create($name){
if(!self::isFormatName($name)) {
public function create($name){
if(!$this->isFormatName($name)) {
throw new \InvalidArgumentException('Format name invalid!');
}
$name = $name . 'Format';
$pathFormat = self::getWorkingDir() . $name . '.php';
$name = $this->sanitizeFormatName($name) . 'Format';
$pathFormat = $this->getWorkingDir() . $name . '.php';
if(!file_exists($pathFormat)) {
throw new \Exception('Format file ' . $filePath . ' does not exist!');
@@ -84,48 +62,6 @@ class Format {
return false;
}
/**
* Sets the working directory.
*
* @param string $dir Path to a directory containing cache classes
* @throws \InvalidArgumentException if $dir is not a string.
* @throws \Exception if the working directory doesn't exist.
* @throws \InvalidArgumentException if $dir is not a directory.
* @return void
*/
public static function setWorkingDir($dir){
self::$workingDir = null;
if(!is_string($dir)) {
throw new \InvalidArgumentException('Dir format must be a string.');
}
if(!file_exists($dir)) {
throw new \Exception('Working directory does not exist!');
}
if(!is_dir($dir)) {
throw new \InvalidArgumentException('Working directory is not a directory!');
}
self::$workingDir = realpath($dir) . '/';
}
/**
* Returns the working directory.
* The working directory must be set with {@see Format::setWorkingDir()}!
*
* @throws \LogicException if the working directory is not set.
* @return string The current working directory.
*/
public static function getWorkingDir(){
if(is_null(self::$workingDir)) {
throw new \LogicException('Working directory is not set!');
}
return self::$workingDir;
}
/**
* Returns true if the provided name is a valid format name.
*
@@ -135,7 +71,7 @@ class Format {
* @param string $name The format name.
* @return bool true if the name is a valid format name, false otherwise.
*/
public static function isFormatName($name){
public function isFormatName($name){
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
@@ -146,11 +82,11 @@ class Format {
*
* @return array List of format names
*/
public static function getFormatNames(){
public function getFormatNames(){
static $formatNames = array(); // Initialized on first call
if(empty($formatNames)) {
$files = scandir(self::getWorkingDir());
$files = scandir($this->getWorkingDir());
if($files !== false) {
foreach($files as $file) {
@@ -163,4 +99,55 @@ class Format {
return $formatNames;
}
/**
* Returns the sanitized format name.
*
* The format name can be specified in various ways:
* * The PHP file name (i.e. `AtomFormat.php`)
* * The PHP file name without file extension (i.e. `AtomFormat`)
* * The format name (i.e. `Atom`)
*
* Casing is ignored (i.e. `ATOM` and `atom` are the same).
*
* A format file matching the given format name must exist in the working
* directory!
*
* @param string $name The format name
* @return string|null The sanitized format name if the provided name is
* valid, null otherwise.
*/
protected function sanitizeFormatName($name) {
if(is_string($name)) {
// Trim trailing '.php' if exists
if(preg_match('/(.+)(?:\.php)/', $name, $matches)) {
$name = $matches[1];
}
// Trim trailing 'Format' if exists
if(preg_match('/(.+)(?:Format)/i', $name, $matches)) {
$name = $matches[1];
}
// Improve performance for correctly written format names
if(in_array($name, $this->getFormatNames())) {
$index = array_search($name, $this->getFormatNames());
return $this->getFormatNames()[$index];
}
// The name is valid if a corresponding format file is found on disk
if(in_array(strtolower($name), array_map('strtolower', $this->getFormatNames()))) {
$index = array_search(strtolower($name), array_map('strtolower', $this->getFormatNames()));
return $this->getFormatNames()[$index];
}
Debug::log('Invalid format name: "' . $name . '"!');
}
return null; // Bad parameter
}
}

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