1
0
mirror of https://github.com/RSS-Bridge/rss-bridge.git synced 2025-08-25 01:16:07 +02:00

Compare commits

..

114 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
93 changed files with 4696 additions and 2116 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.

3
.gitignore vendored
View File

@@ -240,3 +240,6 @@ config.ini.php
#Auth
.htaccess
.htpasswd
#Crawler
robots.txt

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/

213
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,107 +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)
* [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)
* [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)
* [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)
* [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,7 +82,9 @@ class DisplayAction extends ActionAbstract {
);
// Initialize cache
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$cacheFac = new CacheFactory();
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope('');
$cache->purgeCache(86400); // 24 hours
$cache->setKey($cache_params);
@@ -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"]
}

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

@@ -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'));

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();
}
}

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

@@ -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

@@ -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,7 +120,9 @@ class ElloBridge extends BridgeAbstract {
}
private function getAPIKey() {
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$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();

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];

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('.lst-pic img', 0)->getAttribute('src');
$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'] = parent::getURI() . $annonce->find('.lst-ctrls a', 0)->href;
$content = $annonce->find('.lst-dtl', 0)->innertext;
$item['content'] = "<img src='$img' /><br>$content";
$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

@@ -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

@@ -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;
}
}
}

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

@@ -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;
}
}

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,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

@@ -12,11 +12,12 @@ class RadioMelodieBridge extends BridgeAbstract {
public function collectData(){
$html = getSimpleHTMLDOM(self::URI . '/actu/')
or returnServerError('Could not request Radio Melodie.');
$list = $html->find('div[class=actu_col1]', 0)->children();;
$list = $html->find('div[class=displayList]', 0)->children();
foreach($list as $element) {
if($element->tag == 'a') {
$articleURL = self::URI . $element->href;
$article = getSimpleHTMLDOM($articleURL);
$textDOM = $article->find('article', 0);
// Initialise arrays
$item = array();
@@ -24,52 +25,50 @@ class RadioMelodieBridge extends BridgeAbstract {
$picture = array();
// Get the Main picture URL
$picture[] = $this->rewriteImage($article->find('img[id=picturearticle]', 0)->src);
$audioHTML = $article->find('div[class=sm2-playlist-wrapper]');
$picture[] = $this->rewriteImage($article->find('div[id=pictureTitleSupport]', 0)->find('img', 0)->src);
$audioHTML = $article->find('audio');
// Remove the audio placeholder under the Audio player with an <audio>
// element and add the audio element to the enclosure
// Add the audio element to the enclosure
foreach($audioHTML as $audioElement) {
$audioURL = $audioElement->find('a', 0)->href;
$audioURL = $audioElement->src;
$audio[] = $audioURL;
$audioElement->outertext = '<audio controls src="' . $audioURL . '"></audio>';
$article->save();
}
// Rewrite pictures URL
$imgs = $article->find('img[src^="https://www.radiomelodie.com/image.php]');
$imgs = $textDOM->find('img[src^="http://www.radiomelodie.com/image.php]');
foreach($imgs as $img) {
$img->src = $this->rewriteImage($img->src);
$article->save();
}
// Remove inline audio player HTML
$inlinePlayers = $article->find('div[class*=sm2-main-controls]');
foreach($inlinePlayers as $inlinePlayer) {
$inlinePlayer->outertext = '';
$article->save();
}
// Remove Google Ads
$ads = $article->find('div[style^=margin:25px 0; position:relative; height:auto;]');
$ads = $article->find('div[class=adInline]');
foreach($ads as $ad) {
$ad->outertext = '';
$article->save();
}
$author = $article->find('div[id=author]', 0)->find('span', 0)->plaintext;
// 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_category = $article->find('div[class*=date]', 0)->plaintext;
$header = $article->find('a[class=fancybox]', 0)->innertext;
$textDOM = $article->find('div[class=text_content]', 0);
$textDOM->find('div[id=author]', 0)->outertext = '';
$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_category . $header . $text;
$item['content'] = '<h1>' . $item['title'] . '</h1>' . $date . '<br/>' . $header . $text;
$this->items[] = $item;
}
}
@@ -81,7 +80,7 @@ class RadioMelodieBridge extends BridgeAbstract {
private function rewriteImage($url)
{
$parts = explode('?', $url);
parse_str($parts[1], $params);
parse_str(html_entity_decode($parts[1]), $params);
return self::URI . '/' . $params['image'];
}

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;

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();
}
}

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

@@ -52,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);

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(){
@@ -229,7 +246,7 @@ class YoutubeBridge extends BridgeAbstract {
$url_listing = self::URI . 'playlist?list=' . urlencode($this->request);
$html = $this->ytGetSimpleHTMLDOM($url_listing)
or returnServerError("Could not request YouTube. Tried:\n - $url_listing");
$item_count = $this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a', true);
$item_count = $this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a', false);
if ($item_count <= 15 && !$this->skipFeeds() && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) {
$this->ytBridgeParseXmlFeed($xml);
} else {

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

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

View File

@@ -16,19 +16,19 @@ class MemcachedCache implements CacheInterface {
$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 config.ini.php');
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 config.ini.php');
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 config.ini.php');
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 config.ini.php');
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 config.ini.php');
returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
}
$conn = new Memcached();

View File

@@ -15,12 +15,12 @@ class SQLiteCache implements CacheInterface {
$file = Configuration::getConfig(get_called_class(), 'file');
if (empty($file)) {
die('Configuration for ' . get_called_class() . ' missing. Please check your config.ini.php');
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 config.ini.php');
die('Invalid configuration for ' . get_called_class() . '. Please check your ' . FILE_CONFIG);
}
if (!is_file($file)) {

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

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,27 +27,8 @@ define('USER_AGENT',
ini_set('user_agent', USER_AGENT);
// default whitelist
$whitelist_default = array(
'BandcampBridge',
'CryptomeBridge',
'DansTonChatBridge',
'DuckDuckGoBridge',
'FacebookBridge',
'FlickrBridge',
'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

@@ -28,7 +28,7 @@ final class Configuration {
*
* @todo Replace this property by a constant.
*/
public static $VERSION = 'dev.2019-05-08';
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

@@ -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
}
}

View File

@@ -214,6 +214,7 @@ class ParameterValidator {
switch(array_sum($queriedContexts)) {
case 0: // Found no match, is there a context without parameters?
if(isset($data['context'])) return $data['context'];
foreach($queriedContexts as $context => $queried) {
if(is_null($queried)) {
return $context;

View File

@@ -45,7 +45,9 @@ function getContents($url, $header = array(), $opts = array()){
Debug::log('Reading contents from "' . $url . '"');
// Initialize cache
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$cacheFac = new CacheFactory();
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope('server');
$cache->purgeCache(86400); // 24 hours (forced)
@@ -270,7 +272,9 @@ function getSimpleHTMLDOMCached($url,
Debug::log('Caching url ' . $url . ', duration ' . $duration);
// Initialize cache
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$cacheFac = new CacheFactory();
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope('pages');
$cache->purgeCache(86400); // 24 hours (forced)
@@ -322,8 +326,8 @@ function parseResponseHeader($header) {
$header['http_code'] = $line;
} else {
list ($key, $value) = explode(': ', $line);
$header[$key] = $value;
list ($key, $value) = explode(':', $line);
$header[$key] = trim($value);
}

View File

@@ -32,18 +32,7 @@ function sanitize($html,
$htmlContent = str_get_html($html);
/*
* Notice: simple_html_dom currently doesn't support "->find(*)", which is a
* known issue: https://sourceforge.net/p/simplehtmldom/bugs/157/
*
* A solution to this is to find all nodes WITHOUT a specific attribute. If
* the attribute is very unlikely to appear in the DOM, this is essentially
* returning all nodes.
*
* "*[!b38fd2b1fe7f4747d6b1c1254ccd055e]" is doing exactly that. The attrib
* "b38fd2b1fe7f4747d6b1c1254ccd055e" is very unlikely to appear in any DOM.
*/
foreach($htmlContent->find('*[!b38fd2b1fe7f4747d6b1c1254ccd055e]') as $element) {
foreach($htmlContent->find('*') as $element) {
if(in_array($element->tag, $text_to_keep)) {
$element->outertext = $element->plaintext;
} elseif(in_array($element->tag, $tags_to_remove)) {
@@ -90,18 +79,7 @@ function backgroundToImg($htmlContent) {
$regex = '/background-image[ ]{0,}:[ ]{0,}url\([\'"]{0,}(.*?)[\'"]{0,}\)/';
$htmlContent = str_get_html($htmlContent);
/*
* Notice: simple_html_dom currently doesn't support "->find(*)", which is a
* known issue: https://sourceforge.net/p/simplehtmldom/bugs/157/
*
* A solution to this is to find all nodes WITHOUT a specific attribute. If
* the attribute is very unlikely to appear in the DOM, this is essentially
* returning all nodes.
*
* "*[!b38fd2b1fe7f4747d6b1c1254ccd055e]" is doing exactly that. The attrib
* "b38fd2b1fe7f4747d6b1c1254ccd055e" is very unlikely to appear in any DOM.
*/
foreach($htmlContent->find('*[!b38fd2b1fe7f4747d6b1c1254ccd055e]') as $element) {
foreach($htmlContent->find('*') as $element) {
if(preg_match($regex, $element->style, $matches) > 0) {

View File

@@ -15,28 +15,37 @@
define('PATH_ROOT', __DIR__ . '/../');
/** Path to the core library */
define('PATH_LIB', __DIR__ . '/../lib/'); // Path to core library
define('PATH_LIB', PATH_ROOT . 'lib/');
/** Path to the vendor library */
define('PATH_LIB_VENDOR', __DIR__ . '/../vendor/');
define('PATH_LIB_VENDOR', PATH_ROOT . 'vendor/');
/** Path to the bridges library */
define('PATH_LIB_BRIDGES', __DIR__ . '/../bridges/');
define('PATH_LIB_BRIDGES', PATH_ROOT . 'bridges/');
/** Path to the formats library */
define('PATH_LIB_FORMATS', __DIR__ . '/../formats/');
define('PATH_LIB_FORMATS', PATH_ROOT . 'formats/');
/** Path to the caches library */
define('PATH_LIB_CACHES', __DIR__ . '/../caches/');
define('PATH_LIB_CACHES', PATH_ROOT . 'caches/');
/** Path to the actions library */
define('PATH_LIB_ACTIONS', __DIR__ . '/../actions/');
define('PATH_LIB_ACTIONS', PATH_ROOT . 'actions/');
/** Path to the cache folder */
define('PATH_CACHE', __DIR__ . '/../cache/');
define('PATH_CACHE', PATH_ROOT . 'cache/');
/** Path to the whitelist file */
define('WHITELIST', __DIR__ . '/../whitelist.txt');
define('WHITELIST', PATH_ROOT . 'whitelist.txt');
/** Path to the default whitelist file */
define('WHITELIST_DEFAULT', PATH_ROOT . 'whitelist.default.txt');
/** Path to the configuration file */
define('FILE_CONFIG', PATH_ROOT . 'config.ini.php');
/** Path to the default configuration file */
define('FILE_CONFIG_DEFAULT', PATH_ROOT . 'config.default.ini.php');
/** URL to the RSS-Bridge repository */
define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/');
@@ -52,12 +61,12 @@ require_once PATH_LIB . 'FactoryAbstract.php';
require_once PATH_LIB . 'FeedItem.php';
require_once PATH_LIB . 'Debug.php';
require_once PATH_LIB . 'Exceptions.php';
require_once PATH_LIB . 'Format.php';
require_once PATH_LIB . 'FormatFactory.php';
require_once PATH_LIB . 'FormatAbstract.php';
require_once PATH_LIB . 'Bridge.php';
require_once PATH_LIB . 'BridgeFactory.php';
require_once PATH_LIB . 'BridgeAbstract.php';
require_once PATH_LIB . 'FeedExpander.php';
require_once PATH_LIB . 'Cache.php';
require_once PATH_LIB . 'CacheFactory.php';
require_once PATH_LIB . 'Authentication.php';
require_once PATH_LIB . 'Configuration.php';
require_once PATH_LIB . 'BridgeCard.php';
@@ -75,14 +84,3 @@ require_once PATH_LIB . 'contents.php';
define('MAX_FILE_SIZE', 10000000); /* Allow larger files for simple_html_dom */
require_once PATH_LIB_VENDOR . 'simplehtmldom/simple_html_dom.php';
require_once PATH_LIB_VENDOR . 'php-urljoin/src/urljoin.php';
// Initialize static members
try {
Bridge::setWorkingDir(PATH_LIB_BRIDGES);
Format::setWorkingDir(PATH_LIB_FORMATS);
Cache::setWorkingDir(PATH_LIB_CACHES);
} catch(Exception $e) {
error_log($e);
header('Content-type: text/plain', true, 500);
die($e->getMessage());
}

View File

@@ -96,3 +96,20 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
button:hover {
background: #49afff;
}
@media screen and (max-width: 767px) {
section {
width: 100%;
padding: 0;
}
button {
display: inline-block;
width: 40%;
padding: 5px auto;
margin: 3px auto 0;
}
}

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

122
static/favicon.svg Normal file
View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="200mm"
height="200mm"
viewBox="0 0 200 200"
version="1.1"
id="svg871"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="favicon_rssbridge.svg">
<defs
id="defs865" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="495.71429"
inkscape:cy="542.85714"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata868">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-97)">
<g
id="g2147"
transform="matrix(1.7746042,0,0,1.7746042,3145.263,-3080.4079)"
inkscape:export-xdpi="61.620663"
inkscape:export-ydpi="61.620663">
<rect
inkscape:export-ydpi="68"
inkscape:export-xdpi="68"
ry="17.993027"
rx="17.993027"
y="1803.3181"
x="-1759.36"
height="86.856956"
width="86.856956"
id="rect2098"
style="opacity:1;vector-effect:none;fill:#ff6600;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4.00000008, 8.00000016;stroke-dashoffset:0;stroke-opacity:1" />
<flowRoot
inkscape:export-filename="C:\Users\Gyrev\Dropbox\4 - Obs\01 - Com - media\03 - infographies\logo_glasses.png"
transform="matrix(0.5968306,0,0,0.5968306,-1834.59,1688.6939)"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.61466217"
id="flowRoot2106"
xml:space="preserve"
inkscape:export-xdpi="68.183243"
inkscape:export-ydpi="68.183243"><flowRegion
id="flowRegion2102"
style="fill:#ffffff;fill-opacity:1;stroke-width:2.61466217"><rect
y="275.93942"
x="140.55341"
height="65.626038"
width="253.20284"
id="rect2100"
style="fill:#ffffff;fill-opacity:1;stroke-width:2.61466217" /></flowRegion><flowPara
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2 Semi-Bold';letter-spacing:-1px;fill:#ffffff;fill-opacity:1;stroke-width:2.61466217"
id="flowPara2104">Bridge</flowPara></flowRoot> <g
style="stroke-width:2.99397564"
inkscape:export-ydpi="68"
inkscape:export-xdpi="68"
id="g2118"
transform="matrix(0.33400405,0,0,0.33400405,-1609.4253,1569.2886)">
<flowRoot
inkscape:export-filename="C:\Users\Gyrev\Dropbox\4 - Obs\01 - Com - media\03 - infographies\logo_glasses.png"
transform="matrix(1.7868963,0,0,1.7868963,-620.90965,302.19806)"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.61466217"
id="flowRoot2116"
xml:space="preserve"
inkscape:export-xdpi="68.183243"
inkscape:export-ydpi="68.183243"><flowRegion
id="flowRegion2112"
style="fill:#ffffff;fill-opacity:1;stroke-width:2.61466217"><rect
y="275.93942"
x="140.55341"
height="65.626038"
width="253.20284"
id="rect2110"
style="fill:#ffffff;fill-opacity:1;stroke-width:2.61466217" /></flowRegion><flowPara
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2 Semi-Bold';letter-spacing:-1px;fill:#ffffff;fill-opacity:1;stroke-width:2.61466217"
id="flowPara2114">rss</flowPara></flowRoot> </g>
<path
inkscape:export-ydpi="68"
inkscape:export-xdpi="68"
inkscape:connector-curvature="0"
id="path2120"
d="m -1719.2669,1838.3599 c -5.0205,-1.3144 -8.8786,-5.3846 -10.0117,-10.5624 -0.335,-1.5309 -0.2541,-4.567 0.1641,-6.161 1.284,-4.8933 4.9001,-8.573 9.7609,-9.9326 1.5509,-0.4338 4.2673,-0.512 6.1345,-0.1767 4.1633,0.7477 8.2284,4.1132 9.8663,8.1681 0.7975,1.9744 0.9807,2.9793 0.9762,5.3548 0,1.8242 -0.075,2.499 -0.3818,3.58 -1.3733,4.8468 -5.1589,8.5581 -9.9346,9.7395 -1.8168,0.4494 -4.837,0.445 -6.5739,-0.01 z m 2.2445,-7.5051 c -0.2717,-0.1738 -3.5329,-0.3553 -3.6195,-0.2014 -0.1223,0.2172 1.1312,3.2066 1.6601,3.9591 0.2581,0.3672 0.8046,0.9237 1.2144,1.2364 l 0.745,0.5688 0.083,-2.7282 c 0.053,-1.7295 0.022,-2.7672 -0.083,-2.8347 z m 3.4669,4.5392 c 0.6573,-0.6108 1.0277,-1.2558 1.7728,-3.087 0.3973,-0.9763 0.5822,-1.631 0.4789,-1.6949 -0.1723,-0.1065 -2.753,0.054 -3.429,0.2138 -0.3689,0.087 -0.3747,0.1285 -0.3747,2.7067 0,1.4401 0.05,2.6687 0.1116,2.73 0.1328,0.1328 0.7879,-0.2623 1.4404,-0.8686 z m -7.1654,-0.2901 c 0.057,-0.057 -0.07,-0.4503 -0.2823,-0.8742 -0.5012,-1.0013 -0.9314,-2.3267 -1.0704,-3.2976 l -0.1107,-0.7741 -1.0768,-0.2197 c -0.5923,-0.1208 -1.5232,-0.3704 -2.0688,-0.5546 -0.5455,-0.1842 -1.0215,-0.3053 -1.0578,-0.269 -0.1007,0.1007 0.5958,1.4049 1.2973,2.4294 0.7668,1.1198 1.9378,2.2559 3.1536,3.0595 0.9557,0.6316 1.047,0.6692 1.2159,0.5003 z m 11.6482,-1.1869 c 0.4342,-0.3121 1.1176,-0.9425 1.5185,-1.4009 0.7228,-0.8263 2.2126,-3.2507 2.0793,-3.3839 -0.081,-0.081 -3.4631,0.9745 -3.9405,1.23 -0.1894,0.1013 -0.3734,0.436 -0.4412,0.8027 -0.064,0.346 -0.4063,1.3351 -0.7607,2.1982 -0.3543,0.863 -0.6045,1.6335 -0.5558,1.7122 0.096,0.1552 0.8564,-0.2642 2.1004,-1.1583 z m -7.7321,-7.5308 c 0.028,-1.5854 -0.012,-2.9457 -0.089,-3.0227 -0.077,-0.077 -1.1425,-0.1909 -2.3677,-0.2529 l -2.2276,-0.1128 0.1048,2.6632 c 0.058,1.4647 0.1506,2.7824 0.2066,2.9281 0.068,0.1771 0.4702,0.3241 1.2128,0.4429 1.0818,0.1732 2.6072,0.3083 2.9427,0.2606 0.1015,-0.014 0.1865,-1.1506 0.2176,-2.9064 z m 4.7233,2.6386 c 0.6339,-0.097 1.1476,-0.2656 1.1959,-0.3916 0.047,-0.1215 0.1731,-0.9804 0.281,-1.9087 0.1853,-1.5935 0.1233,-3.2044 -0.1388,-3.6068 -0.084,-0.1292 -0.7256,-0.1333 -2.241,-0.014 l -2.1231,0.1669 v 2.9435 c 0,1.6188 0.039,2.9822 0.086,3.0297 0.1077,0.1077 1.4775,0.01 2.9397,-0.2189 z m -10.6043,-0.8169 c -0.1446,-0.4559 -0.1658,-4.6514 -0.026,-5.08 0.1024,-0.3129 -0.039,-0.3779 -1.7622,-0.8077 -1.0305,-0.2571 -1.9525,-0.4987 -2.0489,-0.5369 -0.2228,-0.088 -0.7405,2.5619 -0.7405,3.7909 0,1.3374 0.5346,1.8093 2.914,2.572 1.5268,0.4894 1.8022,0.4996 1.6632,0.062 z m 15.5636,-0.1064 c 1.2002,-0.3876 2.3124,-1.419 2.5141,-2.3313 0.1656,-0.7493 0.01,-1.8941 -0.4444,-3.2707 -0.2107,-0.6382 -0.3209,-0.7776 -0.5391,-0.6817 -0.1509,0.066 -1.0236,0.3247 -1.9395,0.5742 -1.1985,0.3265 -1.6453,0.5175 -1.5943,0.6816 0.1128,0.3635 0.051,4.4177 -0.076,4.9964 l -0.1154,0.5253 0.6589,-0.1053 c 0.3623,-0.058 1.0533,-0.2328 1.5355,-0.3885 z m -9.7252,-6.5935 c 0.045,-0.1165 0.061,-1.9335 0.036,-4.038 l -0.045,-3.8262 -1.1336,1.0763 c -0.8596,0.8162 -1.2919,1.3997 -1.7889,2.4145 -0.6761,1.3806 -1.4646,4.0002 -1.2644,4.2005 0.3609,0.3609 4.0646,0.5135 4.1953,0.1729 z m 5.1134,0.062 c 0.4359,-0.072 0.84,-0.1778 0.8978,-0.2357 0.1638,-0.1638 -0.9127,-3.4126 -1.4982,-4.5217 -0.5028,-0.9524 -2.3179,-3.085 -2.6256,-3.085 -0.1694,0 -0.205,7.7131 -0.036,7.8817 0.1489,0.1489 2.2742,0.1233 3.2624,-0.039 z m -10.6939,-1.057 c 0.4232,-1.6148 0.7365,-2.617 1.2494,-3.9963 0.2979,-0.8014 0.5037,-1.457 0.4573,-1.457 -0.3843,0 -2.3317,1.2786 -3.2089,2.1068 -1.2568,1.1867 -2.3208,2.8017 -2.0344,3.0881 0.1665,0.1666 2.6682,0.9242 3.1493,0.9537 0.111,0.01 0.2852,-0.306 0.3873,-0.6953 z m 14.5638,0.4564 c 1.3057,-0.275 2.0815,-0.556 2.0815,-0.754 0,-0.3409 -1.7648,-2.7156 -2.5545,-3.4374 -0.7298,-0.667 -2.662,-1.8848 -2.9904,-1.8848 -0.048,0 0.095,0.3754 0.3159,0.8343 0.4315,0.8944 1.071,2.9784 1.392,4.5358 0.1085,0.5266 0.2856,0.9561 0.3936,0.9545 0.108,0 0.7209,-0.1133 1.3619,-0.2484 z"
style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.00000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4.00000004, 8.0000001;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

162
static/logo.svg Normal file
View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="300mm"
height="100mm"
viewBox="0 0 300 100"
version="1.1"
id="svg1551"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="logo_rssbridge.svg">
<defs
id="defs1545" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="517.88898"
inkscape:cy="397.65625"
inkscape:document-units="mm"
inkscape:current-layer="g1492"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata1548">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-197)">
<g
style="fill:#1182db;fill-opacity:1"
id="g1492"
transform="matrix(1.063066,0,0,1.063066,31.239097,-1662.8034)"
inkscape:export-xdpi="84.084839"
inkscape:export-ydpi="84.084839">
<flowRoot
inkscape:export-ydpi="68.183243"
inkscape:export-xdpi="68.183243"
xml:space="preserve"
id="flowRoot1458"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:5.40849686"
transform="matrix(0.2885294,0,0,0.2885294,11.385677,1734.3629)"
inkscape:export-filename="C:\Users\Gyrev\Dropbox\4 - Obs\01 - Com - media\03 - infographies\logo_glasses.png"><flowRegion
style="fill:#1182db;fill-opacity:1;stroke-width:5.40849686"
id="flowRegion1454"><rect
style="fill:#1182db;fill-opacity:1;stroke-width:5.40849686"
id="rect1452"
width="555.50842"
height="60.796875"
x="140.55341"
y="275.93942" /></flowRegion><flowPara
id="flowPara1456"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2 Semi-Bold';letter-spacing:3.48859px;fill:#1182db;fill-opacity:1;stroke-width:5.40849686">reconnecting the web</flowPara></flowRoot> <g
id="g1468"
style="fill:#1182db;fill-opacity:1;stroke-width:1.69267535"
transform="matrix(0.59078074,0,0,0.59078074,36.380377,1356.4656)">
<flowRoot
inkscape:export-ydpi="68.183243"
inkscape:export-xdpi="68.183243"
xml:space="preserve"
id="flowRoot1466"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:1.47822654"
transform="matrix(1.7868963,0,0,1.7868963,-131.08627,186.65131)"
inkscape:export-filename="C:\Users\Gyrev\Dropbox\4 - Obs\01 - Com - media\03 - infographies\logo_glasses.png"><flowRegion
style="fill:#1182db;fill-opacity:1;stroke-width:1.47822654"
id="flowRegion1462"><rect
style="fill:#1182db;fill-opacity:1;stroke-width:1.47822654"
id="rect1460"
width="253.20284"
height="65.626038"
x="140.55341"
y="275.93942" /></flowRegion><flowPara
id="flowPara1464"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2 Semi-Bold';letter-spacing:-2.1810503px;fill:#1182db;fill-opacity:1;stroke-width:1.47822654">Bridge</flowPara></flowRoot> </g>
<g
transform="matrix(0.59078074,0,0,0.59078074,66.923727,1356.4656)"
style="fill:#1182db;fill-opacity:1;stroke-width:1.69267535"
id="g1478">
<flowRoot
inkscape:export-filename="C:\Users\Gyrev\Dropbox\4 - Obs\01 - Com - media\03 - infographies\logo_glasses.png"
transform="matrix(1.7868963,0,0,1.7868963,-294.87276,186.65131)"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:1.47822654"
id="flowRoot1476"
xml:space="preserve"
inkscape:export-xdpi="68.183243"
inkscape:export-ydpi="68.183243"><flowRegion
id="flowRegion1472"
style="fill:#1182db;fill-opacity:1;stroke-width:1.47822654"><rect
y="275.93942"
x="140.55341"
height="65.626038"
width="253.20284"
id="rect1470"
style="fill:#1182db;fill-opacity:1;stroke-width:1.47822654" /></flowRegion><flowPara
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2 Semi-Bold';letter-spacing:-2.1810503px;fill:#1182db;fill-opacity:1;stroke-width:1.47822654"
id="flowPara1474">rss</flowPara></flowRoot> </g>
<rect
ry="7.3332076"
rx="7.3332076"
y="1763.6888"
x="3.8301878"
height="41.961315"
width="99.644852"
id="rect1480"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#1182db;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<g
id="g1490"
transform="matrix(0.42065629,0,0,0.42065629,481.55091,842.16455)"
style="fill:#1182db;fill-opacity:1;stroke-width:2.3772378">
<g
style="fill:#1182db;fill-opacity:1;stroke-width:3.88193178"
transform="matrix(0.61238525,0,0,0.61238525,-1199.6119,2184.4357)"
id="g1488">
<path
style="opacity:1;vector-effect:none;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:29.34373856;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:58.68747418, 117.37494829;stroke-dashoffset:0;stroke-opacity:1"
d="m 541.51249,525.34565 c -10.41315,-4.00956 -14.78996,-6.8636 -21.21485,-13.83378 -9.51065,-10.31784 -12.16446,-17.61997 -12.22497,-33.63782 -0.0451,-11.93786 0.4192,-14.53892 3.85734,-21.60913 4.60063,-9.46079 15.51168,-20.18661 24.98644,-24.56227 5.4253,-2.50553 9.38434,-3.11239 20.31033,-3.11325 11.66691,-9.2e-4 14.67404,0.52181 21.42857,3.7249 9.99443,4.73949 19.32279,14.0885 24.07789,24.13117 5.25545,11.09941 5.95363,27.48145 1.65888,38.9238 -4.02442,10.72212 -14.87839,22.6043 -25.31059,27.7083 -10.28725,5.03309 -27.6637,6.08212 -37.56904,2.26808 z"
id="path1482"
inkscape:connector-curvature="0"
transform="scale(0.26458333)" />
<path
style="opacity:1;vector-effect:none;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:29.34373856;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:58.68747418, 117.37494829;stroke-dashoffset:0;stroke-opacity:1"
d="m 683.89345,526.92166 c -0.52382,-0.5238 -0.96246,-2.93451 -0.97477,-5.35714 -0.0352,-6.92667 -5.18979,-29.62173 -10.01089,-44.07679 C 648.75042,405.05698 592.21848,359.91722 516.2625,352.40923 l -9.75001,-0.96376 0.31432,-35.60003 c 0.17288,-19.58002 0.82532,-36.11103 1.44987,-36.73558 1.81708,-1.81708 35.29533,1.40642 53.50247,5.15156 47.00787,9.66933 86.72265,29.83563 117.9638,59.89942 22.60831,21.7563 38.25173,44.31995 51.33931,74.05042 11.72719,26.64016 21.65262,66.64856 24.10447,97.16279 l 1.00439,12.5 h -35.67264 c -19.61996,0 -36.10123,-0.42858 -36.62503,-0.95239 z"
id="path1484"
inkscape:connector-curvature="0"
transform="scale(0.26458333)" />
<path
style="opacity:1;vector-effect:none;fill:#1182db;fill-opacity:1;stroke:none;stroke-width:29.34373856;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:58.68747418, 117.37494829;stroke-dashoffset:0;stroke-opacity:1"
d="m 823.42333,526.81959 -8.80344,-0.50396 -0.79067,-6.72078 c -0.43486,-3.69645 -1.19383,-10.89938 -1.68659,-16.00652 -4.34718,-45.05596 -23.32007,-100.71198 -47.77185,-140.13638 -22.38739,-36.09589 -54.4142,-68.56278 -89.46856,-90.69793 -41.25297,-26.04928 -95.0629,-43.89344 -148.38973,-49.20819 -9.42857,-0.93969 -17.94643,-1.93722 -18.92857,-2.21675 -1.37004,-0.38992 -1.78572,-9.19669 -1.78572,-37.83338 v -37.32517 l 15.35715,0.93347 c 45.68823,2.77713 80.32407,8.98439 117.5,21.05774 91.16555,29.60718 160.46184,87.77122 202.6182,170.06818 24.63558,48.09321 41.9844,114.7859 45.52317,175.00127 l 0.86054,14.64286 -27.71524,-0.27526 c -15.24339,-0.15139 -31.6768,-0.50203 -36.51869,-0.7792 z"
id="path1486"
inkscape:connector-curvature="0"
transform="scale(0.26458333)" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
static/logo_300px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/logo_600px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -43,14 +43,11 @@ header {
color: #1182DB;
}
header > h1 {
font-size: 500%;
font-weight: bold;
}
header > h2 {
margin-left: 1em;
font-size: 200%;
header > div.logo {
background-image: url(logo_600px.png);
width: 599px;
height: 177px;
margin: auto;
}
header > section.warning {
@@ -84,6 +81,12 @@ input[type="number"]:focus {
border-color: #888;
}
input:focus::-webkit-input-placeholder { opacity: 0; }
input:focus::-moz-placeholder { opacity: 0; }
input:focus::placeholder { opacity: 0; }
input:focus:-moz-placeholder { opacity: 0; }
input:focus:-ms-input-placeholder { opacity: 0; }
.searchbar {
width: 40%;
margin: 40px auto 100px;
@@ -101,13 +104,6 @@ input[type="number"]:focus {
text-align: center;
}
.searchbar input[type="text"]:focus::-webkit-input-placeholder,
.searchbar input[type="text"]:focus::-moz-placeholder,
.searchbar input[type="text"]:focus:-moz-placeholder,
.searchbar input[type="text"]:focus:-ms-input-placeholder {
opacity: 0;
}
.searchbar > h3 {
font-size: 200%;
font-weight: bold;
@@ -200,6 +196,7 @@ form {
.parameters label {
text-align: right;
line-height: 1.5em;
}
.parameters label::before {
@@ -304,3 +301,60 @@ h5 {
.advice > li {
text-align: left;
}
@media screen and (max-width: 767px) {
body {
font-size: 75%;
}
header > div.logo {
background-image: url(logo_300px.png);
width: 300px;
height: 89px;
}
header > section.warning {
width: 90%;
}
header > section.critical-warning {
width: 90%;
}
.searchbar {
width: 90%;
margin: 0 auto;
}
section {
width: 90%;
margin: 10px auto;
overflow: hidden;
}
button {
display: inline-block;
width: 40%;
padding: 5px auto;
margin: 3px auto 0;
}
@supports (display: grid) {
.parameters {
grid-template-columns: auto auto;
grid-column-gap: 5px;
}
.parameters label {
line-height: 2em;
word-break: break-word;
}
} /* @supports (display: grid) */
.secure-warning {
width: 100%;
}
}

View File

@@ -77,7 +77,9 @@ class AtomFormatTest extends TestCase {
}
private function initFormat() {
$this->format = \Format::create('Atom');
$formatFac = new FormatFactory();
$formatFac->setWorkingDir(PATH_LIB_FORMATS);
$this->format = $formatFac->create('Atom');
$this->format->setItems($this->sample->items);
$this->format->setExtraInfos($this->sample->meta);
$this->format->setLastModified(strtotime('2000-01-01 12:00:00 UTC'));

View File

@@ -77,7 +77,9 @@ class JsonFormatTest extends TestCase {
}
private function initFormat() {
$this->format = \Format::create('Json');
$formatFac = new FormatFactory();
$formatFac->setWorkingDir(PATH_LIB_FORMATS);
$this->format = $formatFac->create('Json');
$this->format->setItems($this->sample->items);
$this->format->setExtraInfos($this->sample->meta);
$this->format->setLastModified(strtotime('2000-01-01 12:00:00 UTC'));

View File

@@ -42,8 +42,11 @@ class ListActionTest extends TestCase {
'Item count doesn\'t match'
);
$bridgeFac = new BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
$this->assertEquals(
count(Bridge::getBridgeNames()),
count($bridgeFac->getBridgeNames()),
count($items['bridges']),
'Number of bridges doesn\'t match'
);

View File

@@ -78,7 +78,9 @@ class MrssFormatTest extends TestCase {
}
private function initFormat() {
$this->format = \Format::create('Mrss');
$formatFac = new FormatFactory();
$formatFac->setWorkingDir(PATH_LIB_FORMATS);
$this->format = $formatFac->create('Mrss');
$this->format->setItems($this->sample->items);
$this->format->setExtraInfos($this->sample->meta);
$this->format->setLastModified(strtotime('2000-01-01 12:00:00 UTC'));

21
vendor/simplehtmldom/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 S.C. Chen, John Schlick, logmanoriginal
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

15
whitelist.default.txt Normal file
View File

@@ -0,0 +1,15 @@
Bandcamp
Cryptome
DansTonChat
DuckDuckGo
Facebook
Flickr
GoogleSearch
Identica
Instagram
OpenClassrooms
Pinterest
Scmb
Twitter
Wikipedia
Youtube