1
0
mirror of https://github.com/RSS-Bridge/rss-bridge.git synced 2025-08-27 01:54:40 +02:00

Compare commits

..

214 Commits

Author SHA1 Message Date
logmanoriginal
a11ade3442 Bump version to 2018-12-11 2018-12-11 17:01:16 +01:00
logmanoriginal
3932e7b8ef [README] Update list of contributors
Fix links pointing to the API instead of HTML pages
2018-12-10 22:21:33 +01:00
disk0x
5305c405f6 [SoundcloudBridge] Improve Author, Date, Description (#955)
1. Author Name now doesn't include Episode Title
2. It now fetches Episode Creation Timestamp, to allow correct sorting in podcatchers
3. Description is now the actual show notes, and not an <audio> tag
2018-12-10 21:35:18 +01:00
triatic
1c58c04271 [contents] Better error reporting for cUrl errors (#958)
References #954
2018-12-10 21:20:13 +01:00
logmanoriginal
89218f1da6 [.travis.yml] Fix broken checks
- Remove "sudo:false"
- Update composer installation paths

The Linux infrastructure migration removed support for "sudo:false"

-- https://changelog.travis-ci.com/deprecation-container-based-linux-build-environment-82037
-- https://blog.travis-ci.com/2018-11-19-required-linux-infrastructure-migration
2018-12-07 18:52:37 +01:00
disk0x
30e2b79c38 [SoundcloudBridge] Add RSS enclosures (#952)
Minimum viable code change to get SoundcloudBridge produce feeds that podcatchers like gPodder can understand.
2018-12-04 16:16:19 +01:00
Nono
2184f523cd [MozillaSecurity] New Bridge (#946)
* [MozillaSecurity] New Bridge

Kudo to @teromene & @ArthurHoaro on this one !
2018-11-30 18:25:02 +01:00
triatic
242b6953ed [FB2Bridge] Adapt to Facebook html change (#950) 2018-11-30 18:23:37 +01:00
Roliga
bdcb7a9829 [index] Fix detect action after listBridges rename (#947)
Commit 88b0656 renamed listBridges function which was not taken into
account when adding the detect action.
2018-11-29 16:44:38 +01:00
Pierre Mazière
f4b46e497e [GithubIssueBridge] Be consistent in avoiding is_null
Signed-off-by: Pierre Mazière <pierre.maziere@gmx.com>
2018-11-29 16:35:49 +01:00
Pierre Mazière
d5085a4116 [GithubIssueBridge] Fix non existing comments count
Signed-off-by: Pierre Mazière <pierre.maziere@gmx.com>
2018-11-29 16:35:45 +01:00
Pierre Mazière
d7cabfca54 [GithubIssueBridge] Fix issue comments and events parsing
Signed-off-by: Pierre Mazière <pierre.maziere@gmx.com>
2018-11-29 16:35:41 +01:00
Pierre Mazière
de575982a1 [GithubIssueBridge] Fix most relevant coding style related issues
Signed-off-by: Pierre Mazière <pierre.maziere@gmx.com>
2018-11-29 16:35:35 +01:00
LogMANOriginal
3d301fc4ee [contents] Skip caching if the remote server requests no caching (#945)
* Skip caching if Cache-Control defines no-cache
* Skip caching if Cache-Control defines no-store
2018-11-28 17:36:28 +01:00
triatic
263e8872ea core: Don't use server variables in CLI mode (#939) 2018-11-26 18:33:51 +01:00
logmanoriginal
6e9c188a72 [GlassdoorBridge] Fix bridge is marked as executable
References #938
2018-11-26 18:31:25 +01:00
Roliga
49da67cb33 core: Automatically select a bridge based on a URL (#928)
* core: Add bridge parameter auto detection

This adds a new 'detect' action which accepts a URL from which an
appropriate bridge is selected and relevant parameters are extracted.
The user is then automatically redirected to the selected bridge.

For example to get a feed from: https://twitter.com/search?q=%23rss-bridge
we could send a request to:
'/?action=detect&format=Atom&url=twitter.com/search%3Fq%3D%2523rss-bridge'
which would redirect to:
'/?action=display&q=%23rss-bridge&bridge=Twitter&format=Atom'.

This auto detection happens on a per-bridge basis, so a new function
'detectParameters' is added to BridgeInterface which bridges may implement.
It takes a URL for an argument and returns a list of parameters that were
extracted, or null if the URL isn't relevant for the bridge.

* [TwitterBridge] Add parameter auto detection

* [BridgeAbstract] Add generic parameter detection

This adds generic "paramater detection" for bridges that don't have any
parameters defined. If the queried URL matches the URI defined in the
bridge (ignoring https://, www. and trailing /) an emtpy list of parameters is
returned.
2018-11-26 18:05:40 +01:00
sysadminstory
b4dbd191d0 [ZoneTelechargementBridge] Switch to the new Website (#934)
* [ZoneTelechargementBridge] Switch to the new Website

The website zone-telechargement1.org decided that he will be using a new
domain at the end of november :
https://www.annuaire-telechargement.com/

The bridge uses the new domain but still uses the same filename and
class name to keep the existing feed working.
2018-11-20 16:23:17 +01:00
logmanoriginal
e09f452426 [.gitattributes] Exclude files from git archive
Files with the option "export-ignore" are excluded from "git archive"
commands. Release files from GitHub will also ignore those files, so
packages are smaller and don't include unneccessary files.
2018-11-19 18:11:09 +01:00
LogMANOriginal
7b261d1cc2 [contents] Add server side caching for all requests (If-Modified-Since) (#889)
This commit adds a cache for 'getContents' to '/cache/server'. All
contents are cached by default (even in debug mode). If debug mode
is enabled, the cached data is overwritten on each request.

In normal mode RSS-Bridge adds the 'If-Modified-Since' header with
the timestamp from the previously cached data (if available) to the
request.

Find more information on 'If-Modified-Since' here:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since

If the server responds with "304 Not Modified", the cached data is
returned.

If the server responds with "200 OK", the received data is written
to the cache (creates a new cache file if it doesn't exist yet).

No changes were made for all other response codes.

Servers that don't support the 'If-Modified-Since' header, will
respond with "200 OK".

For servers that respond with "304 Not Modified", the required band-
width will decrease and RSS-Bridge will responding faster.

Files in the cache are forcefully removed after 24 hours.

Notice: Only few servers actually do support 'If-Modified-Since'.
Thus, most bridges won't be affected by this change.
2018-11-19 17:53:08 +01:00
logmanoriginal
96a518c9e7 [html] Remove todo as it is already implemented 2018-11-18 17:52:45 +01:00
logmanoriginal
0d2ea9a677 [html] Rename parameters for sanitize() 2018-11-18 17:43:34 +01:00
logmanoriginal
66e82e46db [html] Remove todo tags
It is not feasible to use a single 'substr' in the functions
2018-11-18 17:36:00 +01:00
logmanoriginal
54800fcc8d [html] Clarify meaning of strange find() parameter
simple_html_dom currently doesnt support "->find('*')", which is a
known issue: https://sourceforge.net/p/simplehtmldom/bugs/157/

The solution implemented by RSS-Bridge 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.

This is the meaning behind

"->find('*[!b38fd2b1fe7f4747d6b1c1254ccd055e]')"
2018-11-18 17:32:07 +01:00
logmanoriginal
67004556e6 [BridgeCard] Use self:: instead of BridgeCard:: 2018-11-18 16:59:13 +01:00
logmanoriginal
c6a7b9ac64 exception: Remove HttpException class
This class served no particular purpose (other than adding a
layer on top of Exception).
2018-11-18 16:53:21 +01:00
logmanoriginal
dbffbd4d4e [FormatAbstract] Check content type before sending header 2018-11-18 16:30:34 +01:00
logmanoriginal
1c17ffb5c4 [FeedExpander] Add constants for feed types 2018-11-18 16:18:40 +01:00
logmanoriginal
326cfb21cf [FeedExpander] Rename $name to $title 2018-11-18 16:11:38 +01:00
logmanoriginal
8ab1fb86a9 [FeedExpander] Let collectExpandableDatas() return self 2018-11-18 16:03:32 +01:00
logmanoriginal
a9ec3d0d1f [Configuration] Change scope of $config to private 2018-11-18 15:58:34 +01:00
logmanoriginal
ac5bcb62ec [Configuration] Add documentation for defined constants 2018-11-18 15:52:28 +01:00
logmanoriginal
f24ab8b51b [Configuration] Rename $category to $section in getConfig() 2018-11-18 15:45:17 +01:00
logmanoriginal
4348119adf [Configuration] Make file paths explicit 2018-11-18 15:41:43 +01:00
logmanoriginal
fd4124cda2 [Configuration] Make class final
This class is essential to the core library of RSS-Bridge and must
not be extended. This improves predictability for the behaviour of
this class.
2018-11-18 15:34:16 +01:00
logmanoriginal
91f7405297 [Configuration] Throw exception creating objects of this class
This class only provides static functions.
2018-11-18 15:29:50 +01:00
logmanoriginal
85685b7758 [Authentication] Throw exception creating objects from this class
Callers must use Authentication::showPromptIfNeeded()
2018-11-18 15:20:43 +01:00
logmanoriginal
41d02554f3 [YGGTorrentBridge] Add URI to feed items
References #931
2018-11-18 09:41:14 +01:00
logmanoriginal
c4550be812 lib: Add API documentation 2018-11-18 09:41:14 +01:00
Thibault Couraud
b29ba5b973 [CrewbayBridge] Update bridge according to new crewbay.com website (#930) 2018-11-18 09:16:24 +01:00
logmanoriginal
254fe9212a [Debug] Fix debug mode reports indexing error
Error log reports "PHP Notice:  Undefined offset: 2 in /rss-bridge/
lib/Debug.php on line 112" if the array returned by debug_backtrace
does not contain 3 items.

This commit fixes the issue by always using the last element in the
backtrace "end($backtrace)".
2018-11-16 20:19:52 +01:00
triatic
3806895059 [FacebookBridge] Improve titles (#924)
A slightly improved version of #454 and #468 . Build titles from content rather than author + pre-content (which doesn't reflect anything useful).
2018-11-16 15:33:54 +01:00
triatic
599d438a0d [FacebookBridge] Decode all elements in $item (#925) 2018-11-16 15:25:58 +01:00
triatic
e5a6baab96 [TwitterBridge] Decode HTML entities (#926)
Removes duplicate encoding like &amp;quot; (should be &quot;).
2018-11-15 22:00:01 +01:00
logmanoriginal
b47a30ecc1 [rssbridge] Improve documentation 2018-11-15 20:52:17 +01:00
logmanoriginal
860b36c1e3 [Debug] Use self:: instead of Debug:: inside the class 2018-11-15 20:28:26 +01:00
logmanoriginal
3d475572c6 [Debug] Improve documentation 2018-11-15 20:27:32 +01:00
logmanoriginal
59f2d755fe format: Refactor searchInformation
- Rename function to getFormatName
- Add documentation
- Rename variables
- Remove unused variables
2018-11-15 20:16:21 +01:00
logmanoriginal
d7c374bd8c [Format] Add function isFormatName
Returns true if the provided format name is valid
2018-11-15 20:14:43 +01:00
logmanoriginal
6b6ab6486a [Format] Store real path to working directory 2018-11-15 20:06:45 +01:00
logmanoriginal
6c4e239f64 format: Refactor class Format 2018-11-15 20:06:23 +01:00
logmanoriginal
88b0656954 bridge: Rename listBridge to getBridgeNames 2018-11-15 19:43:23 +01:00
logmanoriginal
66b11b8c41 [Bridge] Fix typo 2018-11-15 19:38:14 +01:00
logmanoriginal
1b34d9860e [Cache] Check if class is instantiable 2018-11-15 19:36:01 +01:00
logmanoriginal
6e70d461e1 [Bridge] Add function isBridgeName
This function returns true if the provided name is a valid
bridge name.
2018-11-15 19:33:56 +01:00
logmanoriginal
0a92b5d29b [Bridge] Refactor Bridge::create to improve readability 2018-11-15 19:31:31 +01:00
logmanoriginal
e3849f45ab [Bridge] Use slashes to enclose regex 2018-11-15 19:30:33 +01:00
logmanoriginal
3d9c4a3718 [Bridge] Improve working directory handling
- Initialize with null to prevent leaking configurations
- Check if the working directory is a directory
- Store the real path instead of raw data
- Add final path separator as expected by Bridge::create
2018-11-15 19:28:56 +01:00
logmanoriginal
5f146a257e [Bridge] Change visibility from private to protected 2018-11-15 19:24:43 +01:00
logmanoriginal
936688e08c [Bridge] Fix typos 2018-11-15 19:22:32 +01:00
logmanoriginal
4b5372638c [Bridge] Use self:: instead of Bridge:: inside the class 2018-11-15 19:19:04 +01:00
logmanoriginal
6f4a8f4d03 [Bridge] Rename to in setWorkingDir 2018-11-15 19:17:18 +01:00
logmanoriginal
39652bb050 [Bridge] Rename to 2018-11-15 19:16:37 +01:00
logmanoriginal
fcac5b8b92 [Bridge] Cleanup documentation and exception messages 2018-11-15 19:15:08 +01:00
logmanoriginal
6f7b56cba8 bridge: Rename setDir and getDir to setWorkingDir and getWorkingDir 2018-11-15 19:07:33 +01:00
logmanoriginal
86ac0a4866 [Cache] Fix typos 2018-11-15 19:00:48 +01:00
logmanoriginal
4a99c6e630 cache: Rename setDir and getDir
- Rename setDir to setWorkingDir
- Rename getDir to getWorkingDir
- Rename parameter $workingDir to $dir in getWorkingDir
2018-11-14 20:39:45 +01:00
logmanoriginal
e8442a3bf8 [Cache] Refactor class
general

- Use self:: instead of Cache:: or static::
- Rename $dirCache to $workingDir
- Initialize $workingDir with null

function setDir

- Clear previous working directory before checking input parameters
- Change wording for the exception messages
- Store realpath instead of raw parameter
- Add path separator
  This ensures the path always ends with the path separator, as assumed
  by Cache::create
- Add check if the provided working directory is a valid directory

function getDir

- Use static parameter instead of function variable
- Change wording for the exception message

function create

- Rename parameter $nameCache to $name
- Rename $pathCache to $filePath
- Change wording for the exception messages

function isValidNameCache

- Rename function to isCacheName
- Rename parameter $nameCache to $name
- Explain in the function documentation the meaning of a 'valid name'
- Ensure Boolean return value (preg_match returns integer)
- Check if $name is a string
- Use slashes to enclose the regex
2018-11-14 20:33:44 +01:00
logmanoriginal
427688fd67 [Cache] Add documentation 2018-11-14 17:06:07 +01:00
logmanoriginal
4a6b3654eb [Bridge] Add and rewrite documentation compatible to phpDocumentor
This is the first step in adding documentation to the core library
of RSS-Bridge. The documentation is not yet extracted by phpdoc,
yet may prove useful to anyone interested in starting with RSS-Bridge.
2018-11-13 20:28:17 +01:00
logmanoriginal
5f867c00b4 [CONTRIBUTING] Add new coding style policies 2018-11-13 18:36:37 +01:00
logmanoriginal
c15b25a07d core: Fix PHPCS violations 2018-11-13 18:27:05 +01:00
logmanoriginal
c296e73c18 [phpcs] Add rules for method declarations in classes 2018-11-13 18:25:37 +01:00
logmanoriginal
007ee4d858 [Bridge] Fix broken bridge initialization
Commit e26d61e introduced a bug that causes the error message "The
bridge you [sic!] looking for does not exist." if the bridge name
specified in the query ends on "Bridge"
(i.e. '&bridge=SoundcloudBridge'), while other queries work fine
(i.e. '&bridge=Soundcloud').

This commit fixes that issue by sanitizing the bridge name before
creating the class.

References #922
2018-11-13 17:36:06 +01:00
Thomas Dalichow
dd95ec6200 core: Fix grammar (#923) 2018-11-13 17:24:36 +01:00
Eugene Molotov
d951000c23 [index] Redirect _cache_timeout requests if the option is disabled (#894)
Requesting `_cache_timeout` on servers where this option is disabled currently results in the error message 'This server doesn\'t support "_cache_timeout"!'. This commit changes that behavior to redirect to the query without `_cache_timeout`.
2018-11-13 17:19:00 +01:00
triatic
51634a72e0 [TwitterBridge] Reorder quoted tweets (#921)
Put content before quoted tweet to match the display order on Twitter
2018-11-12 19:59:46 +01:00
logmanoriginal
78c69b08f0 [index] Fix invalid bridge name FlickrExploreBridge => FlickrBridge 2018-11-10 22:33:19 +01:00
logmanoriginal
3bb3353897 [Bridge] Use static variable in listBridges()
This prevents the function from re-loading the same data over and over
again. Instead the same data is returned on each call, during a single
request.
2018-11-10 22:31:40 +01:00
logmanoriginal
e26d61ec0a core: Refactor bridge whitelisting
- Move all whitelisting functionality inside Bridge.php
- Set default whitelist once in index.php using Bridge::setWhitelist()
- Include bridge sanitizing inside Bridge.php
    Bridge::sanitizeBridgeName($name)

Bridge.php now maintains the whitelist internally.
2018-11-10 22:26:58 +01:00
logmanoriginal
a0490e3673 core: Add Debug::isEnabled() and Debug::isSecure()
Also adds documentation to Debug.php!

* Debug::isEnabled()

Checks if the DEBUG file exists on disk on the first call (stored in
memory for the duration of the instance). Returns true if debug mode
is enabled for the client.

This function also sets the internal flag for Debug::isSecure()!

* Debug::isSecure()

Returns true if debuging is enabled for specific IP addresses, false
otherwise. This is checked on the first call of Debug::isEnabled().
If you call this function before Debug::isEnabled(), the default value
is false.
2018-11-10 20:50:34 +01:00
logmanoriginal
c63af2e7ad core: Add separate Debug class
Replaces 'debugMessage' by specialized debug function 'Debug::log'.
This function takes the same arguments as the previous 'debugMessage'.

A separate Debug class allows for further optimization and separation
of concern.
2018-11-10 20:03:05 +01:00
logmanoriginal
9379854f7a core: Define path to whitelist.txt in rssbridge.php 2018-11-10 19:51:37 +01:00
logmanoriginal
ecdac1b089 core: Add path separator to PATH_CACHE 2018-11-10 19:48:05 +01:00
logmanoriginal
2104fc4d58 core: Move initialization for static paths to rssbridge.php
Bridge, Format and Cache are all part of the core logic of RSS-Bridge
and should therefore be initialized centrally
2018-11-10 19:42:54 +01:00
logmanoriginal
697d63bb96 core: Rename RssBridge.php to rssbridge.php
Using lower case letters because the file doesn't implement a class.
2018-11-10 19:01:57 +01:00
logmanoriginal
2bb13169b4 [Configuration] Use FILTER_VALIDATE_EMAIL on admin/email
This prevents including arbitrary data as email address.
2018-11-10 18:43:16 +01:00
logmanoriginal
4713fb6190 Bump version to dev.2018-11-10 2018-11-10 18:11:49 +01:00
logmanoriginal
a08811f147 Bump version to 2018-11-10 2018-11-10 18:04:58 +01:00
logmanoriginal
a935e310ff travis: Rewrite checks and add PHP compatibility tests
- Remove HHVM

HHVM recently announced ending PHP support:
https://hhvm.com/blog/2018/09/12/end-of-php-support-future-of-hack.html

"HHVM v3.30 will be the last release series where HHVM aims to support
PHP. [...] Ultimately, we recommend that projects either migrate
entirely to the Hack language, or entirely to PHP7 and the PHP runtime."

RSS-Bridge never "officially" supported HHVM, so support can be removed.

- Use composer for all versions

PHP 5.6 is using PEAR, while all other versions use Composer to manage
packages and dependencies. This commit removes PEAR for PHP 5.6 in favor
of Composer. This also simplifies the script.

- Add PHP compatibility tests

Uses https://github.com/PHPCompatibility/PHPCompatibility

RSS-Bridge supports PHP 5.6 or higher. This commit adds tests to check
compatibility and detect breaking changes.

"phpcompatibility.xml" contains the ruleset.

Notice: Technically RSS-Bridge requires PHP 5.6.1, but for some reason
PHPCompatibility doesn't accept "5.6.1" for "testVersion". This is why
INI_SCANNER_TYPED is excluded from tests.

- Rearrange tests

PHP 5.6:
  - Coding style (phpcs.xml)
  - PHP compatibility (phpcompatibility.xml)

PHP 7.0:
  - Coding style (phpcs.xml)
  - Unit tests (phpunit.xml) - using stable release of PHPUnit

PHP nightly:
  - Coding style (phpcs.xml)
  - Unit tests (phpunit.xml) - using latest version of PHPUnit
  - PHP compatibility - no exceptions for PHP 5.6+

- Documentation added to improve maintainability
2018-11-09 20:49:47 +01:00
LogMANOriginal
7e3787a185 .github: Add issue template for bridge requests
This commit adds an issue template for bridge requests, automatically suggested to anyone reporting a new issue.

References https://gist.github.com/4c38d575de8f1edd386fe7c2d529ab6f

Closes #759
2018-11-08 19:45:16 +01:00
logmanoriginal
039c032798 Add folder for GitHub related files
For more information see
https://help.github.com/articles/setting-guidelines-for-repository-contributors/
2018-11-08 19:31:33 +01:00
logmanoriginal
cb91d9cce8 [FacebookBridge] Fix media origin info is not inside a tag
References #912
2018-11-08 19:24:14 +01:00
triatic
bf91f106b4 [FacebookBridge] Remove "Posts" from author name (#917) 2018-11-08 19:04:58 +01:00
logmanoriginal
0b2ede35cd [FacebookBridge] Don't remove origin information from embedded media
References #912
2018-11-08 18:59:12 +01:00
logmanoriginal
5842bdfc83 [FacebookBridge] Simplify implementation 2018-11-08 18:45:25 +01:00
logmanoriginal
68ee24d6bd [FacebookBridge] Remove videos and views
This commit adds filters to remove embedded videos and view counts from
all posts. This doesn't remove the preview image for videos, which are
embedded separately.
2018-11-08 18:36:11 +01:00
logmanoriginal
104ae2298e [FacebookBridge] Remove hidden elements
Hidden elements are used for error conditions and generally made
visible using JavaScript. Since RSS-Bridge doesn't support JS, these
error messages are shown in the final feed. For example:

"It looks like you may be having problems playing this video. If so,
please try restarting your browser."

This commit removes all hidden elements to prevent error messages being
added to the feed.

- "It looks like you may be having problems playing this video. If so,
please try restarting your browser."
2018-11-08 18:24:05 +01:00
logmanoriginal
7026684e34 [FacebookBridge] Don't remove description of embedded media
FB includes origin information (i.e. "YOUTUBE.COM") as well as
descriptions with embedded media (images and video).

These details are currently being removed by the bridge.

This commit changes implementation to only remove origin information
and keep the media description in place. The media description consists
of two elements - title and description. The title provided by FB is
included in an anchor, which gets replaced by a paragraph with the
same contents to improve readability.

References #912
2018-11-08 18:12:57 +01:00
teromene
0b792d77eb [Rue89Bridge] Fix style. 2018-11-07 23:16:28 +01:00
teromene
110b865a54 [Rue89Bridge] Entirely rewrite the bridge. It now uses the JSON api. 2018-11-07 23:13:45 +01:00
teromene
19a7f10160 [InstagramBridge] Support Instagram Locations. Fixes #705. 2018-11-07 22:17:53 +01:00
Antoine Turmel
42e25e7fc0 [OnVaSortirBridge] New Bridge (#914)
Bridge to expand OnVaSortir RSS feed to get the full description of an event
2018-11-07 18:52:29 +01:00
logmanoriginal
4b7fea5ebc [RssBridge] Include interfaces once 2018-11-06 19:23:32 +01:00
logmanoriginal
95bd206e9d core: Move REPOSITORY from index.php to RssBridge.php 2018-11-06 18:53:35 +01:00
logmanoriginal
9910310652 [BridgeImplementationTest] Use PATH_LIB_BRIDGES 2018-11-06 18:46:18 +01:00
logmanoriginal
12f0e5a360 [RssBridge] Include path separator in PATH_* 2018-11-06 18:44:45 +01:00
logmanoriginal
81ba96ff94 core: Add PATH_LIB_BRIDGES, PATH_LIB_FORMATS and PATH_LIB_CACHES
- PATH_LIB_BRIDGES defines the path to bridges
- PATH_LIB_FORMATS defines the path to formats
- PATH_LIB_CACHES defines the path to caches

Include constants in RssBridge.php for consistency
2018-11-06 18:42:27 +01:00
logmanoriginal
984f0b24d0 [RssBridge] Rename PATH_VENDOR to PATH_LIB_VENDOR
This improves clarity for the parameters
2018-11-06 18:39:05 +01:00
logmanoriginal
2126db84ac core: Replace CACHE_DIR by PATH_CACHE
Move CACHE_DIR from index.php to /lib/RssBridge.php and change name
to PATH_CACHE.

PATH_CACHE is one of the core paths of RSS-Bridge and should therefore
be defined in the core file RssBridge.php.
2018-11-06 18:35:43 +01:00
logmanoriginal
4bf45df18e [RssBridge] Simplify documentation for this file
- Remove file documentation and license remark (defined in repository
scope - see README / UNLICENSE)

- Remove example usage (if necessary should be included in the Wiki)
2018-11-06 18:31:48 +01:00
logmanoriginal
a88b148d20 [RssBridge] Add PATH_LIB
Add constant PATH_LIB, pointing to '/lib' to make the include process
same for vendor and lib files.
2018-11-06 18:24:07 +01:00
logmanoriginal
f564925ba0 [RssBridge] Use require_once instead of require
"The require_once statement is identical to require except PHP will
check if the file has already been included, and if so, not include
(require) it again."

-- http://php.net/manual/en/function.require-once.php
2018-11-06 18:15:10 +01:00
logmanoriginal
22e8f8b4aa [RssBridge] Skip searching vendor files
Vendor files (simple_html_dom.php and urljoin.php) are included in the
repository and therefore shipped with all releases. If one of the files
is missing, either the repository or the release is incomplete.

PHP will generate error messages if either of the files is missing, so
there is no need to check availability manually unless it is done for
all files (which doesn't make sense because they are part of the
repository).
2018-11-06 18:11:18 +01:00
logmanoriginal
bfae04d1fe [RssBridge] Include __DIR__ in PATH_VENDOR 2018-11-06 18:08:53 +01:00
teromene
723bd1150a Remove tracking codes from Facebook posts 2018-11-06 16:58:58 +01:00
Thibault Couraud
53d2fbe3a5 [FindACrewBridge] Implement bridge for findacrew.net (#901)
* [FindACrewBridge] Implement bridge for findacrew.net - sailing boats offers
2018-11-06 14:57:54 +01:00
Thibault Couraud
3babd02658 [CrewbayBridge] Implement bridge for crewbay.com (#902)
* [CrewbayBridge] Implement bridge for crewbay.com - sailing boats offers
2018-11-06 14:56:23 +01:00
logmanoriginal
3031fa406d core: Set code in header() instead of calling http_response_code() 2018-11-05 19:29:01 +01:00
logmanoriginal
85c34a0960 [CHANGELOG.md] Remove file
The latest changelog is available at
https://github.com/RSS-Bridge/rss-bridge/releases
2018-11-05 19:14:44 +01:00
logmanoriginal
5deb86acff core: Replace PHP_VERSION_REQUIRED by static text
The required PHP version is used in one place only and
therefore shouldn't require a constant
2018-11-05 19:07:33 +01:00
logmanoriginal
946e66e9df core: Use REPOSITORY constant where applicable 2018-11-05 19:05:59 +01:00
logmanoriginal
1a00dfa412 [index.php] Change user agent to constant and include current version 2018-11-05 19:04:30 +01:00
Corentin Garcia
0f8443e1d3 [RainbowSixSiegeBridge] Fix missing news (#908) 2018-11-05 18:20:17 +01:00
Albirew
7d474e5361 [ThePirateBayBridge] Fix TLD from .org to .wf (#907) 2018-11-05 18:17:46 +01:00
Corentin Garcia
8c97953211 [CommonDreamBridge] Promote to secure bridge (fix #777) (#909) 2018-11-05 17:32:11 +01:00
logmanoriginal
d987ceec73 [CONTRIBUTING.md] Include all policies and link to the Wiki 2018-11-05 14:07:14 +01:00
logmanoriginal
392e3ff6c7 phpcs: Fix violations 2018-11-05 12:55:58 +01:00
logmanoriginal
e295dc5a79 [phpcs] Add check for concatenation operator spacing
The concatenation operator should have one space before and after
2018-11-05 12:52:18 +01:00
logmanoriginal
b9f6bc8197 [XenForoBridge] Fix broken conditions
Restore functionality for https://xenforo.com/community/
2018-11-05 12:19:45 +01:00
logmanoriginal
9c1c0f2974 [XenForoBridge] Fix broken checks 2018-11-05 12:05:14 +01:00
logmanoriginal
65da157fff [XenForoBridge] Add new bridge
Adds a bridge for forums powered by XenForo (see https://xenforo.com).

Support between forums may vary due to ever changing versions with no
clear distinction. Especially timestamps may not work depending on the
supported language (should currently work on en-US and de-DE).

Tested on

- https://xenforo.com/community/
- http://www.ign.com/boards/

Notice: XenForo provides RSS feeds for forums (but not specific topics).
For example: https://xenforo.com/community/forums/-/index.rss
2018-11-05 12:00:12 +01:00
triatic
5fe943562a [FB2Bridge] Prevent shared post duplication (#904)
Prevent shared posts appearing twice in feed.
2018-11-05 11:46:56 +01:00
Thibault Couraud
c58331f74d [BAEBridge] Add bridge for bourse-aux-equipiers.com (#903) 2018-11-05 11:38:22 +01:00
Antoine Turmel
145a46ae1d [ThingiverseBridge] Add new bridge (#869) 2018-11-05 11:27:32 +01:00
mr-flibble
1a7a7bad98 [contents.php] Fix typo (#900)
This fixes "The requested resouce cannot be found!" on line 67
2018-11-05 11:10:32 +01:00
Yardena Cohen
27d6a22675 core: Display optional administrator email (#896) 2018-11-05 10:46:44 +01:00
teromene
b55ec51e0e Fix timestamp decoding 2018-11-04 21:50:18 +01:00
hunhejj
07b4c72d5d [InstagramBridge] Don't add duplicated urls when parsing Instagram stories (#715) 2018-11-03 12:12:37 +01:00
logmanoriginal
2e6cbd1ce7 [GitHubGistBridge] Fix broken bridge
`defaultLinkTo` makes anchors point to the correct path which broke
parsing because it expected href to start with `#gistcomment`.

This commit changes the implementation to make `defaultLinkTo` point
to the correct page (using `getURI` instead of `self::URI`) and search
with `*=` instead of `^=`.
2018-11-03 11:56:51 +01:00
LogMANOriginal
2ac2f3dc66 [README] Add info about feed readers
References #892
2018-11-02 11:45:45 +01:00
logmanoriginal
e2dfea2b77 [index.php] Filter parameter '_error_time' from queries
The parameter is used in error feeds. Since RSS-Bridge returns valid
feeds for error conditions, feed readers may attempt to access the
URI returned for the feed item in order to collect additional data,
thus including the parameter '_error_time' in the query.

This results in another error message, because it is an invalid input
parameter. Filtering the parameter allows RSS-Bridge to return the
original feed.

References #882
2018-11-02 11:05:48 +01:00
Yardena Cohen
c4896c7791 [Configuration] Fix open_basedir warnings (#887)
If .git/HEAD isn't in open_basedir it'd throw ugly warnings.
Suppress errors while checking if file is readable
2018-10-27 10:53:45 +02:00
logmanoriginal
7621784598 bridges: Add favicon to bridges missing it
Adds favicon to bridges that support it. Some sites prevent downloading
favicons, those bridges are left untouched.

Affected bridges:

- AutoJMBridge
- BandcampBridge
- BlaguesDeMerdeBridge
- BloombergBridge
- BundesbankBridge
- ChristianDailyReporterBridge
- ContainerLinuxReleasesBridge
- DailymotionBridge
- DiceBridge
- DribbbleBridge
- EliteDangerousGalnetBridge
- ElsevierBridge
- FacebookBridge
- FB2Bridge
- FDroidBridge
- FierPandaBridge
- GooglePlusPostBridge
- JapanExpoBridge
- KATBridge
- KernelBugTrackerBridge
- LegifranceJOBridge
- NotAlwaysBridge
- NyaaTorrentsBridge
- PinterestBridge
- RadioMelodieBridge
- RainbowSixSiegeBridge
- SupInfoBridge
- TagBoardBridge
- TebeoBridge
- TheTVDBBridge
- WhydBridge
- ZoneTelechargementBridge
2018-10-26 19:10:58 +02:00
logmanoriginal
1cfe939927 [AskfmBridge] Fix broken bridge
References #774
2018-10-24 18:33:07 +02:00
logmanoriginal
c56f7abc2a [FacebookBridge] Reduce occurrence of HTTP error 302
Facebook returns "HTTP/1.1 302 Found" when requesting:
  https://www.facebook.com//pg/username/posts?_fb_noscript=1
Automatically redirecting to:
  https://www.facebook.com/username/posts/

We receive a positive response faster when directly requesting the
correct page:
  https://www.facebook.com/username/posts?_fb_noscript=1

Notice: This is just a minor adjustment to improve performance while
requesting data from the server. The previous version worked fine as
well.
2018-10-24 17:27:46 +02:00
logmanoriginal
e3030cbbfd [InstagramBridge] Reduce occurrence of HTTP error 301
Instagram returns "HTTP/1.1 301 Moved Permanently" on each request
to "https://instagram.com/" because the correct location is
"https://www.instagram.com/".

Instagram will respond with "HTTP/1.1 301 Moved Permanently" if the
URI for the requested user doesn't end with a slash.

Notice: This is only a minor enhancement to prevent error 301 from
happening. The previous version worked fine as is.
2018-10-24 16:42:28 +02:00
logmanoriginal
953c6e1022 [contents] Skip setting options on empty array 2018-10-24 16:28:26 +02:00
logmanoriginal
dbd44f64dd [contents] Add debug messages for 'getContents'
Adds additional messages to the error log when fetching contents. The
data is helpful in finding issues with receiving contents from servers.

References: #879, #882, #884
2018-10-24 16:10:33 +02:00
logmanoriginal
89ca42da54 [index] Always write exceptions to error.log
Exceptions are reported to users, but they do not necessarily appear
in the error log on the server. Using 'error_log' we can explicitly
write exceptions and error messages to the log file, using the
standard PHP message format.

For more information see https://stackoverflow.com/a/26867035
2018-10-24 15:58:12 +02:00
sysadminstory
b4b5340b7e [ZoneTelechargementBridge] Make the bridge more robust to URL change (#881)
Using the classical www.zone-telechargement1.org as base URL, the bridge will
always be redirected to the actual wwX.zone-telechargement1.org final URL. This
makes the bridge more robust to URL changes.
2018-10-22 19:22:02 +02:00
Eugene Molotov
a508dddb36 [core] Fixed broken caching (#880) 2018-10-22 19:14:49 +02:00
logmanoriginal
cb488d9d8c [FacebookBridge] Fix broken feeds
This commit collects the original contents from a different
tag to prevent this issue. The root cause is unknown but closely
related to the regex.

References #877
2018-10-20 15:45:20 +02:00
Antoine Turmel
9820ad5c0f [BridgeCard] Fix checkbox default value (#874)
The current solution just output "1" when checked instead of "checked"
2018-10-20 13:14:46 +02:00
Antoine Turmel
ea2d54523d [EtsyBridge] Fix bridge and correct typos (#873) 2018-10-20 13:08:03 +02:00
Eugene Molotov
87d218296e [YoutubeBridge] Fix playlist mode (#876)
* Corrected duration text selector
* Request YouTube page with English localization
* Filter video items in the beginning of the loop
2018-10-20 12:43:48 +02:00
teromene
afd5ef0f1d [FB2Bridge] Add images support
[FB2Bridge] Add basic "cards" support
2018-10-18 21:10:02 +02:00
teromene
30bc5179c2 Fix number of fetched items.
Strip the username.
2018-10-18 18:44:11 +02:00
teromene
7596be65f2 Use a new URL for the cursor. Should fix #851.
Remove the "...More" item in the output
Remove the information card data
2018-10-18 18:07:07 +02:00
Eugene Molotov
16f0ee7104 [InstagramBridge] added caption existance check in getInstagramStory (#865)
* [InstagramBridge] added caption existance check in getInstagramStory

* [InstagramBridge] Coding policy fixes
2018-10-18 16:45:03 +02:00
fluffy
e0323f06cd update php-urljoin (#867) 2018-10-18 16:43:39 +02:00
logmanoriginal
717b0bdd9c Fix items link to localhost
References #864
2018-10-16 19:16:51 +02:00
logmanoriginal
62d737efe2 Replace emoticon images by their textual representation
References #850
2018-10-16 19:02:55 +02:00
triatic
6fce03daa7 [FB2Bridge] Add updated timestamps to each post (#849)
Additionally, exclude shared posts from output since they already exist inside other posts.
2018-10-16 18:34:39 +02:00
logmanoriginal
7561c0685d [FacebookBridge] Fix 'SpSonSsoSredS' text in title
The function 'defaultLinkTo' applied to the source HTML does break
regex matches later in the bridge. We need to apply the function
right before adding the contents to the item for the bridge to work
properly.

References #856
2018-10-15 19:53:46 +02:00
logmanoriginal
f48eac854f Bump version to 'dev.2018-10-15' 2018-10-15 18:59:03 +02:00
logmanoriginal
a87e7781b1 Bump version to 2018-10-15 2018-10-15 18:54:53 +02:00
logmanoriginal
0dc761d6cf [README] Update authors
Not sure why, but the GitHub API responded with false results the
last time. Cleaning up to reflect current list of contributors.
2018-10-15 18:53:27 +02:00
logmanoriginal
d14f8e3c83 [BundesbankBridge] Add new bridge 2018-10-15 18:38:42 +02:00
logmanoriginal
b4aea21f71 [DesoutterBridge] Add new bridge 2018-10-15 18:35:49 +02:00
logmanoriginal
c06a09fe99 [GlassdoorBridge] Add new bridge 2018-10-15 18:33:02 +02:00
sysadminstory
704ad50607 [DealabsBridge] Follow website changes (#852)
Pepper changed the CSS class of some elements. The bridge was changed to
follow these changes.
2018-10-15 18:25:04 +02:00
sysadminstory
d89c65d219 [ZoneTelechargementBridge] Update the base URL and make URI unique (#853)
- Base URL updated
- Show name has different styles on the Website, use another way to get the show name
- Entry URIs are now unique to make sure RSS readers don't treat episodes as duplicates
- No more new lines in the feed or item title
2018-10-15 18:23:08 +02:00
sysadminstory
9a3c776096 [ExtremeDownloadBridge] Make URI and titles unique (#854)
- Entry URIs are unique to make sure RSS readers don't treat episodes as duplicates
- Titles are unique to make sure RSS readers don't treat streams and downloads as duplicates
2018-10-15 18:19:57 +02:00
triatic
85e8a67568 [MrssFormat.php] Prevent PHP Notice (#858)
Prevent PHP Notice when running in CLI mode
2018-10-15 18:14:06 +02:00
Nicolas Delsaux
ee158468fa Expanded Sexactu to cover the whole GQ magazine (#861)
The bridge has been expanded to better cover the whole GQ magazine.
It should support all countries (provided they all use the same absurdly shitty publication system).
It is guaranteed to be only tested with sexactu articles (that I now obtain by loading Maïa Mazaurette author page).
2018-10-15 18:09:20 +02:00
logmanoriginal
5779f641c0 [FacebookBridge] Add option to limit number of returned items
This commit adds a new optional parameter 'limit' which can be used
to limit the number of items returned by this bridge (i.e. '&limit=10')

As requested in #669
2018-10-15 17:35:10 +02:00
LogMANOriginal
b90bcee1fc Return exceptions in requested feed formats (#841)
* [Exceptions] Don't return header for bridge exceptions
* [Exceptions] Add link to list in exception message

This is an alternative when the button is not rendered
for some reason.

* [index] Don't return bridge exception for formats
* [index] Return feed item for bridge exceptions
* [BridgeAbstract] Rename 'getCacheTime' to 'getModifiedTime'
* [BridgeAbstract] Move caching to index.php to separate concerns

index.php needs more control over caching behavior in order to cache
exceptions. This cannot be done in a bridge, as the bridge might be
broken, thus preventing caching from working.

This also (and more importantly) separates concerns. The bridge should
not even care if caching is involved or not. Its purpose is to collect
and provide data.

Response times should be faster, as more complex bridge functions like
'setDatas' (evaluates all input parameters to predict the current
context) and 'collectData' (collects data from sites) can be skipped
entirely.

Notice: In its current form, index.php takes care of caching. This
could, however, be moved into a separate class (i.e. CacheAbstract)
in order to make implementation details cache specific.

* [index] Add '_error_time' parameter to $item['uri']

This ensures that error messages are recognized by feed readers as
new errors after 24 hours. During that time the same item is returned
no matter how often the cache is cleared.

References https://github.com/RSS-Bridge/rss-bridge/issues/814#issuecomment-420876162

* [index] Include '_error_time' in the title for errors

This prevents feed readers from "updating" feeds based on the title

* [index] Handle "HTTP_IF_MODIFIED_SINCE" client requests

Implementation is based on `BridgeAbstract::dieIfNotModified()`,
introduced in 422c125d8e and
simplified based on https://stackoverflow.com/a/10847262

Basically, before returning cached data we check if the client send
the "HTTP_IF_MODIFIED_SINCE" header. If the modification time is
more recent or equal to the cache time, we reply with "HTTP/1.1 304
 Not Modified" (same as before). Otherwise send the cached data.

* [index] Don't encode exception message with `htmlspecialchars`
* [Exceptions] Include error message in exception
* [index] Show different error message for error code 0
2018-10-15 17:21:43 +02:00
logmanoriginal
996295e82f Add 'dev.' to the release version in master
This helps (roughly) identifying versions when opening issues on
GitHub, using the latest ZIP file for master.

References #773
2018-09-26 20:04:27 +02:00
logmanoriginal
13bd7fe21b [contents] Return error if the server responded with any code other than 200 2018-09-26 19:16:02 +02:00
logmanoriginal
fcc9f9fd61 [FacebookBridge] Use alternative URI to load more posts
The URI "https://facebook.com/username?_fb_noscript=1" returns two
posts per user. Some profiles, however, are very active, causing the
bridge to miss items if more than two posts are send within the cache
duration (5 minutes).

The alternative suggested in #669 is to use a different URI:
"https://facebook.com/pg/username/posts?_fb_noscript=1"

While the contents of this URI essentially look the same when viewed
in a browser, it actually returns more than 10 posts depending on the
profile.

References #669
2018-09-26 18:24:46 +02:00
logmanoriginal
e1c4914b1c [FacebookBridge] Optimize for readability 2018-09-25 18:56:33 +02:00
logmanoriginal
93e7ea9fea [HtmlFormat] Make feeds available via syndication links 2018-09-22 19:51:18 +02:00
logmanoriginal
2d1b446bd1 [DevToBridge] Add new bridge
Returns feeds for tags from https://dev.to

References #840
2018-09-22 18:57:07 +02:00
logmanoriginal
1d451610d6 [ParameterValidator] Move 'getQueriedContext' from BridgeAbstract 2018-09-22 17:04:55 +02:00
logmanoriginal
f853ffc07c [ParameterValidator] Refactor 'validation' into 'ParameterValidator'
Adds a new class 'ParameterValidator' to replace the functions from
'validator.php', separating private functions from 'validateData' to
class private functions in the process.

Instead of echoing error messages, adds messages to a private variable,
accessible via 'getInvalidParameters'.

BridgeAbstract now adds invalid parameter names to the error message.
2018-09-22 16:42:04 +02:00
logmanoriginal
e3a5a6a170 [index] Update and improve parameter handling for bridge and cache
- Use 'array_diff_key' instead of 'unset'
- Remove parameters for caches

By removing certain parameters for caches, the loading times can be
improved considerably:

* action: It doesn't matter which action the user took to generate
feed items.

* format: This has the biggest impact on performance, because cached
items are now shared between different formats (i.e. try switching
between Atom, Html and Mrss and compare previous vs. now). If a
server handles lots of requests, this may even reduce bandwidth if
the same contents are requested for different formats.

* _noproxy: The proxy behavior has no impact on the produced items,
so it can be ignored.

* _cache_timeout: This is another option which might impact performance
for some servers, especially if 'custom_timeout' has been enabled in
the configuration. Requests with different cache timeouts no longer
result in separate cache files.
2018-09-22 15:44:03 +02:00
logmanoriginal
243e324efc [NineGagBridge] Fix missing sections breaking feeds
Posts may supply a list of 'sections' or a single 'postSection'

References #844
2018-09-22 15:19:14 +02:00
logmanoriginal
ae58b1566e [NineGagBridge] Remove type hinting
Type hinting for strings doesn't work prior to PHP 7, see
http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration

References #837
2018-09-22 15:19:14 +02:00
sysadminstory
c044694b21 [ZoneTelechargementBridge] Sort episodes from newest to oldest (#835)
References #834
2018-09-21 20:22:49 +02:00
triatic
db24f55c86 [FB2Bridge] Do not strip <h3> and <h4> (#836)
Do not strip <h3> and <h4>. Output looks better when they are retained. See attached.
2018-09-21 20:19:22 +02:00
logmanoriginal
eb30038d6b [README] Update and reorganize 2018-09-16 18:20:35 +02:00
logmanoriginal
712a581ed6 [README] Add badge for Guix release
Unfortunately there is no way to query the current package version,
so this is only a placeholder
2018-09-16 16:01:51 +02:00
logmanoriginal
d3df4b51b8 [README] Add badge for current debian release 2018-09-16 15:13:30 +02:00
logmanoriginal
e6476a600d [KununuBridge] Fix broken bridge and simplify implementation 2018-09-16 09:55:35 +02:00
Grégory T
811e8d8c88 [ETTVBridge] Improvements and bug fixes (#682)
* Fix typo with status field
* Comply with other bridges

Change the uri element of an item to point, not on the magnet link, but on the page, as similar bridges do.

* Improved to return name & uri matching with query

This change makes it possible for the feed reader to discover a title and url consistent with the user's search.
2018-09-15 17:11:36 +02:00
logmanoriginal
adc6f72e97 [style] Fix first letter of labels not capitalized
This error is caused by setting label::before { content: " "; },
which makes the first letter a whitespace on all labels, neccessary
 for browsers that doesn't support the grid layout.

This commit clears the content if the browser supports the grid layout,
properly capitalizing labels again. If a browser doesn't support grid
layout, labels stay as they are provided by the bridge.
2018-09-15 17:04:20 +02:00
logmanoriginal
182153485c [Arte7Bridge] Move parameter examples into tool tip for readability 2018-09-15 16:50:10 +02:00
LogMANOriginal
bf9946d1fc CSS adjustments to improve readability for bridge parameters (#763)
* Group common selectors
* Fix indentation using tabs
* Use same styles for number and text inputs
* Use grid layout for parameters

Introduces the grid layout for bridge parameters. All parameters are
arranged in a grid to improve readability. Read more on grid layouts
at

- https://www.w3schools.com/css/css_grid.asp
- https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout

Notice:

Grid layouts are not supported in very old browser versions:
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/CSS_Grid_and_Progressive_Enhancement

This is why @supports checks for browser support (not supported in IE)
https://developer.mozilla.org/en-US/docs/Web/CSS/@supports#Browser_compatibility

In case grid layout is not supported, the displayed form is usable
but not very pretty due to <br> being removed by this commit for
cosmetic reasons (breaks grid layout).

Unfortunately it doesn't seem possible to insert line breaks manually
via '::after { content: '\A' }' in cases where grid layout isn't
supported.

* Add padding to card parameters

Adds padding to parameters to improve readability. For bridges without
parameters (count($parameters) === 0), the parameter 'div' is no longer
created.

* Add colon ':' after label via CSS
* Capitalize first letter of label for readability
* Fix checkbox isn't aligned left

Sets the size of the checkbox to 20x20 px for good measure.

* Harmonize formatting
* Add new style to number and select boxes

References #797

* Add fallback solution for browsers without grid support
2018-09-15 16:39:50 +02:00
triatic
ec60752650 [FB2Bridge] Prevent Facebook link href's ending in two quotes (#831)
Additionally prevent Facebook links having two forward slashes after the hostname.
2018-09-15 15:16:15 +02:00
sysadminstory
6688cf0c3b [AutoJMBridge] Fix concatenation bug (#833) 2018-09-15 15:12:34 +02:00
ORelio
ae45a8cfee [contents] Fix open_basedir warning (#832)
References #818
2018-09-15 14:46:11 +02:00
Matthew Seal
e34ef6cb4f [MrssFormat] Escape double quotes in XML attributes (#813)
XML attributes need to have certain characters escaped to be valid. The title attribute can have double quotes in it which need to be properly encoded for attributes.
2018-09-15 14:13:05 +02:00
sysadminstory
5c92a736fa [ZoneTelechargementBridge] Added Bridge for ww2.zone-telechargement1.org (#829)
* [ZoneTelechargementBridge] Added Bridge for ww2.zone-telechargement1.org

Goal for this bridge is to follow the episode publication of a TV show
season while it's broadcasted on the TV.
2018-09-13 19:36:48 +01:00
Eugene Molotov
911bcfb246 [PikabuBridge] Implemented bridge (#830)
* [PikabuBridge] Implemented bridge
2018-09-13 12:52:26 +01:00
ZeNairolf
efa550ef61 Add 9gag.com bridge (#801)
* Add 9gag.com bridge
2018-09-13 10:11:42 +01:00
sysadminstory
d5d7683ed3 [AutoJMBridge] New Bridge (#827)
* [AutoJMBridge] New Bridge

This bridge will show all the car offers AutoJM has for the model you
choosed and using your filter. Very useful to wait for a cheap price for
a new car !
2018-09-13 10:05:07 +01:00
triatic
fe94914eb5 [AtomFormat.php] Eliminate PHP Notice when running in CLI mode (#824) 2018-09-12 14:37:27 +01:00
Quentin Delmas
622802e5d4 Fix multiple warnings.
Fix JSON request string in case of empty location
2018-09-12 13:31:11 +01:00
sysadminstory
6da8daf1a3 [DealabsBridge] Fix for #782 and all categories are now available (#821)
This commit fixes #782 by updating the parameter value of 'Maison &
Jardin', but this means the user has to update his RSS Feed URL (.because
of the bridge structure, it would be a nightmare to fix it in another
way)

This commits add all the categories available on Dealabs Website.
2018-09-11 22:11:00 +01:00
la Bécasse
654e502e84 Arte7 collection support (#819)
* Arte7 collection support
2018-09-11 22:09:47 +01:00
sysadminstory
c8ace9e3bd [ExtremeDownloadBridge] Added Bridge for ww1.extreme-d0wn.com (#820)
* [ExtremeDownloadBridge] Added Bridge for ww1.extreme-d0wn.com

Goal for this bridge is to follow the episode publication of a TV show season
while it's broadcasted on the TV.
2018-09-11 20:10:46 +01:00
Monsieur Poutounours
5722a6c139 Adding a bridge for theyetee.com (#809)
* Adding a bridge for theyetee.com

The bridge fetches daily shirts at theyetee.com.
The Yetee offers two new shirts each day, but you can buy them only for a few hours !
Unfortunately, the site don't provide RSS feed, so the only way to keep up to date on new shirt is their daily mailing ... until now !
2018-09-10 20:56:55 +01:00
Quentin Delmas
458b826871 Remove declaration of extractFromDelimiters, it is now a reusable function. Fixes #815 2018-09-10 09:29:19 +01:00
125 changed files with 7772 additions and 1905 deletions

View File

@@ -4,5 +4,4 @@ DEBUG
Dockerfile
whitelist.txt
phpcs.xml
CHANGELOG.md
CONTRIBUTING.md

14
.gitattributes vendored
View File

@@ -20,3 +20,17 @@
*.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

49
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,49 @@
### Pull request policy
* [Fix one issue per pull request](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#fix-one-issue-per-pull-request)
* [Respect the coding style policy](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#respect-the-coding-style-policy)
* [Properly name your commits](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#properly-name-your-commits)
* When fixing a bridge (located in the `bridges` directory), write `[BridgeName] Feature` <br>(i.e. `[YoutubeBridge] Fix typo in video titles`).
* When fixing other files, use `[FileName] Feature` <br>(i.e. `[index.php] Add multilingual support`).
* When fixing a general problem that applies to multiple files, write `category: feature` <br>(i.e. `bridges: Fix various typos`).
Note that all pull-requests must pass all tests before they can be merged.
### Coding style
* [Whitespace](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace)
* [Add a new line at the end of a file](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#add-a-new-line-at-the-end-of-a-file)
* [Do not add a whitespace before a semicolon](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#add-a-new-line-at-the-end-of-a-file)
* [Do not add whitespace at start or end of a file or end of a line](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#do-not-add-whitespace-at-start-or-end-of-a-file-or-end-of-a-line)
* [Indentation](https://github.com/RSS-Bridge/rss-bridge/wiki/Indentation)
* [Use tabs for indentation](https://github.com/RSS-Bridge/rss-bridge/wiki/Indentation#use-tabs-for-indentation)
* [Maximum line length](https://github.com/RSS-Bridge/rss-bridge/wiki/Maximum-line-length)
* [The maximum line length should not exceed 80 characters](https://github.com/RSS-Bridge/rss-bridge/wiki/Maximum-line-length#the-maximum-line-length-should-not-exceed-80-characters)
* [Strings](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings)
* [Whenever possible use single quoted strings](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#whenever-possible-use-single-quote-strings)
* [Add spaces around the concatenation operator](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#add-spaces-around-the-concatenation-operator)
* [Use a single string instead of concatenating](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#use-a-single-string-instead-of-concatenating)
* [Constants](https://github.com/RSS-Bridge/rss-bridge/wiki/Constants)
* [Use UPPERCASE for constants](https://github.com/RSS-Bridge/rss-bridge/wiki/Constants#use-uppercase-for-constants)
* [Keywords](https://github.com/RSS-Bridge/rss-bridge/wiki/Keywords)
* [Use lowercase for `true`, `false` and `null`](https://github.com/RSS-Bridge/rss-bridge/wiki/Keywords#use-lowercase-for-true-false-and-null)
* [Operators](https://github.com/RSS-Bridge/rss-bridge/wiki/Operators)
* [Operators must have a space around them](https://github.com/RSS-Bridge/rss-bridge/wiki/Operators#operators-must-have-a-space-around-them)
* [Functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions)
* [Parameters with default values must appear last in functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#parameters-with-default-values-must-appear-last-in-functions)
* [Calling functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#calling-functions)
* [Do not add spaces after opening or before closing bracket](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#do-not-add-spaces-after-opening-or-before-closing-bracket)
* [Structures](https://github.com/RSS-Bridge/rss-bridge/wiki/Structures)
* [Structures must always be formatted as multi-line blocks](https://github.com/RSS-Bridge/rss-bridge/wiki/Structures#structures-must-always-be-formatted-as-multi-line-blocks)
* [If-Statement](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement)
* [Use `elseif` instead of `else if`](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#use-elseif-instead-of-else-if)
* [Do not write empty statements](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#do-not-write-empty-statements)
* [Do not write unconditional if-statements](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#do-not-write-unconditional-if-statements)
* [Classes](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes)
* [Use PascalCase for class names](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#use-pascalcase-for-class-names)
* [Do not use final statements inside final classes](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#do-not-use-final-statements-inside-final-classes)
* [Do not override methods to call their parent](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#do-not-override-methods-to-call-their-parent)
* [abstract and final declarations MUST precede the visibility declaration](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#abstract-and-final-declarations-must-precede-the-visibility-declaration)
* [static declaration MUST come after the visibility declaration](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#static-declaration-must-come-after-the-visibility-declaration)
* [Casting](https://github.com/RSS-Bridge/rss-bridge/wiki/Casting)
* [Do not add spaces when casting](https://github.com/RSS-Bridge/rss-bridge/wiki/Casting#do-not-add-spaces-when-casting)

View File

@@ -0,0 +1,61 @@
---
name: Bridge request template
about: Use this template for requesting a new bridge
---
# Bridge request
<!--
This is a bridge request. Start by adding a descriptive title (i.e. `Bridge request for GitHub`). Use the "Preview" button to see a preview of your request. Make sure your request is complete before submitting!
Notice: This comment is only visible to you while you work on your request. Please do not remove any of the lines in the template (you may add your own outside the "<!--" and "- ->" lines!)
-->
## General information
<!--
Please describe what you expect from the bridge. Whenever possible provide sample links and screenshots (you can just paste them here) to express your expectations and help others understand your request. If possible, mark relevant areas in your screenshot. Use the following questions for reference:
-->
- _Host URI for the bridge_ (i.e. `https://github.com`):
- Which information would you like to see?
- How should the information be displayed/formatted?
- Which of the following parameters do you expect?
- [X] Title
- [X] URI (link to the original article)
- [ ] Author
- [ ] Timestamp
- [X] Content (the content of the article)
- [ ] Enclosures (pictures, videos, etc...)
- [ ] Categories (categories, tags, etc...)
## Options
<!--Select options from the list below. Add your own option if one is missing:-->
- [ ] Limit number of returned items
- _Default limit_: 5
- [ ] Load full articles
- _Cache articles_ (articles are stored in a local cache on first request): yes
- _Cache timeout_ (max = 24 hours): 24 hours
- [X] Balance requests (RSS-Bridge uses cached versions to reduce bandwith usage)
- _Timeout_ (default = 5 minutes, max = 24 hours): 5 minutes
<!--Be aware that some options might not be available for your specific request due to technical limitations!-->
<!--
## Additional notes
Keep in mind that opening a request does not guarantee the bridge being implemented! That depends entirely on the interest and time of others to make the bridge for you.
You can also implement your own bridge (with support of the community if needed). Find more information in the [RSS-Bridge Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/For-developers) developer section.
-->

View File

@@ -1,28 +1,36 @@
dist: trusty
sudo: false
language: php
install:
- if [[ $TRAVIS_PHP_VERSION == "hhvm" ]]; then
composer global require squizlabs/PHP_CodeSniffer;
else
pear channel-update pear.php.net;
pear install PHP_CodeSniffer;
fi
- composer global require dealerdirect/phpcodesniffer-composer-installer;
- composer global require phpcompatibility/php-compatibility;
# Use PHPUnit 6 for unit tests (stable), requires PHP 7
- if [[ $TRAVIS_PHP_VERSION == "7.0" ]]; then
composer global require phpunit/phpunit ^6;
fi
# Use latest PHPUnit on nightly to detect breaking changes
- if [[ $TRAVIS_PHP_VERSION == "nightly" ]]; then
composer global require phpunit/phpunit;
fi
script:
- phpenv rehash
- if [[ $TRAVIS_PHP_VERSION == "hhvm" ]]; then
/home/travis/.composer/vendor/bin/phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
else
phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
# Run PHP_CodeSniffer on all versions
- ~/.config/composer/vendor/bin/phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
# Check PHP compatibility for the lowest supported version
- if [[ $TRAVIS_PHP_VERSION == "5.6" ]]; then
~/.config/composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --warning-severity=0 --extensions=php -p;
fi
# Run unit tests (stable)
- if [[ $TRAVIS_PHP_VERSION == "7.0" ]]; then
phpunit --configuration=phpunit.xml --include-path=lib/;
fi
# Run unit tests (latest/nightly)
# Check PHP compatibility for all versions, starting at the lowest supported version in order to detect breaking changes
- if [[ $TRAVIS_PHP_VERSION == "nightly" ]]; then
phpunit --configuration=phpunit.xml --include-path=lib/;
~/.config/composer/vendor/bin/phpcs . --standard=PHPCompatibility --warning-severity=0 --extensions=php -p --runtime-set testVersion 5.6-;
fi
matrix:
fast_finish: true
@@ -30,9 +38,7 @@ matrix:
include:
- php: 5.6
- php: 7.0
- php: hhvm
- php: nightly
allow_failures:
- php: hhvm
- php: nightly

View File

@@ -1,263 +0,0 @@
rss-bridge Changelog
===
RSS-Bridge 2017-08-19
==
## General changes
* whitelist: Do case-insensitive whitelist matching
* [FeedExpander] Fix Serialization of 'SimpleXMLElement' is not allowed
* [FeedExpander] Remove whitespace from source content
* [index] Add GET parameter 'q' for search queries
- **Example**: You can now add `&q=Twitter` to load into the search field
* [index] Check permissions for cache folder and whitelist file
* [index] Show bridge options when loading with URL fragment
- **Example**: You can now add `#bridge-Twitter` to load the card with all
parameters visible
* [style] Center search cursor and hide placeholder
* [validation] Fix error on undefined optional numeric value
## Modified bridges
* [DanbooruBridge] Allow descendant classes to override tag collection
* [DribbbleBridge] Add dribble bridge listing last dribble popular shots (#558)
* [FacebookBridge] Fix &amp; in URLs
* [GelbooruBridge] Fix bridge not getting tags correctly
* [GoComicsBridge] Fix for page structure changes (#568)
* [LeBonCoinBridge] Fix bridge is marked executable
* [LWNprevBridge] Fix everchanging url
* [YoutubeBridge] Fix error on certain keywords
* [YoutubeBridge] Fix issues loading playlists
## Removed bridges
* VineBridge
RSS-Bridge 2017-08-03
==
## Important changes
* RSS-Bridge now has [contribution guidelines](CONTRIBUTING.md)
* [phpcs rules](phpcs.xml) follow the [contribution guidelines](CONTRIBUTING.md)
## General changes
* Added a search bar to make searching for bridges easier
* Added user friendly error page for when a bridge fails
* Added caching of extraInfos (name, uri)
* Added an indicator to warn for bridges using HTTP instead of HTTPS
* Various bug fixes and improvements
## Modified bridges
* AllocineFRBridge] Update Faux Raccord link
* [DanbooruBridge] Fix broken URI
* [DuckDuckGoBridge] Disable DuckDuckGo redirects so that the links returned are correct.
* [FacebookBridge] Add option to hide posts with facebook videos
* [FacebookBridge] Add requester languages to HTTP header
* [FacebookBridge] Handle summary posts
* [FacebookBridge] Replace 'novideo' with 'media_type'
* [FilterBridge] Initial implementation of basic title permit and block
* [FlickrTagBridge] Fix and improve bridge by using the FlickrExploreBridge approach
* [GooglePlusPostBridge] Autofix user names
* [GooglePlusPostBridge] Fix bridge implementation
* [GooglePlusPostBridge] Fix content loading
* [InstagramBridge] Add option to filter for videos and pictures
* [LWNprevBridge] full rewrite
* [MangareaderBridge] Fix double forward slashes
* [NasaApodBridge] Use HTTPS instead of HTTP
* [PinterestBridge] Fix checkbox not working
* [PinterestBridge] Fix implementation after DOM changes
* [RTBFBridge] Update URI
* [SexactuBridge] Fix URI and timestamp
* [SexactuBridge] Use most modern version of bridge api and cached pages (#504)
* [ShanaprojectBridge] Don't throw error if timestamp is missing
* [TwitterBridge] Add option to hide retweets
* [TwitterBridge] Avoid empty content caused by new login policy
* [TwitterBridge] Fix double slashes in URI
* [TwitterBridge] Fix missing spaces
* [TwitterBridge] Fix title includes anchors in plaintext format
* [TwitterBridge] ignore promoted tweets
* [TwitterBridge] Optimize returned image sizes
* [TwitterBridge] Show quotes and pictures
* [WebfailBridge] Properly handle gifs (DOM changed)
* [YoutubeBridge] Improve readability of feed contents
* [YoutubeBridge] Improve URL handling in video descriptions
## New bridges
* AmazonBridge
* DiceBridge
* EtsyBridge
* FB2Bridge
* FilterBridge
* FlickrBridge
* GithubSearchBridge
* GoComicsBridge
* KATBridge
* KernelBugTrackerBridge
* MixCloudBridge
* MoinMoinBridge
* RainbowSixSiegeBridge
* SteamBridge
* TheTVDBBridge
* Torrent9Bridge
* UsbekEtRicaBridge
* WikiLeaksBridge
* WordPressPluginUpdateBridge
Alpha 0.2
===
## Important changes
* RSS-Bridge has been [UNLICENSED](UNLICENSE)
* RSS-Bridge is now a community-managed project on [GitHub](https://github.com/rss-bridge/rss-bridge)
* RSS-Bridge now has a [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
* RSS-Bridge now supports [Travis-CI](https://travis-ci.org)
## General changes
* Added [CHANGELOG](CHANGELOG.md) (this file)
* Added [PHP Simple HTML DOM Parser](http://simplehtmldom.sourceforge.net) to [vendor](vendor/simplehtmldom/)
* Added cache purging function (cache will be force-purged after 24 hours or as defined by bridge)
* Added new format [MrssFormat](formats/MrssFormat.php)
* Added parameter `author` - for display of the feed author name - to all formats
* Added new abstraction of the BridgeInterface:
- [FeedExpander](https://github.com/RSS-Bridge/rss-bridge/wiki/Bridge-API)
* Added optional support for proxy usage on each individual bridge
* Added support for [custom bridge parameter](https://github.com/RSS-Bridge/rss-bridge/wiki/BridgeAbstract#format-specifications) (text, number, list, checkbox)
* Changed design of the welcome screen
* Changed design of HtmlFormat
* Changed behavior of debug mode:
- Enable debug mode by placing a file called "DEBUG" in the root folder
- Debug mode automatically disables cache file loading
* Changed implementation of bridges - see [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
- Changed comment-style metadata to constants
- Added support for multiple utilizations per bridge
- Changed the parameter loading algorithm to be loaded by RSS-Bridge core
* Improved checks for PHP version, configuration and extensions
* Many bug fixes
## Modified Bridges
* FlickrExploreBridge
* GoogleSearchBridge
* TwitterBridge
## New Bridges
* ABCTabsBridge
* AcrimedBridge
* AllocineFRBridge
* AnimeUltimeBridge
* Arte7Bridge
* AskfmBridge
* BandcampBridge
* BastaBridge
* BlaguesDeMerdeBridge
* BooruprojectBridge
* CADBridge
* CNETBridge
* CastorusBridge
* CollegeDeFranceBridge
* CommonDreamsBridge
* CopieDoubleBridge
* CourrierInternationalBridge
* CpasbienBridge
* CryptomeBridge
* DailymotionBridge
* DanbooruBridge
* DansTonChatBridge
* DauphineLibereBridge
* DemoBridge
* DeveloppezDotComBridge
* DilbertBridge
* DollbooruBridge
* DuckDuckGoBridge
* EZTVBridge
* EliteDangerousGalnetBridge
* ElsevierBridge
* EstCeQuonMetEnProdBridge
* FacebookBridge
* FierPandaBridge
* FlickrTagBridge
* FootitoBridge
* FourchanBridge
* FuturaSciencesBridge
* GBAtempBridge
* GelbooruBridge
* GiphyBridge
* GithubIssueBridge
* GizmodoBridge
* GooglePlusPostBridge
* HDWallpapersBridge
* HentaiHavenBridge
* IdenticaBridge
* InstagramBridge
* IsoHuntBridge
* JapanExpoBridge
* KonachanBridge
* KoreusBridge
* KununuBridge
* LWNprevBridge
* LeBonCoinBridge
* LegifranceJOBridge
* LeMondeInformatiqueBridge
* LesJoiesDuCodeBridge
* LichessBridge
* LinkedInCompanyBridge
* LolibooruBridge
* MangareaderBridge
* MilbooruBridge
* MoebooruBridge
* MondeDiploBridge
* MsnMondeBridge
* MspabooruBridge
* NasaApodBridge
* NeuviemeArtBridge
* NextInpactBridge
* NextgovBridge
* NiceMatinBridge
* NovelUpdatesBridge
* OpenClassroomsBridge
* ParuVenduImmoBridge
* PickyWallpapersBridge
* PinterestBridge
* PlanetLibreBridge
* RTBFBridge
* ReadComicsBridge
* Releases3DSBridge
* ReporterreBridge
* Rue89Bridge
* Rule34Bridge
* Rule34pahealBridge
* SafebooruBridge
* SakugabooruBridge
* ScmbBridge
* ScoopItBridge
* SensCritiqueBridge
* SexactuBridge
* ShanaprojectBridge
* Shimmie2Bridge
* SoundcloudBridge
* StripeAPIChangeLogBridge
* SuperbWallpapersBridge
* T411Bridge
* TagBoardBridge
* TbibBridge
* TheCodingLoveBridge
* TheHackerNewsBridge
* ThePirateBayBridge
* UnsplashBridge
* ViadeoCompanyBridge
* VineBridge
* VkBridge
* WallpaperStopBridge
* WebfailBridge
* WeLiveSecurityBridge
* WhydBridge
* WikipediaBridge
* WordPressBridge
* WorldOfTanksBridge
* XbooruBridge
* YandereBridge
* YoutubeBridge
* ZDNetBridge
Alpha 0.1
===
* First tagged version.
* Includes refactoring.
* Unstable.

View File

@@ -1,47 +0,0 @@
### Pull request policy
Fix one issue per pull request.
Squash commits before opening a pull request.
Respect the coding style policy.
Name your PR like the following :
* When correcting a single bridge, use `[BridgeName] Feature`.
* When fixing a problem in a specific file, use `[FileName] Feature`.
* When fixing a general problem, use `category : feature`.
Note that all pull-requests should pass the unit tests before they can be merged.
### Coding style
Use `camelCase` for variables and methods.
Use `UPPERCASE` for constants.
Use `PascalCase` for class names. When creating a bridge, your class and PHP file should be named `MyImplementationBridge`.
Use tabs for indentation.
Add an empty line at the end of your file.
Use `''` to encapsulate strings, including in arrays.
Prefer lines shorter than 80 chars, no line longer than 120 chars.
PHP constants should be in lower case (`true, false, null`...)
* Add spaces between the logical operator and your expressions (not needed for the `!` operator).
* Use `||` and `&&` instead of `or` and `and`.
* Add space between your condition and the opening bracket/closing bracket.
* Don't put a space between `if` and your bracket.
* Use `elseif` instead of `else if`.
* Add new lines in your conditions if they are containing more than one line.
* Example :
```PHP
if($a == true && $b) {
print($a);
} else if(!$b) {
$a = !$a;
$b = $b >> $a;
print($b);
} else {
print($b);
}
```

207
README.md
View File

@@ -1,10 +1,12 @@
rss-bridge
===
[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge) [![Docker Build Status](https://img.shields.io/docker/build/rssbridge/rss-bridge.svg)](https://hub.docker.com/r/rssbridge/rss-bridge/)
[![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/)
rss-bridge is a PHP project capable of generating ATOM feeds for websites which don't have one.
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.
Supported sites/pages (main)
**Important**: RSS-Bridge is __not__ a feed reader or feed aggregator, but a tool to generate feeds that are consumed by feed readers and feed aggregators. Find a list of feed aggregators on [Wikipedia](https://en.wikipedia.org/wiki/Comparison_of_feed_aggregators).
Supported sites/pages (examples)
===
* `Bandcamp` : Returns last release from [bandcamp](https://bandcamp.com/) for a tag
@@ -25,17 +27,20 @@ Supported sites/pages (main)
* `Wikipedia`: highlighted articles from [Wikipedia](https://wikipedia.org/) in English, German, French or Esperanto
* `YouTube` : YouTube user channel, playlist or search
Plus [many other bridges](bridges/) to enable, thanks to the community
And [many more](bridges/), thanks to the community!
Output format
===
Output format can take several forms:
* `Atom` : ATOM Feed, for use in RSS/Feed readers
* `Html` : Simple html page.
* `Json` : Json, for consumption by other applications.
* `Mrss` : MRSS Feed, for use in RSS/Feed readers
* `Plaintext` : raw text (php object, as returned by print_r)
RSS-Bridge is capable of producing several output formats:
* `Atom` : Atom feed, for use in feed readers
* `Html` : Simple HTML page
* `Json` : JSON, for consumption by other applications
* `Mrss` : MRSS feed, for use in feed readers
* `Plaintext` : Raw text, for consumption by other applications
You can extend RSS-Bridge with your own format, using the [Format API](https://github.com/RSS-Bridge/rss-bridge/wiki/Format-API)!
Screenshot
===
@@ -44,88 +49,174 @@ Welcome screen:
![Screenshot](https://github.com/RSS-Bridge/rss-bridge/wiki/images/screenshot_rss-bridge_welcome.png)
RSS-Bridge hashtag (#rss-bridge) search on Twitter, in ATOM format (as displayed by Firefox):
***
RSS-Bridge hashtag (#rss-bridge) search on Twitter, in Atom format (as displayed by Firefox):
![Screenshot](https://github.com/RSS-Bridge/rss-bridge/wiki/images/screenshot_twitterbridge_atom.png)
Requirements
===
* PHP 5.6, e.g. `AddHandler application/x-httpd-php56 .php` in `.htaccess`
* `openssl` extension enabled in PHP config (`php.ini`)
* `curl` extension enabled in PHP config (`php.ini`)
RSS-Bridge requires PHP 5.6 or higher with following extensions enabled:
Enabling/Disabling bridges
- [`openssl`](https://secure.php.net/manual/en/book.openssl.php)
- [`libxml`](https://secure.php.net/manual/en/book.libxml.php)
- [`mbstring`](https://secure.php.net/manual/en/book.mbstring.php)
- [`simplexml`](https://secure.php.net/manual/en/book.simplexml.php)
- [`curl`](https://secure.php.net/manual/en/book.curl.php)
- [`json`](https://secure.php.net/manual/en/book.json.php)
Find more information on our [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
Enable / Disable bridges
===
By default, the script creates `whitelist.txt` and adds the main bridges (see above). `whitelist.txt` is ignored by git, you can edit it:
* to enable extra bridges (one bridge per line)
* to disable main bridges (remove the line)
* to enable all bridges (just one wildcard `*` as file content)
RSS-Bridge allows you to take full control over which bridges are displayed to the user. That way you can host your own RSS-Bridge service with your favorite collection of bridges!
New bridges are disabled by default, so make sure to check regularly what's new and whitelist what you want!
Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting)
**Notice**: By default RSS-Bridge will only show a small subset of bridges. Make sure to read up on [whitelisting](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) to unlock the full potential of RSS-Bridge!
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)
Getting involved
===
There are many ways for you to getting involved with RSS-Bridge. Here are a few things:
- Share RSS-Bridge with your friends (Twitter, Facebook, ..._you name it_...)
- Report broken bridges or bugs by opening [Issues](https://github.com/RSS-Bridge/rss-bridge/issues) on GitHub
- Request new features or suggest ideas (via [Issues](https://github.com/RSS-Bridge/rss-bridge/issues))
- Discuss bugs, features, ideas or [issues](https://github.com/RSS-Bridge/rss-bridge/issues)
- Add new bridges or improve the API
- Improve the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki)
- Host an instance of RSS-Bridge for your personal use or make it available to the community :sparkling_heart:
Authors
===
We are RSS Bridge Community, a group of developers continuing the project initiated by sebsauvage, webmaster of [sebsauvage.net](http://sebsauvage.net), author of [Shaarli](http://sebsauvage.net/wiki/doku.php?id=php:shaarli) and [ZeroBin](http://sebsauvage.net/wiki/doku.php?id=php:zerobin).
Patch/contributors :
We are RSS-Bridge community, a group of developers continuing the project initiated by sebsauvage, webmaster of [sebsauvage.net](http://sebsauvage.net), author of [Shaarli](http://sebsauvage.net/wiki/doku.php?id=php:shaarli) and [ZeroBin](http://sebsauvage.net/wiki/doku.php?id=php:zerobin).
**Contributors** (sorted alphabetically):
<!--
Use this script to generate the list automatically (using the GitHub API):
https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
-->
* Yves ASTIER ([Draeli](https://github.com/Draeli)) : PHP optimizations, fixes, dynamic brigde/format list with all stuff behind and extend cache system. Mail : contact /at\ yves-astier.com
* [Mitsukarenai](https://github.com/Mitsukarenai) : Initial inspiration, collaborator
* [ArthurHoaro](https://github.com/ArthurHoaro)
* [BoboTiG](https://github.com/BoboTiG)
* [Astalaseven](https://github.com/Astalaseven)
* [qwertygc](https://github.com/qwertygc)
* [Djuuu](https://github.com/Djuuu)
* [Anadrark](https://github.com/Anadrark])
* [Grummfy](https://github.com/Grummfy)
* [Polopollo](https://github.com/Polopollo)
* [16mhz](https://github.com/16mhz)
* [kranack](https://github.com/kranack)
* [logmanoriginal](https://github.com/logmanoriginal)
* [polo2ro](https://github.com/polo2ro)
* [Riduidel](https://github.com/Riduidel)
* [superbaillot.net](http://superbaillot.net/)
* [vinzv](https://github.com/vinzv)
* [teromene](https://github.com/teromene)
* [nel50n](https://github.com/nel50n)
* [nyutag](https://github.com/nyutag)
* [Ahiles3005](https://github.com/Ahiles3005)
* [Albirew](https://github.com/Albirew)
* [AmauryCarrade](https://github.com/AmauryCarrade)
* [AntoineTurmel](https://github.com/AntoineTurmel)
* [ArthurHoaro](https://github.com/ArthurHoaro)
* [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42)
* [Daiyousei](https://github.com/Daiyousei)
* [Djuuu](https://github.com/Djuuu)
* [Draeli](https://github.com/Draeli)
* [EtienneM](https://github.com/EtienneM)
* [Frenzie](https://github.com/Frenzie)
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [Glandos](https://github.com/Glandos)
* [GregThib](https://github.com/GregThib)
* [Grummfy](https://github.com/Grummfy)
* [JackNUMBER](https://github.com/JackNUMBER)
* [JeremyRand](https://github.com/JeremyRand)
* [Jocker666z](https://github.com/Jocker666z)
* [LogMANOriginal](https://github.com/LogMANOriginal)
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
* [Nono-m0le](https://github.com/Nono-m0le)
* [ORelio](https://github.com/ORelio)
* [Pitchoule](https://github.com/Pitchoule)
* [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
* [PaulVayssiere](https://github.com/PaulVayssiere)
* [Piranhaplant](https://github.com/Piranhaplant)
* [Riduidel](https://github.com/Riduidel)
* [Roliga](https://github.com/Roliga)
* [Strubbl](https://github.com/Strubbl)
* [TheRadialActive](https://github.com/TheRadialActive)
* [TwizzyDizzy](https://github.com/TwizzyDizzy)
* [WalterBarrett](https://github.com/WalterBarrett)
* [ZeNairolf](https://github.com/ZeNairolf)
* [adamchainz](https://github.com/adamchainz)
* [aledeg](https://github.com/aledeg)
* [alexAubin](https://github.com/alexAubin)
* [az5he6ch](https://github.com/az5he6ch)
* [b1nj](https://github.com/b1nj)
* [benasse](https://github.com/benasse)
* [captn3m0](https://github.com/captn3m0)
* [chemel](https://github.com/chemel)
* [ckiw](https://github.com/ckiw)
* [cnlpete](https://github.com/cnlpete)
* [corenting](https://github.com/corenting)
* [Daiyousei](https://github.com/Daiyousei)
* [erwang](https://github.com/erwang)
* [gsurrel](https://github.com/gsurrel)
* [couraudt](https://github.com/couraudt)
* [da2x](https://github.com/da2x)
* [disk0x](https://github.com/disk0x)
* [eMerzh](https://github.com/eMerzh)
* [em92](https://github.com/em92)
* [fluffy-critter](https://github.com/fluffy-critter)
* [griffaurel](https://github.com/griffaurel)
* [hunhejj](https://github.com/hunhejj)
* [j0k3r](https://github.com/j0k3r)
* [jdigilio](https://github.com/jdigilio)
* [kranack](https://github.com/kranack)
* [kraoc](https://github.com/kraoc)
* [laBecasse](https://github.com/laBecasse)
* [lagaisse](https://github.com/lagaisse)
* [az5he6ch](https://github.com/az5he6ch)
* [niawag](https://github.com/niawag)
* [JeremyRand](https://github.com/JeremyRand)
* [lalannev](https://github.com/lalannev)
* [ldidry](https://github.com/ldidry)
* [m0zes](https://github.com/m0zes)
* [matthewseal](https://github.com/matthewseal)
* [mcbyte-it](https://github.com/mcbyte-it)
* [mdemoss](https://github.com/mdemoss)
* [melangue](https://github.com/melangue)
* [metaMMA](https://github.com/metaMMA)
* [mickael-bertrand](https://github.com/mickael-bertrand)
* [mitsukarenai](https://github.com/mitsukarenai)
* [mr-flibble](https://github.com/mr-flibble)
* [mro](https://github.com/mro)
* [mxmehl](https://github.com/mxmehl)
* [nel50n](https://github.com/nel50n)
* [niawag](https://github.com/niawag)
* [pellaeon](https://github.com/pellaeon)
* [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
* [pitchoule](https://github.com/pitchoule)
* [pmaziere](https://github.com/pmaziere)
* [prysme01](https://github.com/prysme01)
* [quentinus95](https://github.com/quentinus95)
* [qwertygc](https://github.com/qwertygc)
* [regisenguehard](https://github.com/regisenguehard)
* [rogerdc](https://github.com/rogerdc)
* [sebsauvage](https://github.com/sebsauvage)
* [sublimz](https://github.com/sublimz)
* [sysadminstory](https://github.com/sysadminstory)
* [tameroski](https://github.com/tameroski)
* [teromene](https://github.com/teromene)
* [triatic](https://github.com/triatic)
* [wtuuju](https://github.com/wtuuju)
* [yardenac](https://github.com/yardenac)
Licenses
===
Code is [Public Domain](UNLICENSE).
Including `PHP Simple HTML DOM Parser` under the [MIT License](http://opensource.org/licenses/MIT)
The source code for RSS-Bridge is [Public Domain](UNLICENSE).
RSS-Bridge uses third party libraries with their own license:
* [`PHP Simple HTML DOM Parser`](http://simplehtmldom.sourceforge.net/) licensed under the [MIT License](http://opensource.org/licenses/MIT)
* [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](http://opensource.org/licenses/MIT)
Technical notes
===
* There is a cache so that source services won't ban you even if you hammer the rss-bridge with requests. Each bridge can have a different duration for the cache. The `cache` subdirectory will be automatically created and cached objects older than 24 hours get purged.
* To implement a new Bridge, [follow the specifications](https://github.com/RSS-Bridge/rss-bridge/wiki/Bridge-API) and take a look at existing Bridges for examples.
* To enable debug mode (disabling cache and enabling error reporting), create an empty file named `DEBUG` in the root directory (next to `index.php`).
* For more information refer to the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki)
* RSS-Bridge uses caching to prevent services from banning your server for repeatedly updating feeds. The specific cache duration can be different between bridges. Cached files are deleted automatically after 24 hours.
* You can implement your own bridge, [following these instructions](https://github.com/RSS-Bridge/rss-bridge/wiki/Bridge-API).
* You can enable debug mode to disable caching. Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Debug-mode)
Rant
===
@@ -134,10 +225,10 @@ Rant
Your catchword is "share", but you don't want us to share. You want to keep us within your walled gardens. That's why you've been removing RSS links from webpages, hiding them deep on your website, or removed feeds entirely, replacing it with crippled or demented proprietary API. **FUCK YOU.**
You're not social when you hamper sharing by removing feeds. You're happy to have customers creating content for your ecosystem, but you don't want this content out - a content you do not even own. Google Takeout is just a gimmick. We want our data to flow, we want RSS or ATOM feeds.
You're not social when you hamper sharing by removing feeds. You're happy to have customers creating content for your ecosystem, but you don't want this content out - a content you do not even own. Google Takeout is just a gimmick. We want our data to flow, we want RSS or Atom feeds.
We want to share with friends, using open protocols: RSS, ATOM, XMPP, whatever. Because no one wants to have *your* service with *your* applications using *your* API force-feeding them. Friends must be free to choose whatever software and service they want.
We want to share with friends, using open protocols: RSS, Atom, XMPP, whatever. Because no one wants to have *your* service with *your* applications using *your* API force-feeding them. Friends must be free to choose whatever software and service they want.
We are rebuilding bridges you have wilfully destroyed.
Get your shit together: Put RSS/ATOM back in.
Get your shit together: Put RSS/Atom back in.

View File

@@ -28,6 +28,13 @@ class Arte7Bridge extends BridgeAbstract {
)
)
),
'Collection (Français)' => array(
'colfr' => array(
'name' => 'Collection id',
'required' => true,
'title' => 'ex. RC-014095 pour https://www.arte.tv/fr/videos/RC-014095/blow-up/'
)
),
'Catégorie (Allemand)' => array(
'catde' => array(
'type' => 'list',
@@ -45,6 +52,13 @@ class Arte7Bridge extends BridgeAbstract {
'Sonstiges' => 'AUT'
)
)
),
'Collection (Allemand)' => array(
'colde' => array(
'name' => 'Collection id',
'required' => true,
'title' => 'ex. RC-014095 pour https://www.arte.tv/de/videos/RC-014095/blow-up/'
)
)
);
@@ -54,15 +68,24 @@ class Arte7Bridge extends BridgeAbstract {
$category = $this->getInput('catfr');
$lang = 'fr';
break;
case 'Collection (Français)':
$lang = 'fr';
$collectionId = $this->getInput('colfr');
break;
case 'Catégorie (Allemand)':
$category = $this->getInput('catde');
$lang = 'de';
break;
case 'Collection (Allemand)':
$lang = 'de';
$collectionId = $this->getInput('colde');
break;
}
$url = 'https://api.arte.tv/api/opa/v3/videos?sort=-lastModified&limit=10&language='
. $lang
. ($category != null ? '&category.code=' . $category : '');
. ($category != null ? '&category.code=' . $category : '')
. ($collectionId != null ? '&collections.collectionId=' . $collectionId : '');
$header = array(
'Authorization: Bearer ' . self::API_TOKEN

View File

@@ -1,7 +1,7 @@
<?php
class AskfmBridge extends BridgeAbstract {
const MAINTAINER = 'az5he6ch';
const MAINTAINER = 'az5he6ch, logmanoriginal';
const NAME = 'Ask.fm Answers';
const URI = 'https://ask.fm/';
const CACHE_TIMEOUT = 300; //5 min
@@ -19,39 +19,39 @@ class AskfmBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Requested username can\'t be found.');
foreach($html->find('div.streamItem-answer') as $element) {
$html = defaultLinkTo($html, self::URI);
foreach($html->find('article.streamItem-answer') as $element) {
$item = array();
$item['uri'] = self::URI . $element->find('a.streamItemsAge', 0)->href;
$question = trim($element->find('h1.streamItemContent-question', 0)->innertext);
$item['uri'] = $element->find('a.streamItem_meta', 0)->href;
$question = trim($element->find('header.streamItem_header', 0)->innertext);
$item['title'] = trim(
htmlspecialchars_decode($element->find('h1.streamItemContent-question', 0)->plaintext,
htmlspecialchars_decode($element->find('header.streamItem_header', 0)->plaintext,
ENT_QUOTES
)
);
$answer = trim($element->find('p.streamItemContent-answer', 0)->innertext);
$item['timestamp'] = strtotime($element->find('time', 0)->datetime);
// Doesn't work, DOM parser doesn't seem to like data-hint, dunno why
#$item['update'] = $element->find('a.streamitemsage',0)->data-hint;
$answer = trim($element->find('div.streamItem_content', 0)->innertext);
// This probably should be cleaned up, especially for YouTube embeds
$visual = $element->find('div.streamItemContent-visual', 0)->innertext;
if($visual = $element->find('div.streamItem_visual', 0)) {
$visual = $visual->innertext;
}
// Fix tracking links, also doesn't work
foreach($element->find('a') as $link) {
if(strpos($link->href, 'l.ask.fm') !== false) {
// Too slow
#$link->href = str_replace('#_=_', '', get_headers($link->href, 1)['Location']);
$link->href = $link->plaintext;
}
}
$content = '<p>' . $question . '</p><p>' . $answer . '</p><p>' . $visual . '</p>';
// Fix relative links without breaking // scheme used by YouTube stuff
$content = preg_replace('#href="\/(?!\/)#', 'href="' . self::URI, $content);
$item['content'] = $content;
$item['content'] = '<p>' . $question
. '</p><p>' . $answer
. '</p><p>' . $visual . '</p>';
$this->items[] = $item;
}
}
@@ -66,7 +66,7 @@ class AskfmBridge extends BridgeAbstract {
public function getURI(){
if(!is_null($this->getInput('u'))) {
return self::URI . urlencode($this->getInput('u')) . '/answers/more?page=0';
return self::URI . urlencode($this->getInput('u'));
}
return parent::getURI();

65
bridges/AutoJMBridge.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
class AutoJMBridge extends BridgeAbstract {
const NAME = 'AutoJM';
const URI = 'http://www.autojm.fr/';
const DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
'Afficher les offres de véhicules disponible en fonction des critères du site AutoJM' => array(
'url' => array(
'name' => 'URL de la recherche',
'type' => 'text',
'required' => true,
'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/',
'exampleValue' => 'gammes/index/398?order_by=finition_asc&energie[]=3&transmission[]=2&dispo=all'
)
)
);
const CACHE_TIMEOUT = 3600;
public function getIcon() {
return self::URI . 'assets/images/favicon.ico';
}
public function collectData() {
$html = getSimpleHTMLDOM(self::URI . $this->getInput('url'))
or returnServerError('Could not request AutoJM.');
$list = $html->find('div[class*=ligne_modele]');
foreach($list as $element) {
$image = $element->find('img[class=width-100]', 0)->src;
$serie = $element->find('div[class=serie]', 0)->find('span', 0)->plaintext;
$url = $element->find('div[class=serie]', 0)->find('a[class=btn_ligne color-black]', 0)->href;
if($element->find('div[class*=hasStock-info]', 0) != null) {
$dispo = 'Disponible';
} else {
$dispo = 'Sur commande';
}
$carburant = str_replace('dispo |', '', $element->find('div[class=carburant]', 0)->plaintext);
$transmission = $element->find('div[class*=bv]', 0)->plaintext;
$places = $element->find('div[class*=places]', 0)->plaintext;
$portes = $element->find('div[class*=nb_portes]', 0)->plaintext;
$carosserie = $element->find('div[class*=coloris]', 0)->plaintext;
$remise = $element->find('div[class*=remise]', 0)->plaintext;
$prix = $element->find('div[class*=prixjm]', 0)->plaintext;
$item = array();
$item['uri'] = $url;
$item['title'] = $serie;
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />' . $serie . '</p>';
$item['content'] .= '<ul><li>Disponibilité : ' . $dispo . '</li>';
$item['content'] .= '<li>Carburant : ' . $carburant . '</li>';
$item['content'] .= '<li>Transmission : ' . $transmission . '</li>';
$item['content'] .= '<li>Nombre de places : ' . $places . '</li>';
$item['content'] .= '<li>Nombre de portes : ' . $portes . '</li>';
$item['content'] .= '<li>Série : ' . $serie . '</li>';
$item['content'] .= '<li>Carosserie : ' . $carosserie . '</li>';
$item['content'] .= '<li>Remise : ' . $remise . '</li>';
$item['content'] .= '<li>Prix : ' . $prix . '</li></ul>';
$this->items[] = $item;
}
}
}

265
bridges/BAEBridge.php Normal file
View File

@@ -0,0 +1,265 @@
<?php
class BAEBridge extends BridgeAbstract {
const MAINTAINER = 'couraudt';
const NAME = 'Bourse Aux Equipiers Bridge';
const URI = 'https://www.bourse-aux-equipiers.com';
const DESCRIPTION = 'Returns the newest sailing offers.';
const PARAMETERS = array(
array(
'keyword' => array(
'name' => 'Filtrer par mots clés',
'title' => 'Entrez le mot clé à filtrer ici'
),
'type' => array(
'name' => 'Type de recherche',
'title' => 'Afficher seuleument un certain type d\'annonce',
'type' => 'list',
'values' => array(
'Toutes les annonces' => false,
'Les embarquements' => 'boat',
'Les skippers' => 'skipper',
'Les équipiers' => 'crew'
)
)
)
);
public function collectData() {
$url = $this->getURI();
$html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.');
$annonces = $html->find('main article');
foreach ($annonces as $annonce) {
$detail = $annonce->find('footer a', 0);
$htmlDetail = getSimpleHTMLDOMCached(parent::getURI() . $detail->href);
if (!$htmlDetail)
continue;
$item = array();
$item['title'] = $annonce->find('header h2', 0)->plaintext;
$item['uri'] = parent::getURI() . $detail->href;
$content = $htmlDetail->find('article p', 0)->innertext;
if (!empty($this->getInput('keyword'))) {
$keyword = $this->remove_accents(strtolower($this->getInput('keyword')));
$cleanTitle = $this->remove_accents(strtolower($item['title']));
if (strpos($cleanTitle, $keyword) === false) {
$cleanContent = $this->remove_accents(strtolower($content));
if (strpos($cleanContent, $keyword) === false) {
continue;
}
}
}
$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;
$image = $htmlDetail->find('#zoom', 0);
if ($image) {
$item['enclosures'] = array(parent::getURI() . $image->getAttribute('src'));
}
$this->items[] = $item;
}
}
public function getURI() {
$uri = parent::getURI();
if (!empty($this->getInput('type'))) {
if ($this->getInput('type') == 'boat') {
$uri .= '/embarquements.html';
} elseif ($this->getInput('type') == 'skipper') {
$uri .= '/skippers.html';
} else {
$uri .= '/equipiers.html';
}
}
return $uri;
}
private function remove_accents($string) {
$chars = array(
// Decompositions for Latin-1 Supplement
'ª' => 'a', 'º' => 'o',
'À' => 'A', 'Á' => 'A',
'Â' => 'A', 'Ã' => 'A',
'Ä' => 'A', 'Å' => 'A',
'Æ' => 'AE', 'Ç' => 'C',
'È' => 'E', 'É' => 'E',
'Ê' => 'E', 'Ë' => 'E',
'Ì' => 'I', 'Í' => 'I',
'Î' => 'I', 'Ï' => 'I',
'Ð' => 'D', 'Ñ' => 'N',
'Ò' => 'O', 'Ó' => 'O',
'Ô' => 'O', 'Õ' => 'O',
'Ö' => 'O', 'Ù' => 'U',
'Ú' => 'U', 'Û' => 'U',
'Ü' => 'U', 'Ý' => 'Y',
'Þ' => 'TH', 'ß' => 's',
'à' => 'a', 'á' => 'a',
'â' => 'a', 'ã' => 'a',
'ä' => 'a', 'å' => 'a',
'æ' => 'ae', 'ç' => 'c',
'è' => 'e', 'é' => 'e',
'ê' => 'e', 'ë' => 'e',
'ì' => 'i', 'í' => 'i',
'î' => 'i', 'ï' => 'i',
'ð' => 'd', 'ñ' => 'n',
'ò' => 'o', 'ó' => 'o',
'ô' => 'o', 'õ' => 'o',
'ö' => 'o', 'ø' => 'o',
'ù' => 'u', 'ú' => 'u',
'û' => 'u', 'ü' => 'u',
'ý' => 'y', 'þ' => 'th',
'ÿ' => 'y', 'Ø' => 'O',
// Decompositions for Latin Extended-A
'Ā' => 'A', 'ā' => 'a',
'Ă' => 'A', 'ă' => 'a',
'Ą' => 'A', 'ą' => 'a',
'Ć' => 'C', 'ć' => 'c',
'Ĉ' => 'C', 'ĉ' => 'c',
'Ċ' => 'C', 'ċ' => 'c',
'Č' => 'C', 'č' => 'c',
'Ď' => 'D', 'ď' => 'd',
'Đ' => 'D', 'đ' => 'd',
'Ē' => 'E', 'ē' => 'e',
'Ĕ' => 'E', 'ĕ' => 'e',
'Ė' => 'E', 'ė' => 'e',
'Ę' => 'E', 'ę' => 'e',
'Ě' => 'E', 'ě' => 'e',
'Ĝ' => 'G', 'ĝ' => 'g',
'Ğ' => 'G', 'ğ' => 'g',
'Ġ' => 'G', 'ġ' => 'g',
'Ģ' => 'G', 'ģ' => 'g',
'Ĥ' => 'H', 'ĥ' => 'h',
'Ħ' => 'H', 'ħ' => 'h',
'Ĩ' => 'I', 'ĩ' => 'i',
'Ī' => 'I', 'ī' => 'i',
'Ĭ' => 'I', 'ĭ' => 'i',
'Į' => 'I', 'į' => 'i',
'İ' => 'I', 'ı' => 'i',
'IJ' => 'IJ', 'ij' => 'ij',
'Ĵ' => 'J', 'ĵ' => 'j',
'Ķ' => 'K', 'ķ' => 'k',
'ĸ' => 'k', 'Ĺ' => 'L',
'ĺ' => 'l', 'Ļ' => 'L',
'ļ' => 'l', 'Ľ' => 'L',
'ľ' => 'l', 'Ŀ' => 'L',
'ŀ' => 'l', 'Ł' => 'L',
'ł' => 'l', 'Ń' => 'N',
'ń' => 'n', 'Ņ' => 'N',
'ņ' => 'n', 'Ň' => 'N',
'ň' => 'n', 'ʼn' => 'n',
'Ŋ' => 'N', 'ŋ' => 'n',
'Ō' => 'O', 'ō' => 'o',
'Ŏ' => 'O', 'ŏ' => 'o',
'Ő' => 'O', 'ő' => 'o',
'Œ' => 'OE', 'œ' => 'oe',
'Ŕ' => 'R', 'ŕ' => 'r',
'Ŗ' => 'R', 'ŗ' => 'r',
'Ř' => 'R', 'ř' => 'r',
'Ś' => 'S', 'ś' => 's',
'Ŝ' => 'S', 'ŝ' => 's',
'Ş' => 'S', 'ş' => 's',
'Š' => 'S', 'š' => 's',
'Ţ' => 'T', 'ţ' => 't',
'Ť' => 'T', 'ť' => 't',
'Ŧ' => 'T', 'ŧ' => 't',
'Ũ' => 'U', 'ũ' => 'u',
'Ū' => 'U', 'ū' => 'u',
'Ŭ' => 'U', 'ŭ' => 'u',
'Ů' => 'U', 'ů' => 'u',
'Ű' => 'U', 'ű' => 'u',
'Ų' => 'U', 'ų' => 'u',
'Ŵ' => 'W', 'ŵ' => 'w',
'Ŷ' => 'Y', 'ŷ' => 'y',
'Ÿ' => 'Y', 'Ź' => 'Z',
'ź' => 'z', 'Ż' => 'Z',
'ż' => 'z', 'Ž' => 'Z',
'ž' => 'z', 'ſ' => 's',
// Decompositions for Latin Extended-B
'Ș' => 'S', 'ș' => 's',
'Ț' => 'T', 'ț' => 't',
// Euro Sign
'€' => 'E',
// GBP (Pound) Sign
'£' => '',
// Vowels with diacritic (Vietnamese)
// unmarked
'Ơ' => 'O', 'ơ' => 'o',
'Ư' => 'U', 'ư' => 'u',
// grave accent
'Ầ' => 'A', 'ầ' => 'a',
'Ằ' => 'A', 'ằ' => 'a',
'Ề' => 'E', 'ề' => 'e',
'Ồ' => 'O', 'ồ' => 'o',
'Ờ' => 'O', 'ờ' => 'o',
'Ừ' => 'U', 'ừ' => 'u',
'Ỳ' => 'Y', 'ỳ' => 'y',
// hook
'Ả' => 'A', 'ả' => 'a',
'Ẩ' => 'A', 'ẩ' => 'a',
'Ẳ' => 'A', 'ẳ' => 'a',
'Ẻ' => 'E', 'ẻ' => 'e',
'Ể' => 'E', 'ể' => 'e',
'Ỉ' => 'I', 'ỉ' => 'i',
'Ỏ' => 'O', 'ỏ' => 'o',
'Ổ' => 'O', 'ổ' => 'o',
'Ở' => 'O', 'ở' => 'o',
'Ủ' => 'U', 'ủ' => 'u',
'Ử' => 'U', 'ử' => 'u',
'Ỷ' => 'Y', 'ỷ' => 'y',
// tilde
'Ẫ' => 'A', 'ẫ' => 'a',
'Ẵ' => 'A', 'ẵ' => 'a',
'Ẽ' => 'E', 'ẽ' => 'e',
'Ễ' => 'E', 'ễ' => 'e',
'Ỗ' => 'O', 'ỗ' => 'o',
'Ỡ' => 'O', 'ỡ' => 'o',
'Ữ' => 'U', 'ữ' => 'u',
'Ỹ' => 'Y', 'ỹ' => 'y',
// acute accent
'Ấ' => 'A', 'ấ' => 'a',
'Ắ' => 'A', 'ắ' => 'a',
'Ế' => 'E', 'ế' => 'e',
'Ố' => 'O', 'ố' => 'o',
'Ớ' => 'O', 'ớ' => 'o',
'Ứ' => 'U', 'ứ' => 'u',
// dot below
'Ạ' => 'A', 'ạ' => 'a',
'Ậ' => 'A', 'ậ' => 'a',
'Ặ' => 'A', 'ặ' => 'a',
'Ẹ' => 'E', 'ẹ' => 'e',
'Ệ' => 'E', 'ệ' => 'e',
'Ị' => 'I', 'ị' => 'i',
'Ọ' => 'O', 'ọ' => 'o',
'Ộ' => 'O', 'ộ' => 'o',
'Ợ' => 'O', 'ợ' => 'o',
'Ụ' => 'U', 'ụ' => 'u',
'Ự' => 'U', 'ự' => 'u',
'Ỵ' => 'Y', 'ỵ' => 'y',
// Vowels with diacritic (Chinese, Hanyu Pinyin)
'ɑ' => 'a',
// macron
'Ǖ' => 'U', 'ǖ' => 'u',
// acute accent
'Ǘ' => 'U', 'ǘ' => 'u',
// caron
'Ǎ' => 'A', 'ǎ' => 'a',
'Ǐ' => 'I', 'ǐ' => 'i',
'Ǒ' => 'O', 'ǒ' => 'o',
'Ǔ' => 'U', 'ǔ' => 'u',
'Ǚ' => 'U', 'ǚ' => 'u',
// grave accent
'Ǜ' => 'U', 'ǜ' => 'u',
);
$string = strtr($string, $chars);
return $string;
}
}

View File

@@ -14,6 +14,10 @@ class BandcampBridge extends BridgeAbstract {
)
));
public function getIcon() {
return 'https://s4.bcbits.com/img/bc_favicon.ico';
}
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('No results for this query.');

View File

@@ -7,6 +7,10 @@ class BlaguesDeMerdeBridge extends BridgeAbstract {
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Blagues De Merde';
public function getIcon() {
return self::URI . 'assets/img/favicon.ico';
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI)

View File

@@ -31,6 +31,10 @@ class BloombergBridge extends BridgeAbstract
return parent::getName();
}
public function getIcon() {
return 'https://assets.bwbx.io/s3/javelin/public/hub/images/favicon-black-63fe5249d3.png';
}
public function collectData()
{
switch($this->queriedContext) {

View File

@@ -0,0 +1,87 @@
<?php
class BundesbankBridge extends BridgeAbstract {
const PARAM_LANG = 'lang';
const LANG_EN = 'en';
const LANG_DE = 'de';
const NAME = 'Bundesbank Bridge';
const URI = 'https://www.bundesbank.de/';
const DESCRIPTION = 'Returns the latest studies of the Bundesbank (Germany)';
const MAINTAINER = 'logmanoriginal';
const CACHE_TIMEOUT = 86400; // 24 hours
const PARAMETERS = array(
array(
self::PARAM_LANG => array(
'name' => 'Language',
'type' => 'list',
'required' => true,
'defaultValue' => self::LANG_DE,
'values' => array(
'English' => self::LANG_EN,
'Deutsch' => self::LANG_DE
)
)
)
);
public function getIcon() {
return self::URI . 'resource/crblob/1890/a7f48ee0ae35348748121770ba3ca009/mL/favicon-ico-data.ico';
}
public function getURI() {
switch($this->getInput(self::PARAM_LANG)) {
case self::LANG_EN: return self::URI . 'en/publications/reports/studies';
case self::LANG_DE: return self::URI . 'de/publikationen/berichte/studien';
}
return parent::getURI();
}
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('No response for ' . $this->getURI());
$html = defaultLinkTo($html, $this->getURI());
foreach($html->find('ul.resultlist li') as $study) {
$item = array();
$item['uri'] = $study->find('.teasable__link', 0)->href;
// Get title without child elements (i.e. subtitle)
$title = $study->find('.teasable__title div.h2', 0);
foreach($title->children as &$child) {
$child->outertext = '';
}
$item['title'] = $title->innertext;
// Add subtitle to the content if it exists
$item['content'] = '';
if($subtitle = $study->find('.teasable__subtitle', 0)) {
$item['content'] .= '<strong>' . $study->find('.teasable__subtitle', 0)->plaintext . '</strong>';
}
$item['content'] .= '<p>' . $study->find('.teasable__text', 0)->plaintext . '</p>';
$item['timestamp'] = strtotime($study->find('.teasable__date', 0)->plaintext);
// Downloads and older studies don't have images
if($study->find('.teasable__image', 0)) {
$item['enclosures'] = array(
$study->find('.teasable__image img', 0)->src
);
}
$this->items[] = $item;
}
}
}

View File

@@ -7,6 +7,9 @@ class ChristianDailyReporterBridge extends BridgeAbstract {
const DESCRIPTION = 'The Unofficial Christian Daily Reporter RSS';
// const CACHE_TIMEOUT = 86400; // 1 day
public function getIcon() {
return self::URI . 'images/cdrfavicon.png';
}
public function collectData() {
$uri = 'https://www.christiandailyreporter.com/';

View File

@@ -3,7 +3,7 @@ class CommonDreamsBridge extends FeedExpander {
const MAINTAINER = 'nyutag';
const NAME = 'CommonDreams Bridge';
const URI = 'http://www.commondreams.org/';
const URI = 'https://www.commondreams.org/';
const DESCRIPTION = 'Returns the newest articles.';
public function collectData(){

View File

@@ -32,6 +32,10 @@ class ContainerLinuxReleasesBridge extends BridgeAbstract {
return json_decode($json, true);
}
public function getIcon() {
return 'https://coreos.com/assets/ico/favicon.png';
}
public function collectData() {
$data = $this->getReleaseFeed($this->getJsonUri());

227
bridges/CrewbayBridge.php Normal file
View File

@@ -0,0 +1,227 @@
<?php
class CrewbayBridge extends BridgeAbstract {
const MAINTAINER = 'couraudt';
const NAME = 'Crewbay Bridge';
const URI = 'https://www.crewbay.com';
const DESCRIPTION = 'Returns the newest sailing offers.';
const PARAMETERS = array(
array(
'keyword' => array(
'name' => 'Filter by keyword',
'title' => 'Enter the keyword to filter here'
),
'type' => array(
'name' => 'Type of search',
'title' => 'Choose between finding a boat or a crew',
'type' => 'list',
'values' => array(
'Find a boat' => 'boats',
'Find a crew' => 'crew'
)
),
'status' => array(
'name' => 'Status on the boat',
'title' => 'Choose between recreational or professional classified ads',
'type' => 'list',
'values' => array(
'Recreational' => 'recreational',
'Professional' => 'professional'
)
),
'recreational_position' => array(
'name' => 'Recreational position wanted',
'title' => 'Filter by recreational position you wanted aboard',
'required' => false,
'type' => 'list',
'values' => array(
'' => '',
'Amateur Crew' => 'Amateur Crew',
'Friendship' => 'Friendship',
'Competent Crew' => 'Competent Crew',
'Racing' => 'Racing',
'Voluntary work' => 'Voluntary work',
'Mile building' => 'Mile building'
)
),
'professional_position' => array(
'name' => 'Professional position wanted',
'title' => 'Filter by professional position you wanted aboard',
'required' => false,
'type' => 'list',
'values' => array(
'' => '',
'1st Engineer' => '1st Engineer',
'1st Mate' => '1st Mate',
'Beautician' => 'Beautician',
'Bosun' => 'Bosun',
'Captain' => 'Captain',
'Chef' => 'Chef',
'Steward(ess)' => 'Steward(ess)',
'Deckhand' => 'Deckhand',
'Delivery Crew' => 'Delivery Crew',
'Dive Instructor' => 'Dive Instructor',
'Masseur' => 'Masseur',
'Medical Staff' => 'Medical Staff',
'Nanny' => 'Nanny',
'Navigator' => 'Navigator',
'Racing Crew' => 'Racing Crew',
'Teacher' => 'Teacher',
'Electrical Engineer' => 'Electrical Engineer',
'Fitter' => 'Fitter',
'2nd Engineer' => '2nd Engineer',
'3rd Engineer' => '3rd Engineer',
'Lead Deckhand' => 'Lead Deckhand',
'Security Officer' => 'Security Officer',
'O.O.W' => 'O.O.W',
'1st Officer' => '1st Officer',
'2nd Officer' => '2nd Officer',
'3rd Officer' => '3rd Officer',
'Captain/Engineer' => 'Captain/Engineer',
'Hairdresser' => 'Hairdresser',
'Fitness Trainer' => 'Fitness Trainer',
'Laundry' => 'Laundry',
'Solo Steward/ess' => 'Solo Steward/ess',
'Stew/Deck' => 'Stew/Deck',
'2nd Steward/ess' => '2nd Steward/ess',
'3rd Steward/ess' => '3rd Steward/ess',
'Chief Steward/ess' => 'Chief Steward/ess',
'Head Housekeeper' => 'Head Housekeeper',
'Purser' => 'Purser',
'Cook' => 'Cook',
'Cook/Stew' => 'Cook/Stew',
'2nd Chef' => '2nd Chef',
'Head Chef' => 'Head Chef',
'Administrator' => 'Administrator',
'P.A' => 'P.A',
'Villa staff' => 'Villa staff',
'Housekeeping/Stew' => 'Housekeeping/Stew',
'Stew/Beautician' => 'Stew/Beautician',
'Stew/Masseuse' => 'Stew/Masseuse',
'Manager' => 'Manager',
'Sailing instructor' => 'Sailing instructor'
)
)
)
);
public function collectData() {
$url = $this->getURI();
$html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.');
$annonces = $html->find('#SearchResults div.result');
$limit = 0;
foreach ($annonces as $annonce) {
$detail = $annonce->find('.btn--profile', 0);
$htmlDetail = getSimpleHTMLDOMCached($detail->href);
if (!empty($this->getInput('recreational_position')) || !empty($this->getInput('professional_position'))) {
if ($this->getInput('type') == 'boats') {
if ($this->getInput('status') == 'professional') {
$positions = array($annonce->find('.title .position', 0)->plaintext);
} else {
$positions = array(str_replace('Wanted:', '', $annonce->find('.content li', 0)->plaintext));
}
} else {
$list = $htmlDetail->find('.viewer-details .viewer-list');
$positions = explode("\r\n", end($list)->find('span.value', 0)->plaintext);
}
$found = false;
$keyword = $this->getInput('status') == 'professional' ? 'professional_position' : 'recreational_position';
foreach ($positions as $position) {
if (strpos(trim($position), $this->getInput($keyword)) !== false) {
$found = true;
break;
}
}
if (!$found) {
continue;
}
}
$item = array();
if ($this->getInput('type') == 'boats') {
$titleSelector = '.title h2';
} else {
$titleSelector = '.layout__item h2';
}
$userName = $annonce->find('.result--description a', 0)->plaintext;
$annonceTitle = trim($annonce->find($titleSelector, 0)->plaintext);
if (empty($annonceTitle)) {
$item['title'] = $userName;
} else {
$item['title'] = $userName . ' - ' . $annonceTitle;
}
$item['uri'] = $detail->href;
$images = $annonce->find('.avatar img');
$item['enclosures'] = array(end($images)->getAttribute('src'));
$content = $htmlDetail->find('.viewer-intro--info', 0)->innertext;
$sections = $htmlDetail->find('.viewer-container .viewer-section');
foreach ($sections as $section) {
if ($section->find('.viewer-section-title', 0)) {
$class = str_replace('viewer-', '', explode(' ', $section->getAttribute('class'))[0]);
if (!in_array($class, array('apply', 'photos', 'reviews', 'contact', 'experience', 'qa'))) {
// Basic sections
$content .= $section->find('.viewer-section-title h3', 0)->outertext;
$content .= $section->find('.viewer-section-content', 0)->innertext;
}
} else {
// Info section
$content .= $section->find('.viewer-section-content h3', 0)->outertext;
$content .= $section->find('.viewer-section-content p', 0)->outertext;
}
}
if (!empty($this->getInput('keyword'))) {
$keyword = strtolower($this->getInput('keyword'));
if (strpos(strtolower($item['title']), $keyword) === false) {
if (strpos(strtolower($content), $keyword) === false) {
continue;
}
}
}
$item['content'] = $content;
$tags = $htmlDetail->find('li.viewer-tags--tag');
foreach ($tags as $tag) {
if (!isset($item['categories'])) {
$item['categories'] = array();
}
$text = trim($tag->plaintext);
if (!in_array($text, $item['categories'])) {
$item['categories'][] = $text;
}
}
$this->items[] = $item;
$limit += 1;
if ($limit == 10) break;
}
}
public function getURI() {
$uri = parent::getURI();
if ($this->getInput('type') == 'boats') {
$uri .= '/boats';
} else {
$uri .= '/crew';
}
if ($this->getInput('status') == 'professional') {
$uri .= '/professional';
} else {
$uri .= '/recreational';
}
return $uri;
}
}

View File

@@ -48,6 +48,10 @@ class DailymotionBridge extends BridgeAbstract {
return $metadata;
}
public function getIcon() {
return 'https://static1-ssl.dmcdn.net/images/neon/favicons/android-icon-36x36.png.vf806ca4ed0deed812';
}
public function collectData(){
$html = '';
$limit = 5;

View File

@@ -46,23 +46,914 @@ class DealabsBridge extends PepperBridgeAbstract {
'required' => 'true',
'title' => 'Groupe dont il faut afficher les deals',
'values' => array(
'Abonnements internet' => 'abonnements-internet',
'Accessoires & gadgets' => 'accessoires-gadgets',
'Accessoires photo' => 'accessoires-photo',
'Accessoires vélo' => 'accessoires-velo',
'Acer' => 'acer',
'Adaptateurs' => 'adaptateurs',
'Adhérents Fnac' => 'adherents-fnac',
'adidas' => 'adidas',
'adidas Stan Smith' => 'adidas-stan-smith',
'adidas Superstar' => 'adidas-superstar',
'adidas ZX Flux' => 'adidas-zx-flux',
'Adoucissant' => 'adoucissant',
'Agendas' => 'agendas',
'Age of Empires' => 'age-of-empires',
'Alarmes' => 'alarmes',
'Alimentation & boissons' => 'alimentation-boissons',
'Alimentation PC' => 'alimentation-pc',
'Amazon Echo' => 'amazon-echo',
'Amazon Fire TV' => 'amazon-fire-tv',
'Amazon Kindle' => 'amazon-kindle',
'Amazon Prime' => 'amazon-prime',
'AMD Ryzen' => 'amd-ryzen',
'AMD Vega' => 'amd-vega',
'amiibo' => 'amiibo',
'Amplis' => 'amplis',
'Ampoules' => 'ampoules',
'Animaux' => 'animaux',
'Anker' => 'anker',
'Antivirus' => 'antivirus',
'Antivols' => 'antivols',
'Appareils de musculation' => 'appareils-de-musculation',
'Appareils photo' => 'appareils-photo',
'Apple AirPods' => 'apple-airpods',
'Apple' => 'apple',
'Apple iPad' => 'apple-ipad',
'Apple iPad Mini' => 'apple-ipad-mini',
'Apple iPad Pro' => 'apple-ipad-pro',
'Apple iPhone 6' => 'apple-iphone-6',
'Apple iPhone 7' => 'apple-iphone-7',
'Apple iPhone 8' => 'apple-iphone-8',
'Apple iPhone 8 Plus' => 'apple-iphone-8-plus',
'Apple iPhone' => 'apple-iphone',
'Apple iPhone SE' => 'apple-iphone-se',
'Apple iPhone X' => 'apple-iphone-x',
'Apple MacBook Air' => 'apple-macbook-air',
'Apple MacBook Pro' => 'apple-macbook-pro',
'Apple TV' => 'apple-tv',
'Apple Watch' => 'apple-watch',
'Applications Android' => 'applications-android',
'Applications' => 'applications',
'Applications iOS' => 'applications-ios',
'Applis & logiciels' => 'applis-logiciels',
'Arbres à chat' => 'arbres-a-chat',
'Asmodée' => 'asmodee',
'Aspirateurs' => 'aspirateurs',
'Aspirateurs Dyson' => 'aspirateurs-dyson',
'Aspirateurs robot' => 'aspirateurs-robot',
'Assassin&#039;s Creed' => 'assassin-s-creed',
'Assassin&#039;s Creed Origins' => 'assassin-s-creed-origins',
'Assurances' => 'assurances',
'Asus' => 'asus',
'ASUS Transformer' => 'asus-transformer',
'Asus ZenFone 2' => 'asus-zenfone-2',
'Asus ZenFone 3' => 'asus-zenfone-3',
'Asus ZenFone 4' => 'asus-zenfone-4',
'Asus ZenFone GO' => 'asus-zenfone-go',
'Aukey' => 'aukey',
'Auto' => 'auto',
'Auto-Moto' => 'auto-moto',
'Autoradios' => 'autoradios',
'Baby foot' => 'baby-foot',
'BabyLiss' => 'babyliss',
'Babyphones' => 'babyphones',
'Bagagerie' => 'bagagerie',
'Balançoires' => 'balancoires',
'Bandes dessinées' => 'bandes-dessinees',
'Banques' => 'banques',
'Barbecue' => 'barbecue',
'Barbie' => 'barbie',
'Barres de son' => 'barres-de-son',
'Batteries externes' => 'batteries-externes',
'Battlefield 1' => 'battlefield-1',
'Battlefield' => 'battlefield',
'Béaba' => 'beaba',
'Beats by Dre' => 'beats-by-dre',
'BenQ' => 'benq',
'Be quiet!' => 'be-quiet',
'Biberons' => 'biberons',
'Bières' => 'bieres',
'Bijoux' => 'bijoux',
'Billets d&#039;avion' => 'billets-d-avion',
'BioShock' => 'bioshock',
'BioShock Infinite' => 'bioshock-infinite',
'Bitdefender' => 'bitdefender',
'Blackberry' => 'blackberry',
'Black & Decker' => 'black-decker',
'Blédina' => 'bledina',
'Blu-Ray' => 'blu-ray',
'Boissons' => 'boissons',
'Boîtes à outils' => 'boites-a-outils',
'Boîtiers PC' => 'boitiers-pc',
'Bonbons' => 'bonbons',
'Borderlands' => 'borderlands',
'Bosch' => 'bosch',
'Bose' => 'bose',
'Bose SoundLink' => 'bose-soundlink',
'Bottes' => 'bottes',
'Box beauté' => 'box-beaute',
'Bracelet fitness' => 'bracelet-fitness',
'Brandt' => 'brandt',
'Braun Silk Épil' => 'braun-silk-epil',
'Bricolage' => 'bricolage',
'Brosses à dents' => 'brosses-a-dents',
'Cable management' => 'cable-management',
'Câbles' => 'cables',
'Câbles HDMI' => 'cables-hdmi',
'Câbles USB' => 'cables-usb',
'Cadres' => 'cadres',
'Café' => 'cafe',
'Café en grain' => 'cafe-en-grain',
'Cafetières' => 'cafetieres',
'Cahiers' => 'cahiers',
'Call of Duty' => 'call-of-duty',
'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare',
'Calor' => 'calor',
'Caméras' => 'cameras',
'Caméras IP' => 'cameras-ip',
'Camping' => 'camping',
'Carburant' => 'carburant',
'Cartables' => 'cartables',
'Cartes graphiques' => 'cartes-graphiques',
'Cartes mères' => 'cartes-meres',
'Cartes postales' => 'cartes-postales',
'Casques audio' => 'casques-audio',
'Casques sans fil' => 'casques-sans-fil',
'Casquettes' => 'casquettes',
'Casseroles' => 'casseroles',
'CDAV' => 'cdav',
'Ceintures' => 'ceintures',
'Chaises' => 'chaises',
'Chaises hautes' => 'chaises-hautes',
'Chargeurs' => 'chargeurs',
'Chasse' => 'chasse',
'Chats' => 'chats',
'Chaussons' => 'chaussons',
'Chaussures adidas' => 'chaussures-adidas',
'Chaussures' => 'chaussures',
'Chaussures de football' => 'chaussures-de-football',
'Chaussures de randonnée' => 'chaussures-de-randonnee',
'Chaussures de running' => 'chaussures-de-running',
'Chaussures de ski' => 'chaussures-de-ski',
'Chaussures de ville' => 'chaussures-de-ville',
'Chaussures Nike' => 'chaussures-nike',
'Chelsea boots' => 'chelsea-boots',
'Chemises' => 'chemises',
'Chiens' => 'chiens',
'Chocolat' => 'chocolat',
'Chuck Taylor' => 'chuck-taylor',
'Cinéma' => 'cinema',
'Civilization' => 'civilization',
'Civilization VI' => 'civilization-vi',
'Clarks' => 'clarks',
'Claviers' => 'claviers',
'Claviers gamer' => 'claviers-gamer',
'Claviers mécaniques' => 'claviers-mecaniques',
'Clés USB' => 'cles-usb',
'Composteurs' => 'composteurs',
'Concerts' => 'concerts',
'Congélateurs' => 'congelateurs',
'Consoles' => 'consoles',
'Consoles & jeux vidéo' => 'consoles-jeux-video',
'Converse' => 'converse',
'Costumes' => 'costumes',
'Couches' => 'couches',
'Couettes' => 'couettes',
'Couteaux de cuisine' => 'couteaux-de-cuisine',
'Couverts' => 'couverts',
'Covoiturage' => 'covoiturage',
'Crédits' => 'credits',
'Croquettes pour chien' => 'croquettes-pour-chien',
'Cuisinières' => 'cuisinieres',
'Culture & divertissement' => 'culture-divertissement',
'Cyclisme' => 'cyclisme',
'DDR3' => 'ddr3',
'DDR4' => 'ddr4',
'Décoration' => 'decoration',
'Deezer' => 'deezer',
'Dell' => 'dell',
'Delsey' => 'delsey',
'Denon' => 'denon',
'Dentifrices' => 'dentifrices',
'Destiny 2' => 'destiny-2',
'Destiny' => 'destiny',
'Dishonored' => 'dishonored',
'Disneyland Paris' => 'disneyland-paris',
'Disques durs externes' => 'disques-durs-externes',
'Disques durs internes' => 'disques-durs',
'DJI' => 'dji',
'Dosettes Nespresso' => 'dosettes-nespresso',
'Dosettes Senseo' => 'dosettes-senseo',
'Dosettes Tassimo' => 'dosettes-tassimo',
'Draisiennes' => 'draisiennes',
'Drones' => 'drones',
'Durex' => 'durex',
'DVD' => 'dvd',
'Dyson' => 'dyson',
'Eastpak' => 'eastpak',
'ebooks' => 'ebooks',
'Écharpes & foulards' => 'echarpes-et-foulards',
'Écouteurs' => 'ecouteurs',
'Écouteurs intra-auriculaires' => 'ecouteurs-intra-auriculaires',
'Écouteurs sans fil' => 'ecouteurs-sans-fil',
'Écouteurs sport' => 'ecouteurs-sport',
'Écrans 21" et moins' => 'ecrans-21-pouces-et-moins',
'Écrans 24"' => 'ecrans-24-pouces',
'Écrans 27"' => 'ecrans-27-pouces',
'Écrans 29" et plus' => 'ecrans-29-pouces-et-plus',
'Écrans 4K / UHD' => 'ecrans-4k-uhd',
'Écrans Acer' => 'ecrans-acer',
'Écrans Asus' => 'ecrans-asus',
'Écrans BenQ' => 'ecrans-benq',
'Écrans Dell' => 'ecrans-dell',
'Écrans de projection' => 'ecrans-de-projection',
'Écrans' => 'ecrans',
'Écrans FreeSync' => 'ecrans-freesync',
'Écrans gamer' => 'ecrans-gamer',
'Écrans incurvés' => 'ecrans-incurves',
'Écrans Philips' => 'ecrans-philips',
'Écrans Samsung' => 'ecrans-samsung',
'Électricité (matériel)' => 'electricite',
'Electrolux' => 'electrolux',
'Électroménager' => 'electromenager',
'Embauchoirs' => 'embauchoirs',
'Enceintes Bluetooth' => 'enceintes-bluetooth',
'Enceintes' => 'enceintes',
'Engrais' => 'engrais',
'Entretien du jardin' => 'entretien-du-jardin',
'Épicerie' => 'epicerie',
'Épilateurs à lumière pulsée' => 'epilateurs-a-lumiere-pulsee',
'Épilateurs électriques' => 'epilateurs-electriques',
'Épilation' => 'epilation',
'Équipement auto' => 'equipement-auto',
'Équipement motard' => 'equipement-motard',
'Équipement sportif' => 'equipement-sportif',
'Érotisme' => 'erotisme',
'Escarpins' => 'escarpins',
'Événements sportifs' => 'evenements-sportifs',
'Expositions' => 'expositions',
'F1 2017' => 'f1-2017',
'Facom' => 'facom',
'Fallout 4' => 'fallout-4',
'Fallout' => 'fallout',
'Fards à paupières' => 'fards-a-paupieres',
'Fast-foods' => 'fast-foods',
'Fauteuils' => 'fauteuils',
'Fers à lisser / à friser' => 'fers-a-lisser-a-friser',
'Fers à souder' => 'fers-a-souder',
'Festivals' => 'festivals',
'Feutres' => 'feutres',
'FIFA 17' => 'fifa-17',
'FIFA 18' => 'fifa-18',
'FIFA 19' => 'fifa-19',
'FIFA' => 'fifa',
'Figurines' => 'figurines',
'Films' => 'films',
'Final Fantasy' => 'final-fantasy',
'Final Fantasy XII' => 'final-fantasy-xii',
'fitbit' => 'fitbit',
'Flash' => 'flash',
'Fluval' => 'fluval',
'Foires & salons' => 'foires-et-salons',
'Fonds de teint' => 'fonds-de-teint',
'Football' => 'football',
'Forfaits mobiles' => 'forfaits-mobiles',
'For Honor' => 'for-honor',
'Formule 1' => 'formule-1',
'Fortnite' => 'fortnite',
'Forza Horizon 3' => 'forza-horizon-3',
'Forza Motorsport 7' => 'forza-motorsport-7',
'Fossil' => 'fossil',
'Fournitures de bureau' => 'fournitures-de-bureau',
'Fournitures scolaires' => 'fournitures-scolaires',
'Fours à poser' => 'fours-a-poser',
'Fours encastrables' => 'fours-encastrables',
'Fours' => 'fours',
'Friandises pour chat' => 'friandises-pour-chat',
'Friandises pour chien' => 'friandises-pour-chien',
'Friskies' => 'friskies',
'Fruits & légumes' => 'fruits-et-legumes',
'FURminator' => 'furminator',
'Futuroscope' => 'futuroscope',
'Gamelles' => 'gamelles',
'Game of Thrones' => 'game-of-thrones',
'Gants' => 'gants',
'Gants moto' => 'gants-moto',
'Garmin' => 'garmin',
'Gâteaux & biscuits' => 'gateaux-et-biscuits',
'Gels douche' => 'gels-douche',
'Geox' => 'geox',
'Gigoteuses' => 'gigoteuses',
'Gillette' => 'gillette',
'Glaces' => 'glaces',
'God of War' => 'god-of-war',
'Google Chromecast' => 'google-chromecast',
'Google Home' => 'google-home',
'Google Pixel 2' => 'google-pixel-2',
'Google Pixel 2 XL' => 'google-pixel-2-xl',
'Google Pixel' => 'google-pixel',
'Google Pixel XL' => 'google-pixel-xl',
'GoPro Hero' => 'gopro-hero',
'Gran Turismo' => 'gran-turismo',
'Gratuit' => 'gratuit',
'Grille-pain' => 'grille-pain',
'GTA' => 'gta',
'GTA V' => 'gta-v',
'Guitares' => 'guitares',
'Gyropodes' => 'gyropodes',
'Haltères & poids' => 'halteres-et-poids',
'Hamacs' => 'hamacs',
'Hama' => 'hama',
'Hand spinners' => 'hand-spinners',
'Harnais pour chien' => 'harnais-pour-chien',
'Harry Potter' => 'harry-potter',
'Havaianas' => 'havaianas',
'HDD' => 'hdd',
'Hisense' => 'hisense',
'Home Cinéma' => 'home-cinema',
'Honor 6X' => 'honor-6x',
'Honor 8' => 'honor-8',
'Honor 8 Pro' => 'honor-8-pro',
'Honor 9' => 'honor-9',
'Horizon Zero Dawn' => 'horizon-zero-dawn',
'Hôtels' => 'hotels',
'Hoverboards' => 'hoverboards',
'HTC 10' => 'htc-10',
'HTC Desire' => 'htc-desire',
'HTC One M9' => 'htc-one-m9',
'HTC U11' => 'htc-u11',
'HTC U Play' => 'htc-u-play',
'HTC U Ultra' => 'htc-u-ultra',
'HTC Vive' => 'htc-vive',
'Huawei Mate 10' => 'huawei-mate-10',
'Huawei Mate 9' => 'huawei-mate-9',
'Huawei P10' => 'huawei-p10',
'Huawei P10 Lite' => 'huawei-p10-lite',
'Huawei P10 Plus' => 'huawei-p10-plus',
'Huawei P20' => 'huawei-p20',
'Huawei P20 Pro' => 'huawei-p20-pro',
'Huawei P8 Lite' => 'huawei-p8-lite',
'Huawei P9 Lite' => 'huawei-p9-lite',
'Hubs' => 'hubs',
'Huile moteur' => 'huile-moteur',
'Hygiène corporelle' => 'hygiene-corporelle',
'Hygiène de la maison' => 'hygiene-de-la-maison',
'Hygiène des bébés' => 'hygiene-des-bebes',
'Image, son & vidéo' => 'image-son-video',
'Impressions photo' => 'impressions-photo',
'Imprimantes 3D' => 'imprimantes-3d',
'Imprimantes Brother' => 'imprimantes-brother',
'Imprimantes Canon' => 'imprimantes-canon',
'Imprimantes Epson' => 'imprimantes-epson',
'Imprimantes HP' => 'imprimantes-hp',
'Imprimantes' => 'imprimantes',
'Imprimantes laser' => 'imprimantes-laser',
'Imprimantes multifonctions' => 'imprimantes-multifonctions',
'Informatique' => 'informatique',
'Instruments de musique' => 'instruments-de-musique',
'Intel i5' => 'intel-i5',
'Intel i7' => 'intel-i7',
'JBL Flip' => 'jbl-flip',
'JBL' => 'jbl',
'Jeans' => 'jeans',
'Jeux d&#039;apprentissage' => 'jeux-d-apprentissage',
'Jeux d&#039;extérieur' => 'jeux-d-exterieur',
'Jeux d&#039;imitation' => 'jeux-d-imitation',
'Jeux de construction' => 'jeux-de-construction',
'Jeux de société' => 'jeux-de-societe',
'Jeux & jouets' => 'jeux-jouets',
'Maison & jardin' => 'maison-jardin',
'Jeux Nintendo Switch' => 'jeux-nintendo-switch',
'Jeux & paris' => 'jeux-et-paris',
'Jeux PC dématérialisés' => 'jeux-pc-dematerialises',
'Jeux PlayStation 4' => 'jeux-playstation-4',
'Jeux pour bébés' => 'jeux-pour-bebes',
'Jeux PS4 dématérialisés' => 'jeux-ps4-dematerialises',
'Jeux PS Plus' => 'jeux-ps-plus',
'Jeux vidéo' => 'jeux-video',
'Jeux Wii U' => 'jeux-wii-u',
'Jeux Xbox dématérialisés' => 'jeux-xbox-dematerialises',
'Jeux Xbox One' => 'jeux-xbox-one',
'Jeux Xbox with Gold' => 'jeux-xbox-with-gold',
'Journaux numériques' => 'journaux-numeriques',
'Journaux papier' => 'journaux-papier',
'Joy-Con' => 'manettes-nintendo-switch-joy-con',
'Jungle Speed' => 'jungle-speed',
'Kaspersky' => 'kaspersky',
'Kinder' => 'kinder',
'Kindle Paperwhite' => 'kindle-paperwhite',
'Kindle Voyage' => 'kindle-voyage',
'Kobo Aura 2' => 'kobo-aura-2',
'Kobo Aura H2o' => 'kobo-aura-h2o',
'Kobo' => 'kobo',
'L&#039;annale du destin' => 'l-annale-du-destin',
'L&#039;ombre de la guerre' => 'l-ombre-de-la-guerre',
'L&#039;ombre du Mordor' => 'l-ombre-du-mordor',
'Lacoste' => 'lacoste',
'Lapeyre' => 'lapeyre',
'La Terre du Milieu' => 'la-terre-du-milieu',
'Lavage auto' => 'lavage-auto',
'Lave-linge frontal' => 'lave-linge-frontal',
'Lave-linge' => 'lave-linge',
'Lave-linge séchant' => 'lave-linge-sechant',
'Lave-linge top' => 'lave-linge-top',
'Lave-vaisselle' => 'lave-vaisselle',
'Le bâton de la vérité' => 'le-baton-de-la-verite',
'Lecteurs Blu-Ray' => 'lecteurs-blu-ray',
'Lecteurs CD' => 'lecteurs-cd',
'Lecteurs DVD' => 'lecteurs-dvd',
'Lego' => 'lego',
'Lego Star Wars' => 'lego-star-wars',
'Lenovo K6 Note' => 'lenovo-k6-note',
'Lenovo' => 'lenovo',
'Lenovo P8' => 'lenovo-p8',
'Lenovo Tab 3' => 'lenovo-tab-3',
'Lenovo Tab 4' => 'lenovo-tab-4',
'Lenovo Yoga' => 'lenovo-yoga',
'Lenovo Yoga Tab 3' => 'lenovo-yoga-tab-3',
'Lentilles de contact' => 'lentilles-de-contact',
'Le Seigneur des anneaux' => 'le-seigneur-des-anneaux',
'Les Sims' => 'les-sims',
'Lessive' => 'lessive',
'Levi&#039;s' => 'levi-s',
'LG G4' => 'lg-g4',
'LG G5' => 'lg-g5',
'LG G6' => 'lg-g6',
'LG' => 'lg',
'LG OLED TV' => 'lg-oled-tv',
'LG Q6' => 'lg-q6',
'LG Q8' => 'lg-q8',
'Life is Strange' => 'life-is-strange',
'Linge de maison' => 'linge-de-maison',
'Lingerie' => 'lingerie',
'Lingettes pour bébés' => 'lingettes-pour-bebes',
'Liseuses' => 'liseuses',
'Litière pour chat' => 'litiere-pour-chat',
'Lits' => 'lits',
'Lits pour bébé' => 'lits-pour-bebe',
'Livres audio' => 'livres-audio',
'Livres' => 'livres',
'Livres photo' => 'livres-photo',
'Location de voiture' => 'location-de-voiture',
'Logiciels de sécurité' => 'logiciels-de-securite',
'Logiciels Microsoft' => 'logiciels-microsoft',
'Logitech Harmony' => 'logitech-harmony',
'Logitech' => 'logitech',
'Loup-Garou' => 'loup-garou',
'Lubrifiants' => 'lubrifiants',
'Luminaires' => 'luminaires',
'Lunettes de natation' => 'lunettes-de-natation',
'Lunettes de soleil' => 'lunettes-de-soleil',
'MacBook' => 'macbook',
'Mac de bureau' => 'mac-de-bureau',
'Machines à café à dosettes' => 'machines-a-cafe-a-dosettes',
'Machines à café en grain' => 'machines-a-cafe-en-grain',
'Machines à pain' => 'machines-a-pain',
'Machines Dolce Gusto' => 'machines-dolce-gusto',
'Machines Nespresso' => 'machines-nespresso',
'Machines Senseo' => 'machines-senseo',
'Magasins d&#039;usine' => 'magasins-usine',
'Magazines' => 'magazines',
'Maillots de bain' => 'maillots-de-bain',
'Maillots de football' => 'maillots-de-football',
'Maison & Jardin' => 'maison-et-jardin',
'Makita' => 'makita',
'Manettes Nintendo Switch Pro' => 'manettes-nintendo-switch-pro',
'Manettes PlayStation 4' => 'manettes-playstation-4',
'Manettes Xbox One Elite' => 'manettes-xbox-one-elite',
'Manettes Xbox One' => 'manettes-xbox-one',
'Manix' => 'manix',
'Manteaux' => 'manteaux',
'Maquillage' => 'maquillage',
'Mario Kart' => 'mario-kart',
'Marteaux & maillets' => 'marteaux-et-maillets',
'Mascara' => 'mascara',
'Masques de ski' => 'masques-de-ski',
'Mass Effect: Andromeda' => 'mass-effect-andromeda',
'Matchs de football' => 'matchs-de-football',
'Matelas gonflables' => 'matelas-gonflables',
'Matelas' => 'matelas',
'Matériaux de construction' => 'materiaux-de-construction',
'Matériel de ski' => 'materiel-de-ski',
'Medion' => 'medion',
'Meubles pour chat' => 'meubles-pour-chat',
'Micro-casques gaming' => 'micro-casques-gaming',
'Micro-ondes' => 'micro-ondes',
'Microphones' => 'microphones',
'Micro-SD' => 'micro-sd',
'Microsoft Office' => 'microsoft-office',
'Microsoft Surface' => 'microsoft-surface',
'Miele' => 'miele',
'Minecraft' => 'minecraft',
'Mixeurs' => 'mixeurs',
'M&M&#039;s' => 'metm-s',
'Mobilier' => 'mobilier',
'Mode & accessoires' => 'mode-accessoires',
'Santé & cosmétiques' => 'hygiene-sante-cosmetiques',
'Mode enfants' => 'mode-enfants',
'Mode femme' => 'mode-femme',
'Mode homme' => 'mode-homme',
'Modélisme' => 'modelisme',
'Monopoly' => 'monopoly',
'Montage PC' => 'montage-pc',
'Montres' => 'montres',
'Moto C Plus' => 'moto-c-plus',
'Moto E4' => 'moto-e4',
'Moto G5' => 'moto-g5',
'Moto G5 Plus' => 'moto-g5-plus',
'Moto G5S' => 'moto-g5s',
'Moto G5S Plus' => 'moto-g5s-plus',
'Moto M' => 'moto-m',
'Moto' => 'moto',
'Moto Z2' => 'moto-z2',
'Moto Z2 Play' => 'moto-z2-play',
'Moulinex' => 'moulinex',
'Mousses à raser' => 'mousses-a-raser',
'MSI' => 'msi',
'Musées' => 'musees',
'Musique' => 'musique',
'NAS' => 'nas',
'Natation' => 'natation',
'Navigation' => 'navigation',
'NERF' => 'nerf',
'New Balance' => 'new-balance',
'Nike Air Force' => 'nike-air-force',
'Nike Air Max' => 'nike-air-max',
'Nike Free' => 'nike-free',
'Nike Huarache' => 'nike-huarache',
'Nike' => 'nike',
'Nintendo Classic Mini' => 'nintendo-classic-mini',
'Nintendo' => 'nintendo',
'Nintendo Switch' => 'nintendo-switch',
'Nivea' => 'nivea',
'Nokia 5' => 'nokia-5',
'Nokia 6' => 'nokia-6',
'Nokia 8' => 'nokia-8',
'Nourriture pour chat' => 'nourriture-pour-chat',
'Nourriture pour chien' => 'nourriture-pour-chien',
'Nutella' => 'nutella',
'Nvidia GeForce GTX 1060' => 'nvidia-geforce-gtx-1060',
'Nvidia GeForce GTX 1070' => 'nvidia-geforce-gtx-1070',
'Nvidia GeForce GTX 1080' => 'nvidia-geforce-gtx-1080',
'Nvidia GeForce GTX 1080 Ti' => 'nvidia-geforce-gtx-1080-ti',
'Nvidia' => 'nvidia',
'Nvidia Shield' => 'nvidia-shield',
'Objectifs' => 'objectifs',
'Oculus Rift' => 'oculus-rift',
'Oiseaux' => 'oiseaux',
'OnePlus 5' => 'oneplus-5',
'OnePlus 5T' => 'oneplus-5t',
'OnePlus 6' => 'oneplus-6',
'Onkyo' => 'onkyo',
'Ordinateurs de bureau' => 'ordinateurs-de-bureau',
'Oreillers' => 'oreillers',
'Outillage' => 'outillage',
'Outils de jardinage' => 'outils-de-jardinage',
'Overwatch' => 'overwatch',
'Packs clavier-souris' => 'packs-clavier-souris',
'Paiement en ligne' => 'paiement-en-ligne',
'Pampers' => 'pampers',
'Panasonic' => 'panasonic',
'Panier Plus' => 'panier-plus',
'Pantalons' => 'pantalons',
'Papeterie' => 'papeterie',
'Papier peint' => 'papier-peint',
'Papier toilette' => 'papier-toilette',
'Parapharmacie' => 'parapharmacie',
'Parc Astérix' => 'parc-asterix',
'Parfums femme' => 'parfums-femme',
'Parfums homme' => 'parfums-homme',
'Parfums' => 'parfums',
'Parkas' => 'parkas',
'Parrot' => 'parrot',
'Partitions' => 'partitions',
'PC de bureau complets' => 'pc-de-bureau-complets',
'PC gamer complets' => 'pc-gamer-complets',
'PC hybrides' => 'hybrides',
'PC portables' => 'pc-portables',
'Pêche' => 'peche',
'Peintures' => 'peintures',
'Peluches' => 'peluches',
'Perceuses' => 'perceuses',
'Périphériques PC' => 'peripheriques-pc',
'Pèse-personnes' => 'pese-personnes',
'PES' => 'pro-evolution-soccer',
'Petites voitures' => 'petites-voitures',
'Philips Hue' => 'philips-hue',
'Philips Lumea' => 'philips-lumea',
'Philips One Blade' => 'philips-one-blade',
'Philips' => 'philips',
'Philips Sonicare' => 'philips-sonicare',
'Photo' => 'photo',
'Pièces auto' => 'pieces-auto',
'Pièces moto' => 'pieces-moto',
'Pièces vélo' => 'pieces-velo',
'Piles' => 'piles',
'Piles rechargeables' => 'piles-rechargeables',
'Pinces' => 'pinces',
'Pizza' => 'pizza',
'Places de cinéma' => 'places-de-cinema',
'Plage' => 'plage',
'Plantes' => 'plantes',
'Plaques de cuisson' => 'plaques-de-cuisson',
'Platines vinyle' => 'platines-vinyle',
'Playmobil' => 'playmobil',
'PlayStation 4' => 'playstation-4',
'PlayStation 4 Pro' => 'playstation-4-pro',
'PlayStation 4 Slim' => 'playstation-4-slim',
'PlayStation' => 'playstation',
'PlayStation Plus' => 'playstation-plus',
'Playstation Store' => 'playstation-store',
'Plomberie' => 'plomberie',
'Pneus' => 'pneus',
'PocketBook' => 'pocketbook',
'Poêles' => 'poeles',
'Pokémon' => 'pokemon',
'Portables gamer' => 'portables-gamer',
'Porte-bébé' => 'porte-bebe',
'Portefeuilles' => 'portefeuilles',
'Posters' => 'posters',
'Potager' => 'potager',
'Poulaillers' => 'poulaillers',
'Poupées' => 'poupees',
'Poussettes' => 'poussettes',
'Premiers secours' => 'premiers-secours',
'Préservatifs' => 'preservatifs',
'Princesse Tam-Tam' => 'princesse-tam-tam',
'Processeurs' => 'processeurs',
'Protection de la maison' => 'protection-de-la-maison',
'Protections intimes' => 'protections-intimes',
'Puériculture' => 'puericulture',
'Pulls' => 'pulls',
'Puma' => 'puma',
'Purificateurs d&#039;air' => 'purificateurs-d-air',
'Purina' => 'purina',
'Puzzles' => 'puzzles',
'Pyjamas pour bébés' => 'pyjamas-pour-bebes',
'Pyjamas' => 'pyjamas',
'Qobuz' => 'qobuz',
'RAM' => 'ram',
'Randonnée' => 'randonnee',
'Rasage' => 'rasage',
'Rasoirs électriques' => 'rasoirs-electriques',
'Rasoirs manuels' => 'rasoirs-manuels',
'Raspberry Pi' => 'raspberry-pi',
'Ray-Ban' => 'ray-ban',
'Razer' => 'razer',
'Réductions étudiants & jeunes' => 'reductions-etudiants-et-jeunes',
'Reebok' => 'reebok',
'Réfrigérateurs' => 'refrigerateurs',
'Réhausseurs' => 'rehausseurs',
'Remington' => 'remington',
'Répéteurs' => 'repeteurs',
'Réseau' => 'reseau',
'Resident Evil 7' => 'resident-evil-7',
'Resident Evil' => 'resident-evil',
'Restaurants' => 'restaurants',
'Richelieus' => 'richelieus',
'Risk' => 'risk',
'Rongeurs' => 'rongeurs',
'Rouges à lèvres' => 'rouges-a-levres',
'Routeurs' => 'routeurs',
'Royal Canin' => 'royal-canin',
'Running' => 'running',
'Sacs à dos' => 'sacs-a-dos',
'Sacs à langer' => 'sacs-a-langer',
'Sacs à main' => 'sacs-a-main',
'Samsonite' => 'samsonite',
'Samsung Galaxy A5' => 'samsung-galaxy-a5',
'Samsung Galaxy Note 8' => 'samsung-galaxy-note-8',
'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge',
'Samsung Galaxy S7' => 'samsung-galaxy-s7',
'Samsung Galaxy S8' => 'samsung-galaxy-s8',
'Samsung Galaxy S8+' => 'samsung-galaxy-s8plus',
'Samsung Galaxy S9' => 'samsung-galaxy-s9',
'Samsung Galaxy Tab A' => 'samsung-galaxy-tab-a',
'Samsung Galaxy Tab S2' => 'samsung-galaxy-tab-s2',
'Samsung Galaxy Tab S3' => 'samsung-galaxy-tab-s3',
'Samsung Gear' => 'samsung-gear',
'Samsung Gear VR' => 'samsung-gear-vr',
'Samsung' => 'samsung',
'Sandales' => 'sandales',
'SanDisk' => 'sandisk',
'Santé & Cosmétiques' => 'sante-et-cosmetiques',
'Savons' => 'savons',
'Scanners' => 'scanners',
'Scies' => 'scies',
'Scooters' => 'scooters',
'Seagate' => 'seagate',
'Sécateurs' => 'secateurs',
'Sèche-cheveux' => 'seche-cheveux',
'Sèche-linge' => 'seche-linge',
'Séjours' => 'sejours',
'Sennheiser' => 'sennheiser',
'Séries TV' => 'series-tv',
'Services divers' => 'services-divers',
'Serviettes hygiéniques' => 'serviettes-hygieniques',
'Serviettes' => 'serviettes',
'Sextoys' => 'sextoys',
'Shorts de bain' => 'shorts-de-bain',
'Shorts' => 'shorts',
'Sièges auto' => 'sieges-auto',
'Siemens' => 'siemens',
'Skechers' => 'sketchers',
'Ski' => 'ski',
'Skyrim' => 'skyrim',
'Smartbox' => 'smartbox',
'Smart Home' => 'smart-home',
'Smartphones à moins de 100€' => 'smartphones-moins-de-100',
'Smartphones à moins de 200€' => 'smartphones-moins-de-200',
'Smartphones Android' => 'smartphones-android',
'Smartphones Huawei' => 'smartphones-huawei',
'Smartphones Nokia' => 'smartphones-nokia',
'Smartphones Samsung' => 'smartphones-samsung',
'Smartphones' => 'smartphones',
'Smartphones Xiaomi' => 'smartphones-xiaomi',
'Smart TV' => 'smart-tv',
'Smartwatch' => 'smartwatch',
'Sneakers' => 'sneakers',
'Soin des cheveux' => 'soin-des-cheveux',
'Sonos PLAYBAR' => 'sonos-playbar',
'Sonos' => 'sonos',
'Sony PlayStation VR' => 'sony-playstation-vr',
'Sony' => 'sony',
'Sony Xperia XA1' => 'sony-xperia-xa1',
'Sony Xperia X Compact' => 'sony-xperia-x-compact',
'Sony Xperia XZ1 Compact' => 'sony-xperia-xz1-compact',
'Sony Xperia XZ1' => 'sony-xperia-xz1',
'Sony Xperia XZ Premium' => 'sony-xperia-xz-premium',
'Sony Xperia Z3' => 'sony-xperia-z3',
'Sorties' => 'sorties',
'Souris gamer' => 'souris-gamer',
'Souris Logitech' => 'souris-logitech',
'Souris sans fil' => 'souris-sans-fil',
'Souris' => 'souris',
'South Park' => 'south-park',
'Spectacles comiques' => 'spectacles-comiques',
'Spectacles' => 'spectacles',
'Sports & plein air' => 'sports-plein-air',
'Spotify' => 'spotify',
'SSD' => 'ssd',
'Star Wars Battlefront' => 'star-wars-battlefront',
'Stickers muraux' => 'stickers-muraux',
'Stihl' => 'stihl',
'Stockage externe' => 'stockage',
'Streaming musical' => 'streaming-musical',
'Stylos' => 'stylos',
'Sucettes' => 'sucettes',
'Super Mario' => 'super-mario',
'Support GPS & smartphone' => 'support-gps-et-smartphone',
'Surface Pro 4' => 'surface-pro-4',
'Surgelés' => 'surgeles',
'Surveillance' => 'surveillance',
'Swatch' => 'swatch',
'Switch réseau' => 'switch-reseau',
'Systèmes d&#039;exploitation' => 'systemes-d-exploitation',
'Systèmes multiroom' => 'systemes-multiroom',
'Tables à langer' => 'tables-a-langer',
'Tables de camping' => 'tables-de-camping',
'Tables de mixage' => 'tables-de-mixage',
'Tables' => 'tables',
'Tablettes graphiques Huion' => 'huion',
'Tablettes graphiques' => 'tablettes-graphiques',
'Tablettes graphiques Wacom' => 'wacom',
'Tablettes Lenovo' => 'tablettes-lenovo',
'Tablettes Samsung' => 'tablettes-samsung',
'Tablettes' => 'tablettes',
'Tablettes Xiaomi' => 'tablettes-xiaomi',
'Tampons' => 'tampons',
'Tapis' => 'tapis',
'Taxis' => 'taxis',
'Tefal' => 'tefal',
'Télécommandes' => 'telecommandes',
'Téléphones fixes' => 'telephones-fixes',
'Téléphonie' => 'telephonie',
'Voyages & sorties' => 'voyages-sorties-restaurants',
'Téléviseurs' => 'televiseurs',
'Tentes' => 'tentes',
'Têtes de brosse à dents de rechange' => 'tetes-de-brosse-a-dents-de-rechange',
'Théâtre' => 'theatre',
'The Legend of Zelda' => 'the-legend-of-zelda',
'Thermomètres' => 'thermometres',
'Thermomix' => 'thermomix',
'Thés glacés' => 'thes-glaces',
'Thés' => 'thes',
'The Walking dead' => 'the-walking-dead',
'The Witcher 3' => 'the-witcher-3',
'The Witcher' => 'the-witcher',
'Time&#039;s Up!' => 'time-s-up',
'Tom Clancy&#039;s Ghost Recon: Wildlands' => 'tom-clancy-s-ghost-recon-wildlands',
'Tom Clancy&#039;s The Division' => 'tom-clancy-s-the-division',
'Tom Clancy&#039;s' => 'tom-clancy-s',
'TomTom' => 'tomtom',
'Tondeuses à gazon' => 'tondeuses-a-gazon',
'Tondeuses' => 'tondeuses',
'Toner' => 'toner',
'Torchons' => 'torchons',
'Toshiba' => 'toshiba',
'Total War' => 'total-war',
'Total War: Warhammer II' => 'total-war-warhammer-ii',
'Total War: Warhammer' => 'total-war-warhammer',
'Tournevis & visseuses' => 'tournevis-et-visseuses',
'TP-Link' => 'tp-link',
'Transats & cosys' => 'transats-et-cosys',
'Transports en commun' => 'transports-en-commun',
'Trixie' => 'trixie',
'Tronçonneuses' => 'tronconneuses',
'Trottinettes électriques' => 'trottinettes-electriques',
'Trottinettes' => 'trottinettes',
'T-shirts' => 't-shirts',
'TV 39&#039;&#039; et moins' => 'tv-39-pouces-et-moins',
'TV 40&#039;&#039; à 64&#039;&#039;' => 'tv-40-pouces-a-64-pouces',
'TV 4K' => 'tv-4k',
'TV 65&#039;&#039; et plus' => 'tv-65-pouces-et-plus',
'TV Full HD' => 'tv-full-hd',
'TV incurvées' => 'tv-incurvees',
'TV LG' => 'tv-lg',
'TV OLED' => 'tv-oled',
'TV Panasonic' => 'tv-panasonic',
'TV Philips' => 'tv-philips',
'TV Samsung' => 'tv-samsung',
'TV Sony' => 'tv-sony',
'Ultraportables' => 'ultraportables',
'Uncharted 4' => 'uncharted-4',
'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy',
'Uncharted' => 'uncharted',
'Ustensiles de cuisine' => 'ustensiles-de-cuisine',
'Ustensiles de cuisson' => 'ustensiles-de-cuisson',
'Vaisselle' => 'vaisselle',
'Valises cabine' => 'valises-cabine',
'Valises rigides' => 'valises-rigides',
'Valises' => 'valises',
'Variétés & revues' => 'varietes-et-revues',
'Vases' => 'vases',
'Veet' => 'veet',
'Vélos d&#039;appartement' => 'velos-d-appartement',
'Vélos' => 'velos',
'Ventilateurs' => 'ventilateurs',
'Ventirad' => 'ventirad',
'Vernis à ongles' => 'vernis-a-ongles',
'Vestes' => 'vestes',
'Vêtements d&#039;été' => 'vetements-d-ete',
'Vêtements d&#039;hiver' => 'vetements-d-hiver',
'Vêtements de grossesse' => 'vetements-de-grossesse',
'Vêtements de ski' => 'vetements-de-ski',
'Vêtements de sport' => 'vetements-de-sport',
'Vêtements pour bébé' => 'vetements-pour-bebe',
'Vêtements techniques' => 'vetements-techniques',
'Vidéoprojecteurs 3D' => 'videoprojecteurs-3d',
'Vidéoprojecteurs Acer' => 'videoprojecteurs-acer',
'Vidéoprojecteurs BenQ' => 'videoprojecteurs-benq',
'Vidéoprojecteurs Epson' => 'videoprojecteurs-epson',
'Vidéoprojecteurs HD' => 'videoprojecteurs-hd',
'Vidéoprojecteurs LG' => 'videoprojecteurs-lg',
'Vidéoprojecteurs Optoma' => 'videoprojecteurs-optoma',
'Vidéoprojecteurs' => 'projecteurs',
'Vidéo' => 'video',
'Vins' => 'vins',
'Visites & patrimoine' => 'visites-et-patrimoine',
'VOD' => 'vod',
'Voitures télécommandées' => 'voitures-telecommandees',
'Voyages & sorties' => 'voyages-et-sorties',
'Voyages' => 'voyages',
'VPN' => 'vpn',
'VR' => 'vr',
'VTC' => 'vtc',
'VTT' => 'vtt',
'Wacom Cintiq' => 'cintiq',
'Watercooling' => 'watercooling',
'WD (Western Digital)' => 'western-digital',
'Wearables' => 'wearables',
'Whey' => 'whey',
'Whirlpool' => 'whirlpool',
'Whiskas' => 'whiskas',
'Wii U' => 'wii-u',
'Wiko' => 'wiko',
'Windows' => 'windows',
'WindScribe' => 'windscribe',
'Wolfenstein II: The New Colossus' => 'wolfenstein-ii-the-new-colossus',
'Wolfenstein' => 'wolfenstein',
'Wonderbox' => 'wonderbox',
'Xbox Live' => 'xbox-live',
'Xbox One S' => 'xbox-one-s',
'Xbox One' => 'xbox-one',
'Xbox One X' => 'xbox-one-x',
'Xbox' => 'xbox',
'Xiaomi Mi6' => 'xiaomi-mi6',
'Xiaomi Mi A1' => 'xiaomi-mi-a1',
'Xiaomi Mi Band' => 'xiaomi-mi-band',
'Xiaomi Mi Box' => 'xiaomi-mi-box',
'Xiaomi Mi Max' => 'xiaomi-mi-max',
'Xiaomi Mi Mix 2' => 'xiaomi-mi-mix-2',
'Xiaomi Mi Mix' => 'xiaomi-mi-mix',
'Xiaomi Mi Pad 3' => 'xiaomi-mi-pad-3',
'Xiaomi Redmi 4A' => 'xiaomi-redmi-4a',
'Xiaomi Redmi 4X' => 'xiaomi-redmi-4x',
'Xiaomi Redmi Note 4' => 'xiaomi-redmi-note-4',
'Xiaomi Smart Home' => 'xiaomi-smart-home',
'Xiaomi' => 'xiaomi',
'Yamaha' => 'yamaha',
'Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild',
'Zoos' => 'zoos',
)
),
'order' => array(
@@ -229,10 +1120,8 @@ class PepperBridgeAbstract extends BridgeAbstract {
$selectorHot = implode(
' ', /* Notice this is a space! */
array(
'flex',
'flex--align-c',
'flex--justify-space-between',
'space--b-2',
'cept-vote-box',
'vote-box'
)
);
@@ -251,8 +1140,7 @@ class PepperBridgeAbstract extends BridgeAbstract {
array(
'size--all-s',
'flex',
'flex--justify-e',
'flex--grow-1',
'boxAlign-jc--all-fe'
)
);
@@ -284,7 +1172,8 @@ class PepperBridgeAbstract extends BridgeAbstract {
. $this->GetSource($deal)
. $deal->find('div[class*=' . $selectorDescription . ']', 0)->innertext
. '</td><td>'
. $deal->find('div[class='. $selectorHot .']', 0)->children(0)->outertext
. $deal->find('div[class*=' . $selectorHot . ']', 0)
->find('span', 1)->outertext
. '</td></table>';
$dealDateDiv = $deal->find('div[class*=' . $selectorDate . ']', 0)
->find('span[class=hide--toW3]');

240
bridges/DesoutterBridge.php Normal file
View File

@@ -0,0 +1,240 @@
<?php
class DesoutterBridge extends BridgeAbstract {
const CATEGORY_NEWS = 'News & Events';
const CATEGORY_INDUSTRY = 'Industry 4.0 News';
const NAME = 'Desoutter Bridge';
const URI = 'https://www.desouttertools.com';
const DESCRIPTION = 'Returns feeds for news from Desoutter';
const MAINTAINER = 'logmanoriginal';
const CACHE_TIMEOUT = 86400; // 24 hours
const PARAMETERS = array(
self::CATEGORY_NEWS => array(
'news_lang' => array(
'name' => 'Language',
'type' => 'list',
'required' => true,
'title' => 'Select your language',
'defaultValue' => 'Corporate',
'values' => array(
'Corporate'
=> 'https://www.desouttertools.com/about-desoutter/news-events',
'Česko'
=> 'https://www.desouttertools.cz/o-desoutter/aktuality-udalsoti',
'Deutschland'
=> 'https://www.desoutter.de/ueber-desoutter/news-events',
'España'
=> 'https://www.desouttertools.es/sobre-desoutter/noticias-eventos',
'México'
=> 'https://www.desouttertools.mx/acerca-desoutter/noticias-eventos',
'France'
=> 'https://www.desouttertools.fr/a-propos-de-desoutter/actualites-evenements',
'Magyarország'
=> 'https://www.desouttertools.hu/a-desoutter-vallalatrol/hirek-esemenyek',
'Italia'
=> 'https://www.desouttertools.it/su-desoutter/news-eventi',
'日本'
=> 'https://www.desouttertools.jp/desotanituite/niyusu-ibento',
'대한민국'
=> 'https://www.desouttertools.co.kr/desoteoe-daehaeseo/nyuseu-mic-ibenteu',
'Polska'
=> 'https://www.desouttertools.pl/o-desoutter/aktualnosci-wydarzenia',
'Brasil'
=> 'https://www.desouttertools.com.br/sobre-desoutter/noti%C2%ADcias-eventos',
'Portugal'
=> 'https://www.desouttertools.pt/sobre-desoutter/notIcias-eventos',
'România'
=> 'https://www.desouttertools.ro/despre-desoutter/noutati-evenimente',
'Российская Федерация'
=> 'https://www.desouttertools.com.ru/o-desoutter/novosti-mieropriiatiia',
'Slovensko'
=> 'https://www.desouttertools.sk/o-spolocnosti-desoutter/novinky-udalosti',
'Slovenija'
=> 'https://www.desouttertools.si/o-druzbi-desoutter/novice-dogodki',
'Sverige'
=> 'https://www.desouttertools.se/om-desoutter/nyheter-evenemang',
'Türkiye'
=> 'https://www.desoutter.com.tr/desoutter-hakkinda/haberler-etkinlikler',
'中国'
=> 'https://www.desouttertools.com.cn/guan-yu-ma-tou/xin-wen-he-huo-dong',
)
),
),
self::CATEGORY_INDUSTRY => array(
'industry_lang' => array(
'name' => 'Language',
'type' => 'list',
'required' => true,
'title' => 'Select your language',
'defaultValue' => 'Corporate',
'values' => array(
'Corporate'
=> 'https://www.desouttertools.com/industry-4-0/news',
'Česko'
=> 'https://www.desouttertools.cz/prumysl-4-0/novinky',
'Deutschland'
=> 'https://www.desoutter.de/industrie-4-0/news',
'España'
=> 'https://www.desouttertools.es/industria-4-0/noticias',
'México'
=> 'https://www.desouttertools.mx/industria-4-0/noticias',
'France'
=> 'https://www.desouttertools.fr/industrie-4-0/actualites',
'Magyarország'
=> 'https://www.desouttertools.hu/industry-4-0/hirek',
'Italia'
=> 'https://www.desouttertools.it/industry-4-0/news',
'日本'
=> 'https://www.desouttertools.jp/industry-4-0/news',
'대한민국'
=> 'https://www.desouttertools.co.kr/industry-4-0/news',
'Polska'
=> 'https://www.desouttertools.pl/przemysl-4-0/wiadomosci',
'Brasil'
=> 'https://www.desouttertools.com.br/industria-4-0/noticias',
'Portugal'
=> 'https://www.desouttertools.pt/industria-4-0/noticias',
'România'
=> 'https://www.desouttertools.ro/industry-4-0/noutati',
'Российская Федерация'
=> 'https://www.desouttertools.com.ru/industry-4-0/news',
'Slovensko'
=> 'https://www.desouttertools.sk/priemysel-4-0/novinky',
'Slovenija'
=> 'https://www.desouttertools.si/industrija-4-0/novice',
'Sverige'
=> 'https://www.desouttertools.se/industri-4-0/nyheter',
'Türkiye'
=> 'https://www.desoutter.com.tr/endustri-4-0/haberler',
'中国'
=> 'https://www.desouttertools.com.cn/industry-4-0/news',
)
),
),
'global' => array(
'full' => array(
'name' => 'Load full articles',
'type' => 'checkbox',
'required' => false,
'title' => 'Enable to load the full article for each item'
)
)
);
private $title;
public function getURI() {
switch($this->queriedContext) {
case self::CATEGORY_NEWS:
return $this->getInput('news_lang') ?: parent::getURI();
case self::CATEGORY_INDUSTRY:
return $this->getInput('industry_lang') ?: parent::getURI();
}
return parent::getURI();
}
public function getName() {
return isset($this->title) ? $this->title . ' - ' . parent::getName() : parent::getName();
}
public function collectData() {
// Uncomment to generate list of languages automtically (dev mode)
/*
switch($this->queriedContext) {
case self::CATEGORY_NEWS:
$this->extractNewsLanguages(); die;
case self::CATEGORY_INDUSTRY:
$this->extractIndustryLanguages(); die;
}
*/
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request ' . $this->getURI());
$html = defaultLinkTo($html, $this->getURI());
$this->title = html_entity_decode($html->find('title', 0)->plaintext, ENT_QUOTES);
foreach($html->find('article') as $article) {
$item = array();
$item['uri'] = $article->find('[itemprop="name"]', 0)->href;
$item['title'] = $article->find('[itemprop="name"]', 0)->title;
if($this->getInput('full')) {
$item['content'] = $this->getFullNewsArticle($item['uri']);
} else {
$item['content'] = $article->find('[itemprop="description"]', 0)->plaintext;
}
$this->items[] = $item;
}
}
private function getFullNewsArticle($uri) {
$html = getSimpleHTMLDOMCached($uri)
or returnServerError('Unable to load full article!');
$html = defaultLinkTo($html, $this->getURI());
return $html->find('section.article', 0);
}
/**
* Generates a HTML page with a PHP formatted array of languages,
* pointing to the corresponding news pages. Implementation is based
* on the 'Corporate' site.
* @return void
*/
private function extractNewsLanguages() {
$html = getSimpleHTMLDOMCached('https://www.desouttertools.com/about-desoutter/news-events')
or returnServerError('Error loading news!');
$html = defaultLinkTo($html, static::URI);
$items = $html->find('ul[class="dropdown-menu"] li');
$list = "\t'Corporate'\n\t=> 'https://www.desouttertools.com/about-desoutter/news-events',\n";
foreach($items as $item) {
$lang = trim($item->plaintext);
$uri = $item->find('a', 0)->href;
$list .= "\t'{$lang}'\n\t=> '{$uri}',\n";
}
echo $list;
}
/**
* Generates a HTML page with a PHP formatted array of languages,
* pointing to the corresponding news pages. Implementation is based
* on the 'Corporate' site.
* @return void
*/
private function extractIndustryLanguages() {
$html = getSimpleHTMLDOMCached('https://www.desouttertools.com/industry-4-0/news')
or returnServerError('Error loading news!');
$html = defaultLinkTo($html, static::URI);
$items = $html->find('ul[class="dropdown-menu"] li');
$list = "\t'Corporate'\n\t=> 'https://www.desouttertools.com/industry-4-0/news',\n";
foreach($items as $item) {
$lang = trim($item->plaintext);
$uri = $item->find('a', 0)->href;
$list .= "\t'{$lang}'\n\t=> '{$uri}',\n";
}
echo $list;
}
}

105
bridges/DevToBridge.php Normal file
View File

@@ -0,0 +1,105 @@
<?php
class DevToBridge extends BridgeAbstract {
const CONTEXT_BY_TAG = 'By tag';
const NAME = 'dev.to Bridge';
const URI = 'https://dev.to';
const DESCRIPTION = 'Returns feeds for tags';
const MAINTAINER = 'logmanoriginal';
const CACHE_TIMEOUT = 10800; // 15 min.
const PARAMETERS = array(
self::CONTEXT_BY_TAG => array(
'tag' => array(
'name' => 'Tag',
'type' => 'text',
'required' => true,
'title' => 'Insert your tag',
'exampleValue' => 'python'
),
'full' => array(
'name' => 'Full article',
'type' => 'checkbox',
'required' => false,
'title' => 'Enable to receive the full article for each item',
'defaultValue' => false
)
)
);
public function getURI() {
switch($this->queriedContext) {
case self::CONTEXT_BY_TAG:
if($tag = $this->getInput('tag')) {
return static::URI . '/t/' . urlencode($tag);
}
break;
}
return parent::getURI();
}
public function getIcon() {
return 'https://practicaldev-herokuapp-com.freetls.fastly.net/assets/
apple-icon-5c6fa9f2bce280428589c6195b7f1924206a53b782b371cfe2d02da932c8c173.png';
}
public function collectData() {
$html = getSimpleHTMLDOMCached($this->getURI())
or returnServerError('Could not request ' . $this->getURI());
$html = defaultLinkTo($html, static::URI);
$articles = $html->find('div[class="single-article"]')
or returnServerError('Could not find articles!');
foreach($articles as $article) {
if($article->find('[class*="cta"]', 0)) { // Skip ads
continue;
}
$item = array();
$item['uri'] = $article->find('a[id*=article-link]', 0)->href;
$item['title'] = $article->find('h3', 0)->plaintext;
// i.e. "Charlie Harrington・Sep 21"
$item['timestamp'] = strtotime(explode('・', $article->find('h4 a', 0)->plaintext, 2)[1]);
$item['author'] = explode('・', $article->find('h4 a', 0)->plaintext, 2)[0];
// Profile image
$item['enclosures'] = array($article->find('img', 0)->src);
if($this->getInput('full')) {
$fullArticle = $this->getFullArticle($item['uri']);
$item['content'] = <<<EOD
<img src="{$item['enclosures'][0]}" alt="{$item['author']}">
<p>{$fullArticle}</p>
EOD;
} else {
$item['content'] = <<<EOD
<img src="{$item['enclosures'][0]}" alt="{$item['author']}">
<p>{$item['title']}</p>
EOD;
}
$item['categories'] = array_map(function($e){ return $e->plaintext; }, $article->find('div.tags span.tag'));
$this->items[] = $item;
}
}
private function getFullArticle($url) {
$html = getSimpleHTMLDOMCached($url)
or returnServerError('Unable to load article from "' . $url . '"!');
$html = defaultLinkTo($html, static::URI);
return $html->find('[id="article-body"]', 0);
}
}

View File

@@ -75,6 +75,10 @@ class DiceBridge extends BridgeAbstract {
),
));
public function getIcon() {
return 'https://assets.dice.com/techpro/img/favicons/favicon.ico';
}
public function collectData() {
$uri = 'https://www.dice.com/jobs/advancedResult.html';
$uri .= '?for_one=' . urlencode($this->getInput('for_one'));

View File

@@ -7,6 +7,11 @@ class DribbbleBridge extends BridgeAbstract {
const CACHE_TIMEOUT = 1800;
const DESCRIPTION = 'Returns the newest popular shots from Dribbble.';
public function getIcon() {
return 'https://cdn.dribbble.com/assets/
favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI . '/shots')
or returnServerError('Error while downloading the website content');

View File

@@ -94,17 +94,20 @@ class ETTVBridge extends BridgeAbstract {
)
));
protected $results_link;
public function collectData(){
// No control on inputs, because all have defaultValue set
// No control on inputs, because all defaultValue are set
$query_str = 'torrents-search.php';
$query_str .= '?search=' . urlencode('+' . str_replace(' ', ' +', $this->getInput('query')));
$query_str .= '&cat=' . $this->getInput('cat');
$query_str .= 'incldead&=' . $this->getInput('status');
$query_str .= '&incldead=' . $this->getInput('status');
$query_str .= '&lang=' . $this->getInput('lang');
$query_str .= '&sort=id&order=desc';
// Get results page
$html = getSimpleHTMLDOM(self::URI . $query_str)
$this->results_link = self::URI . $query_str;
$html = getSimpleHTMLDOM($this->results_link)
or returnServerError('Could not request ' . $this->getName());
// Loop on each entry
@@ -125,7 +128,7 @@ class ETTVBridge extends BridgeAbstract {
$item = array();
$item['author'] = $details->children(6)->children(1)->plaintext;
$item['title'] = $entry->title;
$item['uri'] = $dllinks->children(0)->children(0)->children(0)->href;
$item['uri'] = $link;
$item['timestamp'] = strtotime($details->children(7)->children(1)->plaintext);
$item['content'] = '';
$item['content'] .= '<br/><b>Name: </b>' . $details->children(0)->children(1)->innertext;
@@ -139,4 +142,20 @@ class ETTVBridge extends BridgeAbstract {
$this->items[] = $item;
}
}
public function getName(){
if($this->getInput('query')) {
return '[' . self::NAME . '] ' . $this->getInput('query');
}
return self::NAME;
}
public function getURI(){
if(isset($this->results_link) && !empty($this->results_link)) {
return $this->results_link;
}
return self::URI;
}
}

View File

@@ -7,6 +7,11 @@ class EliteDangerousGalnetBridge extends BridgeAbstract {
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Returns the latest page of news from Galnet';
public function getIcon() {
return 'https://community.elitedangerous.com/sites/
EDSITE_COMM/themes/bootstrap/bootstrap_community/favicon.ico';
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Error while downloading the website content');

View File

@@ -121,7 +121,7 @@ class ElloBridge extends BridgeAbstract {
private function getAPIKey() {
$cache = Cache::create('FileCache');
$cache->setPath(CACHE_DIR);
$cache->setPath(PATH_CACHE);
$cache->setParameters(['key']);
$key = $cache->loadData();

View File

@@ -57,6 +57,10 @@ class ElsevierBridge extends BridgeAbstract {
return '';
}
public function getIcon() {
return 'https://cdn.elsevier.io/verona/includes/favicons/favicon-32x32.png';
}
public function collectData(){
$uri = self::URI . $this->getInput('j') . '/recent-articles/';
$html = getSimpleHTMLDOM($uri)

View File

@@ -17,7 +17,7 @@ class EtsyBridge extends BridgeAbstract {
'queryextension' => array(
'name' => 'Query extension',
'type' => 'text',
'requied' => false,
'required' => false,
'title' => 'Insert additional query parts here
(anything after ?search=<your search query>)',
'exampleValue' => '&explicit=1&locationQuery=2921044'
@@ -25,9 +25,9 @@ class EtsyBridge extends BridgeAbstract {
'showimage' => array(
'name' => 'Show image in content',
'type' => 'checkbox',
'requrired' => false,
'required' => false,
'title' => 'Activate to show the image in the content',
'defaultValue' => false
'defaultValue' => 'checked'
)
)
);
@@ -36,26 +36,27 @@ class EtsyBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Failed to receive ' . $this->getURI());
$results = $html->find('div.block-grid-item');
$results = $html->find('li.block-grid-item');
foreach($results as $result) {
// Skip banner cards (ads for categories)
if($result->find('a.banner-card'))
if($result->find('span.ad-indicator'))
continue;
$item = array();
$item['title'] = $result->find('a', 0)->title;
$item['uri'] = $result->find('a', 0)->href;
$item['author'] = $result->find('div.card-shop-name', 0)->plaintext;
$item['author'] = $result->find('p.text-gray-lighter', 0)->plaintext;
$item['content'] = '<p>'
. $result->find('div.card-price', 0)->plaintext
. $result->find('span.currency-value', 0)->plaintext . ' '
. $result->find('span.currency-symbol', 0)->plaintext
. '</p><p>'
. $result->find('div.card-title', 0)->plaintext
. $result->find('a', 0)->title
. '</p>';
$image = $result->find('img.placeholder', 0)->src;
$image = $result->find('img.display-block', 0)->src;
if($this->getInput('showimage')) {
$item['content'] .= '<img src="' . $image . '">';

View File

@@ -0,0 +1,104 @@
<?php
class ExtremeDownloadBridge extends BridgeAbstract {
const NAME = 'Extreme Download';
const URI = 'https://ww1.extreme-d0wn.com/';
const DESCRIPTION = 'Suivi de série sur Extreme Download';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
'Suivre la publication des épisodes d\'une série en cours de diffusion' => array(
'url' => array(
'name' => 'URL de la série',
'type' => 'text',
'required' => true,
'title' => 'URL d\'une série sans le https://ww1.extreme-d0wn.com/',
'exampleValue' => 'series-hd/hd-series-vostfr/46631-halt-and-catch-fire-saison-04-vostfr-hdtv-720p.html'),
'filter' => array(
'name' => 'Type de contenu',
'type' => 'list',
'required' => 'true',
'title' => 'Type de contenu à suivre : Téléchargement, Streaming ou les deux',
'values' => array(
'Streaming et Téléchargement' => 'both',
'Téléchargement' => 'download',
'Streaming' => 'streaming'
)
)
)
);
public function collectData(){
$html = getSimpleHTMLDOM(self::URI . $this->getInput('url'))
or returnServerError('Could not request Extreme Download.');
$filter = $this->getInput('filter');
$typesText = array(
'download' => 'Téléchargement',
'streaming' => 'Streaming'
);
// Get the TV show title
$this->showTitle = trim($html->find('span[id=news-title]', 0)->plaintext);
$list = $html->find('div[class=prez_7]');
foreach($list as $element) {
$add = false;
// Link type is needed is needed to generate an unique link
$type = $this->findLinkType($element);
if($filter == 'both') {
$add = true;
} else {
if($type == $filter) {
$add = true;
}
}
if($add == true) {
$item = array();
// Get the element name
$title = $element->plaintext;
// Get thee element links
$links = $element->next_sibling()->innertext;
$item['content'] = $links;
$item['title'] = $this->showTitle . ' ' . $title . ' - ' . $typesText[$type];
// As RSS Bridge use the URI as GUID they need to be unique : adding a md5 hash of the title element
// should geneerate unique URI to prevent confusion for RSS readers
$item['uri'] = self::URI . $this->getInput('url') . '#' . hash('md5', $item['title']);
$this->items[] = $item;
}
}
}
public function getName(){
switch($this->queriedContext) {
case 'Suivre la publication des épisodes d\'une série en cours de diffusion':
return $this->showTitle . ' - ' . self::NAME;
break;
default:
return self::NAME;
}
}
private function findLinkType($element)
{
$return = '';
// Walk through all elements in the reverse order until finding one with class 'presz_2'
while($element->class != 'prez_2') {
$element = $element->prev_sibling();
}
$text = html_entity_decode($element->plaintext);
// Regarding the text of the element, return the according link type
if(stristr($text, 'téléchargement') != false) {
$return = 'download';
} else if(stristr($text, 'streaming') != false) {
$return = 'streaming';
}
return $return;
}
}

View File

@@ -15,24 +15,18 @@ class FB2Bridge extends BridgeAbstract {
)
));
public function getIcon() {
return 'https://static.xx.fbcdn.net/rsrc.php/yo/r/iRmz9lCMBD2.ico';
}
public function collectData(){
function extractFromDelimiters($string, $start, $end){
if(strpos($string, $start) !== false) {
$section_retrieved = substr($string, strpos($string, $start) + strlen($start));
$section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
return $section_retrieved;
}
return false;
}
//Utility function for cleaning a Facebook link
$unescape_fb_link = function($matches){
if(is_array($matches) && count($matches) > 1) {
$link = $matches[1];
if(strpos($link, '/') === 0)
$link = self::URI . $link . '"';
$link = self::URI . substr($link, 1);
if(strpos($link, 'facebook.com/l.php?u=') !== false)
$link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&'));
return ' href="' . $link . '"';
@@ -75,14 +69,14 @@ class FB2Bridge extends BridgeAbstract {
if($this->getInput('u') !== null) {
$page = 'https://touch.facebook.com/' . $this->getInput('u');
$cookies = $this->getCookies($page);
$pageID = $this->getPageID($page, $cookies);
$pageInfo = $this->getPageInfos($page, $cookies);
if($pageID === null) {
if($pageInfo['userId'] === null) {
echo <<<EOD
Unable to get the page id. You should consider getting the ID by hand, then importing it into FB2Bridge
EOD;
die();
} elseif($pageID == -1) {
} elseif($pageInfo['userId'] == -1) {
echo <<<EOD
This page is not accessible without being logged in.
EOD;
@@ -91,24 +85,31 @@ EOD;
}
//Build the string for the first request
$requestString = 'https://touch.facebook.com/pages_reaction_units/more/?page_id='
. $pageID
. '&cursor={"card_id"%3A"videos"%2C"has_next_page"%3Atrue}&surface=mobile_page_home&unit_count=8';
$requestString = 'https://touch.facebook.com/page_content_list_view/more/?page_id='
. $pageInfo['userId']
. '&start_cursor=1&num_to_fetch=105&surface_type=timeline';
$fileContent = getContents($requestString);
$articleIndex = 0;
$maxArticle = 3;
$html = $this->buildContent($fileContent);
$author = $this->getInput('u');
$author = $pageInfo['username'];
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];
else
$timestamp = 0;
$item['uri'] = 'http://touch.facebook.com'
. $content->find("div[class='_52jc _5qc4 _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href');
$item['uri'] = html_entity_decode('http://touch.facebook.com'
. $content->find("div[class='_52jc _5qc4 _78cz _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href'), ENT_QUOTES);
//Decode images
$imagecleaned = preg_replace_callback('/<i [^>]* style="[^"]*url\(\'(.*?)\'\).*?><\/i>/m', function ($matches) {
return "<img src='" . str_replace(['\\3a ', '\\3d ', '\\26 '], [':', '=', '&'], $matches[1]) . "' />";
}, $content);
$content = str_get_html($imagecleaned);
if($content->find('header', 0) !== null) {
$content->find('header', 0)->innertext = '';
@@ -118,8 +119,13 @@ EOD;
$content->find('footer', 0)->innertext = '';
}
// Replace emoticon images by their textual representation (part of the span)
foreach($content->find('span[title*="emoticon"]') as $emoticon) {
$emoticon->innertext = $emoticon->find('span[aria-hidden="true"]', 0)->innertext;
}
//Remove html nodes, keep only img, links, basic formatting
$content = strip_tags($content, '<a><img><i><u><br><p>');
$content = strip_tags($content, '<a><img><i><u><br><p><h3><h4><section>');
//Adapt link hrefs: convert relative links into absolute links and bypass external link redirection
$content = preg_replace_callback('/ href=\"([^"]+)\"/i', $unescape_fb_link, $content);
@@ -132,7 +138,6 @@ EOD;
'ajaxify',
'tabindex',
'class',
'style',
'data-[^=]*',
'aria-[^=]*',
'role',
@@ -145,7 +150,36 @@ EOD;
// "<i><u>smile emoticon</u></i>" back to ASCII emoticons eg ":)"
$content = preg_replace_callback('/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i', $unescape_fb_emote, $content);
$item['content'] = $content;
//Remove the "...Plus" tag
$content = preg_replace(
'/… (<span>|)<a href="https:\/\/www\.facebook\.com\/story\.php\?story_fbid=.*?<\/a>/m',
'', $content, 1);
//Remove tracking images
$content = preg_replace('/<img src=\'.*?safe_image\.php.*?\' \/>/m', '', $content);
//Remove the double section tags
$content = str_replace(['<section><section>', '</section></section>'], ['<section>', '</section>'], $content);
//Move the section tag link upper, if it is down
$content = str_get_html($content);
$sectionContent = $content->find('section', 0);
if($sectionContent != null) {
$sectionLink = $sectionContent->nextSibling();
if($sectionLink != null) {
$fullLink = '<a href="' . $sectionLink->getAttribute('href') . '">' . $sectionContent->innertext . '</a>';
$sectionContent->innertext = $fullLink;
}
}
//Move the href tag upper if it is inside the section
foreach($content->find('section > a') as $sectionToFix) {
$sectionLink = $sectionToFix->getAttribute('href');
$section = $sectionToFix->parent();
$section->outertext = '<a href="' . $sectionLink . '">' . $section . '</a>';
}
$item['content'] = html_entity_decode($content, ENT_QUOTES);
$title = $author;
if (strlen($title) > 24)
@@ -154,57 +188,29 @@ EOD;
if (strlen($title) > 64)
$title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
$item['title'] = $title;
$item['author'] = $author;
$item['title'] = html_entity_decode($title, ENT_QUOTES);
$item['author'] = html_entity_decode($author, ENT_QUOTES);
$item['timestamp'] = html_entity_decode($timestamp, ENT_QUOTES);
if($item['timestamp'] != 0)
array_push($this->items, $item);
}
}
// Currently not used. Is used to get more than only 3 elements, as they appear on another page.
private function computeNextLink($string, $pageID){
$regex = implode(
'',
array(
'/timeline_unit',
"\\\\\\\\u00253A1",
"\\\\\\\\u00253A([0-9]*)",
"\\\\\\\\u00253A([0-9]*)",
"\\\\\\\\u00253A([0-9]*)",
"\\\\\\\\u00253A([0-9]*)/"
)
);
preg_match($regex, $string, $result);
return implode(
'',
array(
'https://touch.facebook.com/pages_reaction_units/more/?page_id=',
$pageID,
'&cursor=%7B%22timeline_cursor%22%3A%22timeline_unit%3A1%3A',
$result[1],
'%3A',
$result[2],
'%3A',
$result[3],
'%3A',
$result[4],
'%22%2C%22timeline_section_cursor%22%3A%7B%7D%2C%22',
'has_next_page%22%3Atrue%7D&surface=mobile_page_home&unit_count=3'
)
);
}
//Builds the HTML from the encoded JS that Facebook provides.
private function buildContent($pageContent){
// The html ends with:
// /div>","replaceifexists
$regex = '/\\"html\\":(\".+\/div>"),"replace/';
preg_match($regex, $pageContent, $result);
return str_get_html(html_entity_decode(json_decode($result[1])));
$htmlContent = json_decode($result[1]);
$htmlContent = preg_replace('/(?<!style)="(.*?)"/', '=\'$1\'', $htmlContent);
$htmlContent = html_entity_decode($htmlContent, ENT_QUOTES, 'UTF-8');
return str_get_html($htmlContent);
}
@@ -234,8 +240,8 @@ EOD;
return substr($cookies, 1);
}
//Get the page ID from the Facebook page.
private function getPageID($page, $cookies){
//Get the page ID and username from the Facebook page.
private function getPageInfos($page, $cookies){
$context = stream_context_create(array(
'http' => array(
@@ -251,19 +257,28 @@ EOD;
return -1;
}
//Get the username
$usernameRegex = '/data-nt=\"FB:TEXT4\">(.*?)<\/div>/m';
preg_match($usernameRegex, $pageContent, $usernameMatches);
if(count($usernameMatches) > 0) {
$username = strip_tags($usernameMatches[1]);
} else {
$username = $this->getInput('u');
}
//Get the page ID if we don't have a captcha
$regex = '/page_id=([0-9]*)&/';
preg_match($regex, $pageContent, $matches);
if(count($matches) > 0) {
return $matches[1];
return array('userId' => $matches[1], 'username' => $username);
}
//Get the page ID if we do have a captcha
$regex = '/"pageID":"([0-9]*)"/';
preg_match($regex, $pageContent, $matches);
return $matches[1];
return array('userId' => $matches[1], 'username' => $username);
}

View File

@@ -19,6 +19,10 @@ class FDroidBridge extends BridgeAbstract {
)
));
public function getIcon() {
return self::URI . 'assets/favicon.ico?v=8j6PKzW9Mk';
}
public function collectData(){
$url = self::URI;
$html = getSimpleHTMLDOM($url)

View File

@@ -2,7 +2,7 @@
class FacebookBridge extends BridgeAbstract {
const MAINTAINER = 'teromene, logmanoriginal';
const NAME = 'Facebook';
const NAME = 'Facebook Bridge';
const URI = 'https://www.facebook.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
@@ -41,23 +41,75 @@ class FacebookBridge extends BridgeAbstract {
'exampleValue' => 'https://www.facebook.com/groups/743149642484225',
'title' => 'Insert group name or facebook group URL'
)
),
'global' => array(
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'required' => false,
'title' => 'Specify the number of items to return (default: -1)',
'defaultValue' => -1
)
)
);
private $authorName = '';
private $groupName = '';
public function getIcon() {
return 'https://static.xx.fbcdn.net/rsrc.php/yo/r/iRmz9lCMBD2.ico';
}
public function getName(){
switch($this->queriedContext) {
case 'User':
if(!empty($this->authorName)) {
return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName
. ' - ' . static::NAME;
}
break;
case 'Group':
if(!empty($this->groupName)) {
return $this->groupName . ' - ' . static::NAME;
}
break;
}
return parent::getName();
}
public function getURI() {
$uri = self::URI;
switch($this->queriedContext) {
case 'Group':
// Discover groups via https://www.facebook.com/groups/
// Example group: https://www.facebook.com/groups/sailors.worldwide
$uri .= 'groups/' . $this->sanitizeGroup(filter_var($this->getInput('g'), FILTER_SANITIZE_URL));
break;
case 'User':
// Example user 1: https://www.facebook.com/artetv/
// Example user 2: artetv
$user = $this->sanitizeUser($this->getInput('u'));
if(!strpos($user, '/')) {
$uri .= urlencode($user) . '/posts';
} else {
$uri .= 'pages/' . $user;
}
break;
}
// Request the mobile version to reduce page size (no javascript)
// More information: https://stackoverflow.com/a/11103592
return $uri .= '?_fb_noscript=1';
}
@@ -78,6 +130,12 @@ class FacebookBridge extends BridgeAbstract {
}
$limit = $this->getInput('limit') ?: -1;
if($limit > 0 && count($this->items) > $limit) {
$this->items = array_slice($this->items, 0, $limit);
}
}
#region Group
@@ -249,24 +307,89 @@ class FacebookBridge extends BridgeAbstract {
}
#endregion
#endregion (Group)
private function collectUserData(){
#region User
//Utility function for cleaning a Facebook link
$unescape_fb_link = function($matches){
/**
* Checks if $user is a valid username or URI and returns the username
*/
private function sanitizeUser($user) {
if (filter_var($user, FILTER_VALIDATE_URL)) {
$urlparts = parse_url($user);
if($urlparts['host'] !== parse_url(self::URI)['host']) {
returnClientError('The host you provided is invalid! Received "'
. $urlparts['host']
. '", expected "'
. parse_url(self::URI)['host']
. '"!');
}
if(!array_key_exists('path', $urlparts)
|| $urlparts['path'] === '/') {
returnClientError('The URL you provided doesn\'t contain the user name!');
}
return explode('/', $urlparts['path'])[1];
} else {
// First character cannot be a forward slash
if(strpos($user, '/') === 0) {
returnClientError('Remove leading slash "/" from the username!');
}
return $user;
}
}
/**
* Bypass external link redirection
*/
private function unescape_fb_link($content){
return preg_replace_callback('/ href=\"([^"]+)\"/i', function($matches){
if(is_array($matches) && count($matches) > 1) {
$link = $matches[1];
if(strpos($link, '/') === 0)
$link = self::URI . $link;
if(strpos($link, 'facebook.com/l.php?u=') !== false)
$link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&'));
return ' href="' . $link . '"';
}
};
//Utility function for converting facebook emoticons
$unescape_fb_emote = function($matches){
return ' href="' . $link . '"';
}
}, $content);
}
/**
* Remove Facebook's tracking code
*/
private function remove_tracking_codes($content){
return preg_replace_callback('/ href=\"([^"]+)\"/i', function($matches){
if(is_array($matches) && count($matches) > 1) {
$link = $matches[1];
if(strpos($link, 'facebook.com') !== false) {
if(strpos($link, '?') !== false) {
$link = substr($link, 0, strpos($link, '?'));
}
}
return ' href="' . $link . '"';
}
}, $content);
}
/**
* Convert textual representation of emoticons back to ASCII emoticons.
* i.e. "<i><u>smile emoticon</u></i>" => ":)"
*/
private function unescape_fb_emote($content){
return preg_replace_callback('/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i', function($matches){
static $facebook_emoticons = array(
'smile' => ':)',
'frown' => ':(',
@@ -289,106 +412,42 @@ class FacebookBridge extends BridgeAbstract {
'upset' => 'xD',
'colonthree' => ':3',
'like' => '&#x1F44D;');
$len = count($matches);
if ($len > 1)
for ($i = 1; $i < $len; $i++)
foreach ($facebook_emoticons as $name => $emote)
if ($matches[$i] === $name)
return $emote;
return $matches[0];
};
$html = null;
//Handle captcha response sent by the viewer
if (isset($_POST['captcha_response'])) {
if (session_status() == PHP_SESSION_NONE)
session_start();
if (isset($_SESSION['captcha_fields'], $_SESSION['captcha_action'])) {
$captcha_action = $_SESSION['captcha_action'];
$captcha_fields = $_SESSION['captcha_fields'];
$captcha_fields['captcha_response'] = preg_replace('/[^a-zA-Z0-9]+/', '', $_POST['captcha_response']);
$header = array("Content-type:
application/x-www-form-urlencoded\r\nReferer: $captcha_action\r\nCookie: noscript=1\r\n");
$opts = array(
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => http_build_query($captcha_fields)
);
$html = getContents($captcha_action, $header, $opts);
if($html === false) {
returnServerError('Failed to submit captcha response back to Facebook');
}
unset($_SESSION['captcha_fields']);
$html = str_get_html($html);
}
unset($_SESSION['captcha_fields']);
unset($_SESSION['captcha_action']);
}, $content);
}
//Retrieve page contents
if(is_null($html)) {
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n");
// Check if the user provided a fully qualified URL
if (filter_var($this->getInput('u'), FILTER_VALIDATE_URL)) {
$urlparts = parse_url($this->getInput('u'));
if($urlparts['host'] !== parse_url(self::URI)['host']) {
returnClientError('The host you provided is invalid! Received "'
. $urlparts['host']
. '", expected "'
. parse_url(self::URI)['host']
. '"!');
}
if(!array_key_exists('path', $urlparts)
|| $urlparts['path'] === '/') {
returnClientError('The URL you provided doesn\'t contain the user name!');
}
$user = explode('/', $urlparts['path'])[1];
$html = getSimpleHTMLDOM(self::URI . urlencode($user) . '?_fb_noscript=1', $header)
or returnServerError('No results for this query.');
} else {
// First character cannot be a forward slash
if(strpos($this->getInput('u'), '/') === 0) {
returnClientError('Remove leading slash "/" from the username!');
}
if(!strpos($this->getInput('u'), '/')) {
$html = getSimpleHTMLDOM(self::URI . urlencode($this->getInput('u')) . '?_fb_noscript=1', $header)
or returnServerError('No results for this query.');
} else {
$html = getSimpleHTMLDOM(self::URI . 'pages/' . $this->getInput('u') . '?_fb_noscript=1', $header)
or returnServerError('No results for this query.');
}
}
}
//Handle captcha form?
$captcha = $html->find('div.captcha_interstitial', 0);
if (!is_null($captcha)) {
/**
* Returns the captcha message for the given captcha
*/
private function returnCaptchaMessage($captcha) {
// Save form for submitting after getting captcha response
if (session_status() == PHP_SESSION_NONE)
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$captcha_fields = array();
foreach ($captcha->find('input, button') as $input)
foreach ($captcha->find('input, button') as $input) {
$captcha_fields[$input->name] = $input->value;
}
$_SESSION['captcha_fields'] = $captcha_fields;
$_SESSION['captcha_action'] = $captcha->find('form', 0)->action;
// Show captcha filling form to the viewer, proxying the captcha image
$img = base64_encode(getContents($captcha->find('img', 0)->src));
http_response_code(500);
header('Content-Type: text/html');
header('Content-Type: text/html', true, 500);
$message = <<<EOD
<form method="post" action="?{$_SERVER['QUERY_STRING']}">
<h2>Facebook captcha challenge</h2>
@@ -399,12 +458,73 @@ application/x-www-form-urlencoded\r\nReferer: $captcha_action\r\nCookie: noscrip
<input type="submit" value="Submit!" /></p>
</form>
EOD;
die($message);
}
/**
* Checks if a capture response was received and tries to load the contents
* @return mixed null if no capture response was received, simplhtmldom document otherwise
*/
private function handleCaptchaResponse() {
if (isset($_POST['captcha_response'])) {
if (session_status() == PHP_SESSION_NONE)
session_start();
if (isset($_SESSION['captcha_fields'], $_SESSION['captcha_action'])) {
$captcha_action = $_SESSION['captcha_action'];
$captcha_fields = $_SESSION['captcha_fields'];
$captcha_fields['captcha_response'] = preg_replace('/[^a-zA-Z0-9]+/', '', $_POST['captcha_response']);
$header = array(
'Content-type: application/x-www-form-urlencoded',
'Referer: ' . $captcha_action,
'Cookie: noscript=1'
);
$opts = array(
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => http_build_query($captcha_fields)
);
$html = getSimpleHTMLDOM($captcha_action, $header, $opts)
or returnServerError('Failed to submit captcha response back to Facebook');
return $html;
}
unset($_SESSION['captcha_fields']);
unset($_SESSION['captcha_action']);
}
return null;
}
private function collectUserData(){
$html = $this->handleCaptchaResponse();
// Retrieve page contents
if(is_null($html)) {
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE'));
$html = getSimpleHTMLDOM($this->getURI(), $header)
or returnServerError('No results for this query.');
}
// Handle captcha form?
$captcha = $html->find('div.captcha_interstitial', 0);
if (!is_null($captcha)) {
$this->returnCaptchaMessage($captcha);
}
// No captcha? We can carry on retrieving page contents :)
// First, we check wether the page is public or not
$loginForm = $html->find('._585r', 0);
if($loginForm != null) {
returnServerError('You must be logged in to view this page. This is not supported by RSS-Bridge.');
}
@@ -413,18 +533,14 @@ EOD;
->find('#pagelet_timeline_main_column')[0]
->children(0)
->children(0)
->children(0)
->next_sibling()
->children(0);
if(isset($element)) {
defaultLinkTo($element, self::URI);
$author = str_replace(' - Posts | Facebook', '', $html->find('title#pageTitle', 0)->innertext);
$author = str_replace(' | Facebook', '', $html->find('title#pageTitle', 0)->innertext);
$profilePic = 'https://graph.facebook.com/'
. $this->getInput('u')
. '/picture?width=200&amp;height=200#.image';
$profilePic = $html->find('meta[property="og:image"]', 0)->content;
$this->authorName = $author;
@@ -459,16 +575,30 @@ EOD;
if(count($post->find('abbr')) > 0) {
//Retrieve post contents
$content = preg_replace(
'/(?i)><div class=\"clearfix([^>]+)>(.+?)div\ class=\"userContent\"/i',
'',
$post);
$content = $post->find('.userContentWrapper', 0);
$content = preg_replace(
'/(?i)><div class=\"_59tj([^>]+)>(.+?)<\/div><\/div><a/i',
'',
$content);
// This array specifies filters applied to all posts in order of appearance
$content_filters = array(
'._5mly', // Remove embedded videos (the preview image remains)
'._2ezg', // Remove "Views ..."
'.hidden_elem', // Remove hidden elements (they are hidden anyway)
);
foreach($content_filters as $filter) {
foreach($content->find($filter) as $subject) {
$subject->outertext = '';
}
}
// Change origin tag for embedded media from div to paragraph
foreach($content->find('._59tj') as $subject) {
$subject->outertext = '<p>' . $subject->innertext . '</p>';
}
// Change title tag for embedded media from anchor to paragraph
foreach($content->find('._3n1k a') as $anchor) {
$anchor->outertext = '<p>' . $anchor->innertext . '</p>';
}
$content = preg_replace(
'/(?i)><div class=\"_3dp([^>]+)>(.+?)div\ class=\"[^u]+userContent\"/i',
@@ -489,8 +619,7 @@ EOD;
// Remove html nodes, keep only img, links, basic formatting
$content = strip_tags($content, '<a><img><i><u><br><p>');
//Adapt link hrefs: convert relative links into absolute links and bypass external link redirection
$content = preg_replace_callback('/ href=\"([^"]+)\"/i', $unescape_fb_link, $content);
$content = $this->unescape_fb_link($content);
// Clean useless html tag properties and fix link closing tags
foreach (array(
@@ -505,31 +634,33 @@ EOD;
'aria-[^=]*',
'role',
'rel',
'id') as $property_name)
'id') as $property_name) {
$content = preg_replace('/ ' . $property_name . '=\"[^"]*\"/i', '', $content);
}
$content = preg_replace('/<\/a [^>]+>/i', '</a>', $content);
//Convert textual representation of emoticons eg
//"<i><u>smile emoticon</u></i>" back to ASCII emoticons eg ":)"
$content = preg_replace_callback(
'/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i',
$unescape_fb_emote,
$content
);
$this->unescape_fb_emote($content);
// Restore links in the post before further parsing
$post = defaultLinkTo($post, self::URI);
// Restore links in the content before adding to the item
$content = defaultLinkTo($content, self::URI);
$content = $this->remove_tracking_codes($content);
// Retrieve date of the post
$date = $post->find('abbr')[0];
if(isset($date) && $date->hasAttribute('data-utime')) {
$date = $date->getAttribute('data-utime');
} else {
$date = 0;
}
//Build title from username and content
$title = $author;
if(strlen($title) > 24)
$title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...';
$title = $title . ' | ' . strip_tags($content);
// Build title from content
$title = strip_tags($post->find('.userContent', 0)->innertext);
if(strlen($title) > 64)
$title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
@@ -540,13 +671,15 @@ EOD;
}
//Build and add final item
$item['uri'] = htmlspecialchars_decode($uri);
$item['content'] = htmlspecialchars_decode($content);
$item['title'] = $title;
$item['author'] = $author;
$item['uri'] = htmlspecialchars_decode($uri, ENT_QUOTES);
$item['content'] = htmlspecialchars_decode($content, ENT_QUOTES);
$item['title'] = htmlspecialchars_decode($title, ENT_QUOTES);
$item['author'] = htmlspecialchars_decode($author, ENT_QUOTES);
$item['timestamp'] = $date;
if(strpos($item['content'], '<img') === false)
if(strpos($item['content'], '<img') === false) {
$item['enclosures'] = array($profilePic);
}
$this->items[] = $item;
}
@@ -555,25 +688,6 @@ EOD;
}
}
public function getName(){
switch($this->queriedContext) {
case 'User':
if(!empty($this->authorName)) {
return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName
. ' - Facebook Bridge';
}
break;
case 'Group':
if(!empty($this->groupName)) {
return $this->groupName . ' - Facebook Bridge';
}
break;
#endregion (User)
}
return parent::getName();
}
}

View File

@@ -7,6 +7,10 @@ class FierPandaBridge extends BridgeAbstract {
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Returns latest articles from Fier Panda.';
public function getIcon() {
return self::URI . 'wp-content/themes/fier-panda/img/favicon.png';
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI)

View File

@@ -94,7 +94,7 @@ class FilterBridge extends FeedExpander {
}
try{
$this->collectExpandableDatas($this->getURI());
} catch (HttpException $e) {
} catch (Exception $e) {
$this->collectExpandableDatas($this->getURI());
}
}

View File

@@ -0,0 +1,82 @@
<?php
class FindACrewBridge extends BridgeAbstract {
const MAINTAINER = 'couraudt';
const NAME = 'Find A Crew Bridge';
const URI = 'https://www.findacrew.net';
const DESCRIPTION = 'Returns the newest sailing offers.';
const PARAMETERS = array(
array(
'type' => array(
'name' => 'Type of search',
'title' => 'Choose between finding a boat or a crew',
'type' => 'list',
'values' => array(
'Find a boat' => 'boat',
'Find a crew' => 'crew'
)
),
'long' => array(
'name' => 'Longitude of the searched location',
'title' => 'Center the search at that longitude (e.g: -42.02)'
),
'lat' => array(
'name' => 'Latitude of the searched location',
'title' => 'Center the search at that latitude (e.g: 12.42)'
),
'distance' => array(
'name' => 'Limit boundary of search in KM',
'title' => 'Boundary of the search in kilometers when using longitude and latitude'
)
)
);
public function collectData() {
$url = $this->getURI();
if ($this->getInput('type') == 'boat') {
$data = array('SrhLstBtAction' => 'Create');
} else {
$data = array('SrhLstCwAction' => 'Create');
}
if ($this->getInput('long') && $this->getInput('lat')) {
$data['real_LocSrh_Lng'] = $this->getInput('long');
$data['real_LocSrh_Lat'] = $this->getInput('lat');
if ($this->getInput('distance')) {
$data['LocDis'] = (int)$this->getInput('distance') * 1000;
}
}
$header = array(
'Content-Type: application/x-www-form-urlencoded'
);
$opts = array(
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => http_build_query($data) . "\n"
);
$html = getSimpleHTMLDOM($url, $header, $opts) or returnClientError('No results for this query.');
$annonces = $html->find('.css_SrhRst');
foreach ($annonces as $annonce) {
$item = array();
$img = parent::getURI() . $annonce->find('.css_LstPic img', 0)->getAttribute('src');
$item['title'] = $annonce->find('.css_LstCtrls span', 0)->plaintext;
$item['uri'] = parent::getURI() . $annonce->find('.css_PnlCtrls a', 0)->href;
$content = $annonce->find('.css_LstDtl div', 2)->innertext;
$item['content'] = "<img src='$img' /><br>$content";
$item['enclosures'] = array($img);
$item['categories'] = array($annonce->find('.css_AccLocCur', 0)->plaintext);
$this->items[] = $item;
}
}
public function getURI() {
$uri = parent::getURI();
// Those params must be in the URL
$uri .= '/en/' . $this->getInput('type') . '/search?srhtyp=srhrst&mdl=2';
return $uri;
}
}

View File

@@ -0,0 +1,119 @@
<?php
/**
* An extension of the previous SexactuBridge to cover the whole GQMagazine.
* This one taks a page (as an example sexe/news or journaliste/maia-mazaurette) which is to be configured,
* reads all the articles visible on that page, and make a stream out of it.
* @author nicolas-delsaux
*
*/
class GQMagazineBridge extends BridgeAbstract
{
const MAINTAINER = 'Riduidel';
const NAME = 'GQMagazine';
// URI is no more valid, since we can address the whole gq galaxy
const URI = 'https://www.gqmagazine.fr';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'GQMagazine section extractor bridge. This bridge allows you get only a specific section.';
const PARAMETERS = array( array(
'domain' => array(
'name' => 'Domain to use',
'required' => true,
'values' => array(
'www.gqmagazine.fr' => 'www.gqmagazine.fr'
),
'defaultValue' => 'www.gqmagazine.fr'
),
'page' => array(
'name' => 'Initial page to load',
'required' => true
),
));
const REPLACED_ATTRIBUTES = array(
'href' => 'href',
'src' => 'src',
'data-original' => 'src'
);
private function getDomain() {
return $this->getInput('domain');
}
public function getURI()
{
return $this->getDomain() . '/' . $this->getInput('page');
}
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI()) or returnServerError('Could not request ' . $this->getURI());
// Since GQ don't want simple class scrapping, let's do it the hard way and ... discover content !
$main = $html->find('main', 0);
foreach ($main->find('a') as $link) {
$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;
}
$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;
}
}
/**
* Loads the full article and returns the contents
* @param $uri The article URI
* @return The article content
*/
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;
}
/**
* Replaces all relative URIs with absolute ones
* @param $element A simplehtmldom element
* @return The $element->innertext with all URIs replaced
*/
private function replaceUriInHtmlElement($element){
$returned = $element->innertext;
foreach (self::REPLACED_ATTRIBUTES as $initial => $final) {
$returned = str_replace($initial . '="/', $final . '="' . self::URI . '/', $returned);
}
return $returned;
}
}

View File

@@ -54,7 +54,7 @@ class GitHubGistBridge extends BridgeAbstract {
DEFAULT_SPAN_TEXT)
or returnServerError('Could not request ' . $this->getURI());
$html = defaultLinkTo($html, static::URI);
$html = defaultLinkTo($html, $this->getURI());
$fileinfo = $html->find('[class="file-info"]', 0)
or returnServerError('Could not find file info!');
@@ -69,7 +69,7 @@ class GitHubGistBridge extends BridgeAbstract {
foreach($comments as $comment) {
$uri = $comment->find('a[href^=#gistcomment]', 0)
$uri = $comment->find('a[href*=#gistcomment]', 0)
or returnServerError('Could not find comment anchor!');
$title = $comment->find('div[class="unminimized-comment"] h3[class="timeline-comment-header-text"]', 0)
@@ -86,7 +86,7 @@ class GitHubGistBridge extends BridgeAbstract {
$item = array();
$item['uri'] = $this->getURI() . $uri->href;
$item['uri'] = $uri->href;
$item['title'] = str_replace('commented', 'commented on', $title->plaintext);
$item['timestamp'] = strtotime($datetime->datetime);
$item['author'] = '<a href="' . $author->href . '">' . $author->plaintext . '</a>';

View File

@@ -37,10 +37,9 @@ class GithubIssueBridge extends BridgeAbstract {
$name = $this->getInput('u') . '/' . $this->getInput('p');
switch($this->queriedContext) {
case 'Project Issues':
$prefix = static::NAME . 's for ';
if($this->getInput('c')) {
$prefix = static::NAME . 's comments for ';
} else {
$prefix = static::NAME . 's for ';
}
$name = $prefix . $name;
break;
@@ -53,8 +52,9 @@ class GithubIssueBridge extends BridgeAbstract {
}
public function getURI(){
if(!is_null($this->getInput('u')) && !is_null($this->getInput('p'))) {
$uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p') . '/issues';
if(null !== $this->getInput('u') && null !== $this->getInput('p')) {
$uri = static::URI . $this->getInput('u') . '/'
. $this->getInput('p') . '/issues';
if($this->queriedContext === 'Issue comments') {
$uri .= '/' . $this->getInput('i');
} elseif($this->getInput('c')) {
@@ -66,54 +66,54 @@ class GithubIssueBridge extends BridgeAbstract {
return parent::getURI();
}
protected function extractIssueComment($issueNbr, $title, $comment){
$class = $comment->getAttribute('class');
$classes = explode(' ', $class);
$event = false;
if(in_array('discussion-item', $classes)) {
$event = true;
}
$author = 'unknown';
if($comment->find('.author', 0)) {
$author = $comment->find('.author', 0)->plaintext;
}
$uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p') . '/issues/' . $issueNbr;
protected function extractIssueEvent($issueNbr, $title, $comment){
$comment = $comment->firstChild();
if(!$event) {
$comment = $comment->nextSibling();
}
$uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p')
. '/issues/' . $issueNbr . '#' . $comment->getAttribute('id');
if($event) {
$title .= ' / ' . substr($class, strpos($class, 'discussion-item-') + strlen('discussion-item-'));
if(!$comment->hasAttribute('id')) {
$items = array();
$timestamp = strtotime($comment->find('relative-time', 0)->getAttribute('datetime'));
$content = $comment->innertext;
while($comment = $comment->nextSibling()) {
$item = array();
$item['author'] = $author;
$item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
$item['timestamp'] = $timestamp;
$item['content'] = $content . '<p>' . $comment->children(1)->innertext . '</p>';
$item['uri'] = $uri . '#' . $comment->children(1)->getAttribute('id');
$items[] = $item;
$author = $comment->find('.author', 0)->plaintext;
$title .= ' / ' . trim($comment->plaintext);
$content = $title;
if (null !== $comment->nextSibling()) {
$content = $comment->nextSibling()->innertext;
if ($comment->nextSibling()->nodeName() === 'span') {
$content = $comment->nextSibling()->nextSibling()->innertext;
}
return $items;
}
$content = $comment->parent()->innertext;
} else {
$title .= ' / ' . trim($comment->firstChild()->plaintext);
$content = '<pre>' . $comment->find('.comment-body', 0)->innertext . '</pre>';
}
$item = array();
$item['author'] = $author;
$item['uri'] = $uri . '#' . $comment->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'));
$item['timestamp'] = strtotime(
$comment->find('relative-time', 0)->getAttribute('datetime')
);
$item['content'] = $content;
return $item;
}
protected function extractIssueComment($issueNbr, $title, $comment){
$uri = static::URI . $this->getInput('u') . '/'
. $this->getInput('p') . '/issues/' . $issueNbr;
$author = $comment->find('.author', 0)->plaintext;
$title .= ' / ' . trim(
$comment->find('.comment .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['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
$item['timestamp'] = strtotime(
$comment->find('relative-time', 0)->getAttribute('datetime')
);
$item['content'] = $content;
return $item;
}
@@ -121,17 +121,29 @@ class GithubIssueBridge extends BridgeAbstract {
protected function extractIssueComments($issue){
$items = array();
$title = $issue->find('.gh-header-title', 0)->plaintext;
$issueNbr = trim(substr($issue->find('.gh-header-number', 0)->plaintext, 1));
$issueNbr = trim(
substr($issue->find('.gh-header-number', 0)->plaintext, 1)
);
$comments = $issue->find('.js-discussion', 0);
foreach($comments->children() as $comment) {
$classes = explode(' ', $comment->getAttribute('class'));
if(in_array('discussion-item', $classes)
|| in_array('timeline-comment-wrapper', $classes)) {
$item = $this->extractIssueComment($issueNbr, $title, $comment);
if(array_keys($item) !== range(0, count($item) - 1)) {
$item = array($item);
if (!$comment->hasChildNodes()) {
continue;
}
$items = array_merge($items, $item);
$comment = $comment->firstChild();
$classes = explode(' ', $comment->getAttribute('class'));
if (in_array('timeline-comment-wrapper', $classes)) {
$item = $this->extractIssueComment($issueNbr, $title, $comment);
$items[] = $item;
continue;
}
while (in_array('discussion-item', $classes)) {
$item = $this->extractIssueEvent($issueNbr, $title, $comment);
$items[] = $item;
$comment = $comment->nextSibling();
if (null == $comment) {
break;
}
$classes = explode(' ', $comment->getAttribute('class'));
}
}
return $items;
@@ -139,7 +151,9 @@ class GithubIssueBridge extends BridgeAbstract {
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('No results for Github Issue ' . $this->getURI());
or returnServerError(
'No results for Github Issue ' . $this->getURI()
);
switch($this->queriedContext) {
case 'Issue comments':
@@ -148,31 +162,40 @@ class GithubIssueBridge extends BridgeAbstract {
case 'Project Issues':
foreach($html->find('.js-active-navigation-container .js-navigation-item') as $issue) {
$info = $issue->find('.opened-by', 0);
$issueNbr = substr(trim($info->plaintext), 1, strpos(trim($info->plaintext), ' '));
$issueNbr = substr(
trim($info->plaintext), 1, strpos(trim($info->plaintext), ' ')
);
$item = array();
$item['content'] = '';
if($this->getInput('c')) {
$uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p') . '/issues/' . $issueNbr;
$uri = static::URI . $this->getInput('u')
. '/' . $this->getInput('p') . '/issues/' . $issueNbr;
$issue = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT);
if($issue) {
$this->items = array_merge($this->items, $this->extractIssueComments($issue));
$this->items = array_merge(
$this->items,
$this->extractIssueComments($issue)
);
continue;
}
$item['content'] = 'Can not extract comments from ' . $uri;
}
$item['author'] = $info->find('a', 0)->plaintext;
$item['timestamp'] = strtotime($info->find('relative-time', 0)->getAttribute('datetime'));
$item['timestamp'] = strtotime(
$info->find('relative-time', 0)->getAttribute('datetime')
);
$item['title'] = html_entity_decode(
$issue->find('.js-navigation-open', 0)->plaintext,
ENT_QUOTES,
'UTF-8'
);
$comments = $issue->find('.col-5', 0)->plaintext;
$comments = trim($issue->find('.col-5', 0)->plaintext);
$item['content'] .= "\n" . 'Comments: ' . ($comments ? $comments : '0');
$item['uri'] = self::URI . $issue->find('.js-navigation-open', 0)->getAttribute('href');
$item['uri'] = self::URI
. $issue->find('.js-navigation-open', 0)->getAttribute('href');
$this->items[] = $item;
}
break;
@@ -180,7 +203,11 @@ class GithubIssueBridge extends BridgeAbstract {
array_walk($this->items, function(&$item){
$item['content'] = preg_replace('/\s+/', ' ', $item['content']);
$item['content'] = str_replace('href="/', 'href="' . static::URI, $item['content']);
$item['content'] = str_replace(
'href="/',
'href="' . static::URI,
$item['content']
);
$item['content'] = str_replace(
'href="#',
'href="' . substr($item['uri'], 0, strpos($item['uri'], '#') + 1),

222
bridges/GlassdoorBridge.php Normal file
View File

@@ -0,0 +1,222 @@
<?php
class GlassdoorBridge extends BridgeAbstract {
// Contexts
const CONTEXT_BLOG = 'Blogs';
const CONTEXT_REVIEW = 'Company Reviews';
const CONTEXT_GLOBAL = 'global';
// Global context parameters
const PARAM_LIMIT = 'limit';
// Blog context parameters
const PARAM_BLOG_TYPE = 'blog_type';
const PARAM_BLOG_FULL = 'full_article';
const BLOG_TYPE_HOME = 'Home';
const BLOG_TYPE_COMPANIES_HIRING = 'Companies Hiring';
const BLOG_TYPE_CAREER_ADVICE = 'Career Advice';
const BLOG_TYPE_INTERVIEWS = 'Interviews';
const BLOG_TYPE_GUIDE = 'Guides';
// Review context parameters
const PARAM_REVIEW_COMPANY = 'company';
const MAINTAINER = 'logmanoriginal';
const NAME = 'Glassdoor Bridge';
const URI = 'https://www.glassdoor.com/';
const DESCRIPTION = 'Returns feeds for blog posts and company reviews';
const CACHE_TIMEOUT = 86400; // 24 hours
const PARAMETERS = array(
self::CONTEXT_BLOG => array(
self::PARAM_BLOG_TYPE => array(
'name' => 'Blog type',
'type' => 'list',
'title' => 'Select the blog you want to follow',
'values' => array(
self::BLOG_TYPE_HOME => 'blog/',
self::BLOG_TYPE_COMPANIES_HIRING => 'blog/companies-hiring/',
self::BLOG_TYPE_CAREER_ADVICE => 'blog/career-advice/',
self::BLOG_TYPE_INTERVIEWS => 'blog/interviews/',
self::BLOG_TYPE_GUIDE => 'blog/guide/'
)
),
self::PARAM_BLOG_FULL => array(
'name' => 'Full article',
'type' => 'checkbox',
'title' => 'Enable to return the full article for each post'
),
),
self::CONTEXT_REVIEW => array(
self::PARAM_REVIEW_COMPANY => array(
'name' => 'Company URL',
'type' => 'text',
'required' => true,
'title' => 'Paste the company review page URL here!',
'exampleValue' => 'https://www.glassdoor.com/Reviews/GitHub-Reviews-E671945.htm'
)
),
self::CONTEXT_GLOBAL => array(
self::PARAM_LIMIT => array(
'name' => 'Limit',
'type' => 'number',
'defaultValue' => -1,
'title' => 'Specifies the maximum number of items to return (default: All)'
)
)
);
private $host = self::URI; // They redirect without notice :/
private $title = '';
public function getURI() {
switch($this->queriedContext) {
case self::CONTEXT_BLOG:
return self::URI . $this->getInput(self::PARAM_BLOG_TYPE);
case self::CONTEXT_REVIEW:
return $this->filterCompanyURI($this->getInput(self::PARAM_REVIEW_COMPANY));
}
return parent::getURI();
}
public function getName() {
return $this->title ? $this->title . ' - ' . self::NAME : parent::getName();
}
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Failed loading contents!');
$this->host = $html->find('link[rel="canonical"]', 0)->href;
$html = defaultLinkTo($html, $this->host);
$this->title = $html->find('meta[property="og:title"]', 0)->content;
$limit = $this->getInput(self::PARAM_LIMIT);
switch($this->queriedContext) {
case self::CONTEXT_BLOG:
$this->collectBlogData($html, $limit);
break;
case self::CONTEXT_REVIEW:
$this->collectReviewData($html, $limit);
break;
}
}
private function collectBlogData($html, $limit) {
$posts = $html->find('section')
or returnServerError('Unable to find blog posts!');
foreach($posts as $post) {
$item = array();
$item['uri'] = $post->find('header a', 0)->href;
$item['title'] = $post->find('header', 0)->plaintext;
$item['content'] = $post->find('div[class="excerpt-content"]', 0)->plaintext;
$item['enclosures'] = array(
$this->getFullSizeImageURI($post->find('div[class="post-thumb"]', 0)->{'data-original'})
);
// optionally load full articles
if($this->getInput(self::PARAM_BLOG_FULL)) {
$full_html = getSimpleHTMLDOMCached($item['uri'])
or returnServerError('Unable to load full article!');
$full_html = defaultLinkTo($full_html, $this->host);
$item['author'] = $full_html->find('a[rel="author"]', 0);
$item['content'] = $full_html->find('article', 0);
$item['timestamp'] = strtotime($full_html->find('time.updated', 0)->datetime);
$item['categories'] = $full_html->find('span[class="post_tag"]');
}
$this->items[] = $item;
if($limit > 0 && count($this->items) >= $limit)
return;
}
}
private function collectReviewData($html, $limit) {
$reviews = $html->find('#EmployerReviews li[id^="empReview]')
or returnServerError('Unable to find reviews!');
foreach($reviews as $review) {
$item = array();
$item['uri'] = $review->find('a.reviewLink', 0)->href;
$item['title'] = $review->find('[class="summary"]', 0)->plaintext;
$item['author'] = $review->find('div.author span', 0)->plaintext;
$item['timestamp'] = strtotime($review->find('time', 0)->datetime);
$mainText = $review->find('p.mainText', 0)->plaintext;
$description = $review->find('div.prosConsAdvice', 0)->innertext;
$item['content'] = "<p>{$mainText}</p><p>{$description}</p>";
$this->items[] = $item;
if($limit > 0 && count($this->items) >= $limit)
return;
}
}
private function getFullSizeImageURI($uri) {
/* Images are scaled for display on the website. The scaling takes place
* on the host, who provides images in different sizes.
*
* For example:
* https://www.glassdoor.com/blog/app/uploads/sites/2/GettyImages-982402074-e1538092065712-390x193.jpg
*
* By removing the size information we receive the full sized image.
*
* For example:
* https://www.glassdoor.com/blog/app/uploads/sites/2/GettyImages-982402074-e1538092065712.jpg
*/
$uri = filter_var($uri, FILTER_SANITIZE_URL);
return preg_replace('/(.*)(\-\d+x\d+)(\.jpg)/', '$1$3', $uri);
}
private function filterCompanyURI($uri) {
/* Make sure the URI is a valid review page. Unfortunately there is no
* simple way to determine if the URI is valid, because of automagic
* redirection and strange naming conventions.
*/
if(!filter_var($uri,
FILTER_VALIDATE_URL,
FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED | FILTER_FLAG_PATH_REQUIRED)) {
returnClientError('The specified URL is invalid!');
}
$uri = filter_var($uri, FILTER_SANITIZE_URL);
$path = parse_url($uri, PHP_URL_PATH);
$parts = explode('/', $path);
$allowed_strings = array(
'de-DE' => 'Bewertungen',
'en-AU' => 'Reviews',
'nl-BE' => 'Reviews',
'fr-BE' => 'Avis',
'en-CA' => 'Reviews',
'fr-CA' => 'Avis',
'fr-FR' => 'Avis',
'en-IN' => 'Reviews',
'en-IE' => 'Reviews',
'nl-NL' => 'Reviews',
'de-AT' => 'Bewertungen',
'de-CH' => 'Bewertungen',
'fr-CH' => 'Avis',
'en-GB' => 'Reviews',
'en' => 'Reviews'
);
if(!in_array($parts[1], $allowed_strings)) {
returnClientError('Please specify a URL pointing to the companies review page!');
}
return $uri;
}
}

View File

@@ -22,6 +22,10 @@ class GooglePlusPostBridge extends BridgeAbstract{
)
));
public function getIcon() {
return 'https://ssl.gstatic.com/images/branding/product/ico/google_plus_alldp.ico';
}
public function collectData(){
$username = $this->getInput('username');

View File

@@ -3,7 +3,7 @@ class InstagramBridge extends BridgeAbstract {
const MAINTAINER = 'pauder';
const NAME = 'Instagram Bridge';
const URI = 'https://instagram.com/';
const URI = 'https://www.instagram.com/';
const DESCRIPTION = 'Returns the newest images';
const PARAMETERS = array(
@@ -19,6 +19,12 @@ class InstagramBridge extends BridgeAbstract {
'required' => true
)
),
array(
'l' => array(
'name' => 'location',
'required' => true
)
),
'global' => array(
'media_type' => array(
'name' => 'Media type',
@@ -38,16 +44,18 @@ class InstagramBridge extends BridgeAbstract {
public function collectData(){
if(!is_null($this->getInput('h')) && $this->getInput('media_type') == 'story') {
returnClientError('Stories are not supported for hashtags!');
if(is_null($this->getInput('u')) && $this->getInput('media_type') == 'story') {
returnClientError('Stories are not supported for hashtags nor locations!');
}
$data = $this->getInstagramJSON($this->getURI());
if(!is_null($this->getInput('u'))) {
$userMedia = $data->entry_data->ProfilePage[0]->graphql->user->edge_owner_to_timeline_media->edges;
} else {
} elseif(!is_null($this->getInput('h'))) {
$userMedia = $data->entry_data->TagPage[0]->graphql->hashtag->edge_hashtag_to_media->edges;
} elseif(!is_null($this->getInput('l'))) {
$userMedia = $data->entry_data->LocationsPage[0]->graphql->location->edge_location_to_media->edges;
}
foreach($userMedia as $media) {
@@ -101,16 +109,21 @@ class InstagramBridge extends BridgeAbstract {
$mediaInfo = $data->entry_data->PostPage[0]->graphql->shortcode_media;
//Process the first element, that isn't in the node graph
if (count($mediaInfo->edge_media_to_caption->edges) > 0) {
$caption = $mediaInfo->edge_media_to_caption->edges[0]->node->text;
} else {
$caption = '';
}
$enclosures = [$mediaInfo->display_url];
$content = '<img src="' . htmlentities($mediaInfo->display_url) . '" alt="' . $caption . '" />';
foreach($mediaInfo->edge_sidecar_to_children->edges as $media) {
$content .= '<img src="' . htmlentities($media->node->display_url) . '" alt="'. $caption . '" />';
$enclosures[] = $media->node->display_url;
$display_url = $media->node->display_url;
if(!in_array($display_url, $enclosures)) { // add only if not added yet
$content .= '<img src="' . htmlentities($display_url) . '" alt="' . $caption . '" />';
$enclosures[] = $display_url;
}
}
return [$content, $enclosures];
@@ -139,11 +152,12 @@ class InstagramBridge extends BridgeAbstract {
public function getURI(){
if(!is_null($this->getInput('u'))) {
return self::URI . urlencode($this->getInput('u'));
return self::URI . urlencode($this->getInput('u')) . '/';
} elseif(!is_null($this->getInput('h'))) {
return self::URI . 'explore/tags/' . urlencode($this->getInput('h'));
} elseif(!is_null($this->getInput('l'))) {
return self::URI . 'explore/locations/' . urlencode($this->getInput('l'));
}
return parent::getURI();
}
}

View File

@@ -13,6 +13,10 @@ class JapanExpoBridge extends BridgeAbstract {
)
));
public function getIcon() {
return 'https://s.japan-expo.com/katana/images/JES073/favicons/paris.png';
}
public function collectData(){
function frenchPubDateToTimestamp($date_to_parse) {

View File

@@ -132,7 +132,7 @@ class JustETFBridge extends BridgeAbstract {
date_time_set($df, 0, 0);
// debugMessage(date_format($df, 'U'));
// Debug::log(date_format($df, 'U'));
return date_format($df, 'U');
}
@@ -210,7 +210,7 @@ class JustETFBridge extends BridgeAbstract {
$element = $article->find('div.subheadline', 0)
or returnServerError('Date not found!');
// debugMessage($element->plaintext);
// Debug::log($element->plaintext);
$date = trim(explode('|', $element->plaintext)[0]);
@@ -223,7 +223,7 @@ class JustETFBridge extends BridgeAbstract {
$element->find('a', 0)->onclick = '';
// debugMessage($element->innertext);
// Debug::log($element->innertext);
return $element->innertext;
}
@@ -288,7 +288,7 @@ class JustETFBridge extends BridgeAbstract {
$element = $html->find('div.infobox div.vallabel', 0)
or returnServerError('Date not found!');
// debugMessage($element->plaintext);
// Debug::log($element->plaintext);
$date = trim(explode("\r\n", $element->plaintext)[1]);

View File

@@ -36,6 +36,11 @@ class KATBridge extends BridgeAbstract {
'name' => 'Only get results from Elite or Verified uploaders ?',
),
));
public function getIcon() {
return 'https://statuskatcrco-631f.kxcdn.com/assets/images/favicon.ico';
}
public function collectData(){
function parseDateTimestamp($element){
$guessedDate = strptime($element, '%d-%m-%Y %H:%M:%S');

View File

@@ -38,6 +38,10 @@ class KernelBugTrackerBridge extends BridgeAbstract {
private $bugid = '';
private $bugdesc = '';
public function getIcon() {
return self::URI . '/images/favicon.ico';
}
public function collectData(){
$limit = $this->getInput('limit');
$sorting = $this->getInput('sorting');

View File

@@ -64,7 +64,7 @@ class KununuBridge extends BridgeAbstract {
return parent::getURI();
}
function getName(){
public function getName(){
if(!is_null($this->getInput('company'))) {
$company = $this->fixCompanyName($this->getInput('company'));
return ($this->companyName ?: $company) . ' - ' . self::NAME;
@@ -73,52 +73,67 @@ class KununuBridge extends BridgeAbstract {
return parent::getName();
}
public function getIcon() {
return 'https://www.kununu.com/favicon-196x196.png';
}
public function collectData(){
$full = $this->getInput('full');
// Load page
$html = getSimpleHTMLDOMCached($this->getURI());
if(!$html)
returnServerError('Unable to receive data from ' . $this->getURI() . '!');
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Unable to receive data from ' . $this->getURI() . '!');
$html = defaultLinkTo($html, static::URI);
// Update name for this request
$this->companyName = $this->extractCompanyName($html);
$company = $html->find('span[class="company-name"]', 0)
or returnServerError('Cannot find company name!');
$this->companyName = $company->innertext;
// Find the section with all the panels (reviews)
$section = $html->find('section.kununu-scroll-element', 0);
if($section === false)
returnServerError('Unable to find panel section!');
$section = $html->find('section.kununu-scroll-element', 0)
or returnServerError('Unable to find panel section!');
// Find all articles (within the panels)
$articles = $section->find('article');
if($articles === false || empty($articles))
returnServerError('Unable to find articles!');
$articles = $section->find('article')
or returnServerError('Unable to find articles!');
// Go through all articles
foreach($articles as $article) {
$anchor = $article->find('h1.review-title a', 0)
or returnServerError('Cannot find article URI!');
$date = $article->find('meta[itemprop=dateCreated]', 0)
or returnServerError('Cannot find article date!');
$rating = $article->find('span.rating', 0)
or returnServerError('Cannot find article rating!');
$summary = $article->find('[itemprop=name]', 0)
or returnServerError('Cannot find article summary!');
$item = array();
$item['author'] = $this->extractArticleAuthorPosition($article);
$item['timestamp'] = $this->extractArticleDate($article);
$item['title'] = $this->extractArticleRating($article)
$item['timestamp'] = strtotime($date);
$item['title'] = $rating->getAttribute('aria-label')
. ' : '
. $this->extractArticleSummary($article);
. strip_tags($summary->innertext);
$item['uri'] = $this->extractArticleUri($article);
$item['uri'] = $anchor->href;
if($full)
if($full) {
$item['content'] = $this->extractFullDescription($item['uri']);
else
} else {
$item['content'] = $this->extractArticleDescription($article);
}
$this->items[] = $item;
}
}
/**
* Fixes relative URLs in the given text
*/
private function fixUrl($text){
return preg_replace('/href=(\'|\")\//i', 'href="'.self::URI, $text);
}
}
/*
@@ -128,73 +143,11 @@ class KununuBridge extends BridgeAbstract {
$company = trim($company);
$company = str_replace(' ', '-', $company);
$company = strtolower($company);
return $this->encodeUmlauts($company);
}
/**
* Encodes unmlauts in the given text
*/
private function encodeUmlauts($text){
$umlauts = Array('/ä/','/ö/','/ü/','/Ä/','/Ö/','/Ü/','/ß/');
$replace = Array('ae','oe','ue','Ae','Oe','Ue','ss');
return preg_replace($umlauts, $replace, $text);
}
/**
* Returns the company name from the review html
*/
private function extractCompanyName($html){
$company_name = $html->find('h1[itemprop=name]', 0);
if(is_null($company_name))
returnServerError('Cannot find company name!');
return $company_name->plaintext;
}
/**
* Returns the date from a given article
*/
private function extractArticleDate($article){
// They conviniently provide a time attribute for us :)
$date = $article->find('meta[itemprop=dateCreated]', 0);
if(is_null($date))
returnServerError('Cannot find article date!');
return strtotime($date->content);
}
/**
* Returns the rating from a given article
*/
private function extractArticleRating($article){
$rating = $article->find('span.rating', 0);
if(is_null($rating))
returnServerError('Cannot find article rating!');
return $rating->getAttribute('aria-label');
}
/**
* Returns the summary from a given article
*/
private function extractArticleSummary($article){
$summary = $article->find('[itemprop=name]', 0);
if(is_null($summary))
returnServerError('Cannot find article summary!');
return strip_tags($summary->innertext);
}
/**
* Returns the URI from a given article
*/
private function extractArticleUri($article){
$anchor = $article->find('h1.review-title a', 0);
if(is_null($anchor))
returnServerError('Cannot find article URI!');
return self::URI . $anchor->href;
return preg_replace($umlauts, $replace, $company);
}
/**
@@ -202,9 +155,8 @@ class KununuBridge extends BridgeAbstract {
*/
private function extractArticleAuthorPosition($article){
// We need to parse the user-content manually
$user_content = $article->find('div.user-content', 0);
if(is_null($user_content))
returnServerError('Cannot find user content!');
$user_content = $article->find('div.user-content', 0)
or returnServerError('Cannot find user content!');
// Go through all h2 elements to find index of required span (I know... it's stupid)
$author_position = 'Unknown';
@@ -222,11 +174,10 @@ class KununuBridge extends BridgeAbstract {
* Returns the description from a given article
*/
private function extractArticleDescription($article){
$description = $article->find('[itemprop=reviewBody]', 0);
if(is_null($description))
returnServerError('Cannot find article description!');
$description = $article->find('[itemprop=reviewBody]', 0)
or returnServerError('Cannot find article description!');
return $this->fixUrl($description->innertext);
return $description->innertext;
}
/**
@@ -234,14 +185,14 @@ class KununuBridge extends BridgeAbstract {
*/
private function extractFullDescription($uri){
// Load full article
$html = getSimpleHTMLDOMCached($uri);
if($html === false)
returnServerError('Could not load full description!');
$html = getSimpleHTMLDOMCached($uri)
or returnServerError('Could not load full description!');
$html = defaultLinkTo($html, static::URI);
// Find the article
$article = $html->find('article', 0);
if(is_null($article))
returnServerError('Cannot find article!');
$article = $html->find('article', 0)
or returnServerError('Cannot find article!');
// Luckily they use the same layout for the review overview and full article pages :)
return $this->extractArticleDescription($article);

View File

@@ -362,7 +362,7 @@ class LeBonCoinBridge extends BridgeAbstract {
);
$opts = array(
CURL_CUSTOMREQUEST => 'POST',
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $data
);
@@ -424,7 +424,7 @@ class LeBonCoinBridge extends BridgeAbstract {
$requestJson = new StdClass();
$requestJson->owner_type = $this->getInput('owner');
$requestJson->filters->location = array();
$requestJson->filters = new StdClass();
$requestJson->filters->keywords = array(
'text' => $this->getInput('keywords')

View File

@@ -38,6 +38,10 @@ class LegifranceJOBridge extends BridgeAbstract {
return $item;
}
public function getIcon() {
return 'https://www.legifrance.gouv.fr/img/favicon.ico';
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI)
or $this->returnServer('Unable to download ' . self::URI);

View File

@@ -0,0 +1,28 @@
<?php
class MozillaSecurityBridge extends BridgeAbstract {
const MAINTAINER = 'm0le.net';
const NAME = 'Mozilla Security Advisories';
const URI = 'https://www.mozilla.org/en-US/security/advisories/';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Mozilla Security Advisories';
const WEBROOT = 'https://www.mozilla.org';
public function collectData(){
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request MSA.');
$html = defaultLinkTo($html, self::WEBROOT);
$item = array();
$articles = $html->find('div[itemprop="articleBody"] h2');
foreach ($articles as $element) {
$item['title'] = $element->innertext;
$item['timestamp'] = strtotime($element->innertext);
$item['content'] = $element->next_sibling()->innertext;
$item['uri'] = self::URI;
$this->items[] = $item;
}
}
}

331
bridges/NineGagBridge.php Normal file
View File

@@ -0,0 +1,331 @@
<?php
class NineGagBridge extends BridgeAbstract {
const NAME = '9gag Bridge';
const URI = 'https://9gag.com/';
const DESCRIPTION = 'Returns latest quotes from 9gag.';
const MAINTAINER = 'ZeNairolf';
const CACHE_TIMEOUT = 3600;
const PARAMETERS = array(
'Popular' => array(
'd' => array(
'name' => 'Section',
'type' => 'list',
'required' => true,
'values' => array(
'Hot' => 'hot',
'Trending' => 'trending',
'Fresh' => 'fresh',
),
),
'p' => array(
'name' => 'Pages',
'type' => 'number',
'defaultValue' => 3,
),
),
'Sections' => array(
'g' => array(
'name' => 'Section',
'type' => 'list',
'required' => true,
'values' => array(
'Animals' => 'cute',
'Anime & Manga' => 'anime-manga',
'Ask 9GAG' => 'ask9gag',
'Awesome' => 'awesome',
'Basketball' => 'basketball',
'Car' => 'car',
'Classical Art Memes' => 'classicalartmemes',
'Comic' => 'comic',
'Cosplay' => 'cosplay',
'Countryballs' => 'country',
'DIY & Crafts' => 'imadedis',
'Drawing & Illustration' => 'drawing',
'Fan Art' => 'animefanart',
'Food & Drinks' => 'food',
'Football' => 'football',
'Fortnite' => 'fortnite',
'Funny' => 'funny',
'GIF' => 'gif',
'Gaming' => 'gaming',
'Girl' => 'girl',
'Girly Things' => 'girly',
'Guy' => 'guy',
'History' => 'history',
'Home Design' => 'home',
'Horror' => 'horror',
'K-Pop' => 'kpop',
'LEGO' => 'lego',
'League of Legends' => 'leagueoflegends',
'Movie & TV' => 'movie-tv',
'Music' => 'music',
'NFK - Not For Kids' => 'nsfw',
'Overwatch' => 'overwatch',
'PC Master Race' => 'pcmr',
'PUBG' => 'pubg',
'Pic Of The Day' => 'photography',
'Pokémon' => 'pokemon',
'Politics' => 'politics',
'Relationship' => 'relationship',
'Roast Me' => 'roastme',
'Satisfying' => 'satisfying',
'Savage' => 'savage',
'School' => 'school',
'Sci-Tech' => 'science',
'Sport' => 'sport',
'Star Wars' => 'starwars',
'Superhero' => 'superhero',
'Surreal Memes' => 'surrealmemes',
'Timely' => 'timely',
'Travel' => 'travel',
'Video' => 'video',
'WTF' => 'wtf',
'Wallpaper' => 'wallpaper',
'Warhammer' => 'warhammer',
),
),
't' => array(
'name' => 'Type',
'type' => 'list',
'required' => true,
'values' => array(
'Hot' => 'hot',
'Fresh' => 'fresh',
),
),
'p' => array(
'name' => 'Pages',
'type' => 'number',
'defaultValue' => 3,
),
),
);
const MIN_NBR_PAGE = 1;
const MAX_NBR_PAGE = 6;
protected $p = null;
public function collectData() {
$url = sprintf(
'%sv1/group-posts/group/%s/type/%s?',
self::URI,
$this->getGroup(),
$this->getType()
);
$cursor = 'c=10';
$posts = array();
for ($i = 0; $i < $this->getPages(); ++$i) {
$content = getContents($url . $cursor);
$json = json_decode($content, true);
$posts = array_merge($posts, $json['data']['posts']);
$cursor = $json['data']['nextCursor'];
}
foreach ($posts as $post) {
$item['uri'] = $post['url'];
$item['title'] = $post['title'];
$item['content'] = self::getContent($post);
$item['categories'] = self::getCategories($post);
$item['timestamp'] = self::getTimestamp($post);
$this->items[] = $item;
}
}
public function getName() {
if ($this->getInput('d')) {
$name = sprintf('%s - %s', '9GAG', $this->getParameterKey('d'));
} elseif ($this->getInput('g')) {
$name = sprintf('%s - %s', '9GAG', $this->getParameterKey('g'));
if ($this->getInput('t')) {
$name = sprintf('%s [%s]', $name, $this->getParameterKey('t'));
}
}
if (!empty($name)) {
return $name;
}
return self::NAME;
}
public function getURI() {
$uri = $this->getInput('g');
if ($uri === 'default') {
$uri = $this->getInput('t');
}
return self::URI . $uri;
}
protected function getGroup() {
if ($this->getInput('d')) {
return 'default';
}
return $this->getInput('g');
}
protected function getType() {
if ($this->getInput('d')) {
return $this->getInput('d');
}
return $this->getInput('t');
}
protected function getPages() {
if ($this->p === null) {
$value = (int) $this->getInput('p');
$value = ($value < self::MIN_NBR_PAGE) ? self::MIN_NBR_PAGE : $value;
$value = ($value > self::MAX_NBR_PAGE) ? self::MAX_NBR_PAGE : $value;
$this->p = $value;
}
return $this->p;
}
protected function getParameterKey($input = '') {
$params = $this->getParameters();
$tab = 'Sections';
if ($input === 'd') {
$tab = 'Popular';
}
if (!isset($params[$tab][$input])) {
return '';
}
return array_search(
$this->getInput($input),
$params[$tab][$input]['values']
);
}
protected static function getContent($post) {
if ($post['type'] === 'Animated') {
$content = self::getAnimated($post);
} elseif ($post['type'] === 'Article') {
$content = self::getArticle($post);
} else {
$content = self::getPhoto($post);
}
return $content;
}
protected static function getPhoto($post) {
$image = $post['images']['image460'];
$photo = '<picture>';
$photo .= sprintf(
'<source srcset="%s" type="image/webp">',
$image['webpUrl']
);
$photo .= sprintf(
'<img src="%s" alt="%s" %s>',
$image['url'],
$post['title'],
'width="500"'
);
$photo .= '</picture>';
return $photo;
}
protected static function getAnimated($post) {
$poster = $post['images']['image460']['url'];
$sources = $post['images'];
$video = sprintf(
'<video poster="%s" %s>',
$poster,
'preload="auto" loop controls style="min-height: 300px" width="500"'
);
$video .= sprintf(
'<source src="%s" type="video/webm">',
$sources['image460sv']['vp9Url']
);
$video .= sprintf(
'<source src="%s" type="video/mp4">',
$sources['image460sv']['h265Url']
);
$video .= sprintf(
'<source src="%s" type="video/mp4">',
$sources['image460svwm']['url']
);
$video .= '</video>';
return $video;
}
protected static function getArticle($post) {
$blocks = $post['article']['blocks'];
$medias = $post['article']['medias'];
$contents = array();
foreach ($blocks as $block) {
if ('Media' === $block['type']) {
$mediaId = $block['mediaId'];
$contents[] = self::getContent($medias[$mediaId]);
} elseif ('RichText' === $block['type']) {
$contents[] = self::getRichText($block['content']);
}
}
$content = join('</div><div>', $contents);
$content = sprintf(
'<%1$s>%2$s</%1$s>',
'div',
$content
);
return $content;
}
protected static function getRichText($text = '') {
$text = trim($text);
if (preg_match('/^>\s(?<text>.*)/', $text, $matches)) {
$text = sprintf(
'<%1$s>%2$s</%1$s>',
'blockquote',
$matches['text']
);
} else {
$text = sprintf(
'<%1$s>%2$s</%1$s>',
'p',
$text
);
}
return $text;
}
protected static function getCategories($post) {
$params = self::PARAMETERS;
$sections = $params['Sections']['g']['values'];
if(isset($post['sections'])) {
$postSections = $post['sections'];
} elseif (isset($post['postSection'])) {
$postSections = array($post['postSection']);
} else {
$postSections = array();
}
foreach ($postSections as $key => $section) {
$postSections[$key] = array_search($section, $sections);
}
return $postSections;
}
protected static function getTimestamp($post) {
$url = $post['images']['image460']['url'];
$headers = get_headers($url, true);
$date = $headers['Date'];
$time = strtotime($date);
return $time;
}
}

View File

@@ -26,6 +26,10 @@ class NotAlwaysBridge extends BridgeAbstract {
)
));
public function getIcon() {
return self::URI . 'favicon_nar.png';
}
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request NotAlways.');

View File

@@ -54,6 +54,10 @@ class NyaaTorrentsBridge extends BridgeAbstract {
)
);
public function getIcon() {
return self::URI . 'static/favicon.png';
}
public function collectData() {
// Build Search URL from user-provided parameters

View File

@@ -0,0 +1,130 @@
<?php
class OnVaSortirBridge extends FeedExpander {
const MAINTAINER = 'AntoineTurmel';
const NAME = 'OnVaSortir';
const URI = 'https://www.onvasortir.com';
const DESCRIPTION = 'Returns the newest events from OnVaSortir (full text)';
const PARAMETERS = array(
array(
'city' => array(
'name' => 'City',
'type' => 'list',
'required' => true,
'values' => array(
'Agen' => 'Agen',
'Ajaccio' => 'Ajaccio',
'Albi' => 'Albi',
'Amiens' => 'Amiens',
'Angers' => 'Angers',
'Angoulême' => 'Angouleme',
'Annecy' => 'annecy',
'Aurillac' => 'aurillac',
'Auxerre' => 'auxerre',
'Avignon' => 'avignon',
'Béziers' => 'Beziers',
'Bastia' => 'Bastia',
'Beauvais' => 'Beauvais',
'Belfort' => 'Belfort',
'Bergerac' => 'bergerac',
'Besançon' => 'Besancon',
'Biarritz' => 'Biarritz',
'Blois' => 'Blois',
'Bordeaux' => 'bordeaux',
'Bourg-en-Bresse' => 'bourg-en-bresse',
'Bourges' => 'Bourges',
'Brest' => 'Brest',
'Brive' => 'brive-la-gaillarde',
'Bruxelles' => 'bruxelles',
'Caen' => 'Caen',
'Calais' => 'Calais',
'Carcassonne' => 'Carcassonne',
'Châteauroux' => 'Chateauroux',
'Chalon-sur-saone' => 'chalon-sur-saone',
'Chambéry' => 'chambery',
'Chantilly' => 'chantilly',
'Charleroi' => 'charleroi',
'Charleville-Mézières' => 'Charleville-Mezieres',
'Chartres' => 'Chartres',
'Cherbourg' => 'Cherbourg',
'Cholet' => 'cholet',
'Clermont-Ferrand' => 'Clermont-Ferrand',
'Compiègne' => 'compiegne',
'Dieppe' => 'dieppe',
'Dijon' => 'Dijon',
'Dunkerque' => 'Dunkerque',
'Evreux' => 'evreux',
'Fréjus' => 'frejus',
'Gap' => 'gap',
'Genève' => 'geneve',
'Grenoble' => 'Grenoble',
'La Roche sur Yon' => 'La-Roche-sur-Yon',
'La Rochelle' => 'La-Rochelle',
'Lausanne' => 'lausanne',
'Laval' => 'Laval',
'Le Havre' => 'le-havre',
'Le Mans' => 'le-mans',
'Liège' => 'liege',
'Lille' => 'lille',
'Limoges' => 'Limoges',
'Lorient' => 'Lorient',
'Luxembourg' => 'Luxembourg',
'Lyon' => 'lyon',
'Marseille' => 'marseille',
'Metz' => 'Metz',
'Mons' => 'Mons',
'Mont de Marsan' => 'mont-de-marsan',
'Montauban' => 'Montauban',
'Montluçon' => 'montlucon',
'Montpellier' => 'montpellier',
'Mulhouse' => 'Mulhouse',
'Nîmes' => 'nimes',
'Namur' => 'Namur',
'Nancy' => 'Nancy',
'Nantes' => 'nantes',
'Nevers' => 'nevers',
'Nice' => 'nice',
'Niort' => 'niort',
'Orléans' => 'orleans',
'Périgueux' => 'perigueux',
'Paris' => 'paris',
'Pau' => 'Pau',
'Perpignan' => 'Perpignan',
'Poitiers' => 'Poitiers',
'Quimper' => 'Quimper',
'Reims' => 'Reims',
'Rennes' => 'Rennes',
'Roanne' => 'roanne',
'Rodez' => 'rodez',
'Rouen' => 'Rouen',
'Saint-Brieuc' => 'Saint-Brieuc',
'Saint-Etienne' => 'saint-etienne',
'Saint-Malo' => 'saint-malo',
'Saint-Nazaire' => 'saint-nazaire',
'Saint-Quentin' => 'saint-quentin',
'Saintes' => 'saintes',
'Strasbourg' => 'Strasbourg',
'Tarbes' => 'Tarbes',
'Toulon' => 'Toulon',
'Toulouse' => 'Toulouse',
'Tours' => 'Tours',
'Troyes' => 'troyes',
'Valence' => 'valence',
'Vannes' => 'vannes',
'Zurich' => 'zurich',
),
'defaultValue' => ''
)
)
);
protected function parseItem($item){
$item = parent::parseItem($item);
$html = getSimpleHTMLDOMCached($item['uri']);
$text = $html->find('div.corpsMax', 0)->innertext;
$item['content'] = utf8_encode($text);
return $item;
}
public function collectData(){
$this->collectExpandableDatas('https://' .
$this->getInput('city') . '.onvasortir.com/rss.php');
}
}

100
bridges/PikabuBridge.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
class PikabuBridge extends BridgeAbstract {
const NAME = 'Пикабу';
const URI = 'https://pikabu.ru';
const DESCRIPTION = 'Выводит посты по тегу';
const MAINTAINER = 'em92';
const PARAMETERS = array(
'По тегу' => array(
'tag' => array(
'name' => 'Тег',
'exampleValue' => 'it',
'required' => true
),
'filter' => array(
'name' => 'Фильтр',
'type' => 'list',
'values' => array(
'Горячее' => 'hot',
'Свежее' => 'new',
),
'defaultValue' => 'hot'
)
)
);
public function getURI() {
if ($this->getInput('tag')) {
return self::URI . '/tag/' . rawurlencode($this->getInput('tag')) . '/' . rawurlencode($this->getInput('filter'));
} else {
return parent::getURI();
}
}
public function getIcon() {
return 'https://cs.pikabu.ru/assets/favicon.ico';
}
public function getName() {
if (is_string($this->getInput('tag'))) {
return $this->getInput('tag') . ' - ' . parent::getName();
} else {
return parent::getName();
}
}
public function collectData(){
$link = $this->getURI();
$text_html = getContents($link) or returnServerError('Could not fetch ' . $link);
$text_html = iconv('windows-1251', 'utf-8', $text_html);
$html = str_get_html($text_html);
foreach($html->find('article.story') as $post) {
$time = $post->find('time.story__datetime', 0);
if (is_null($time)) continue;
$el_to_remove_selectors = array(
'.story__read-more',
'svg.story-image__stretch',
);
foreach($el_to_remove_selectors as $el_to_remove_selector) {
foreach($post->find($el_to_remove_selector) as $el) {
$el->outertext = '';
}
}
foreach($post->find('img') as $img) {
$src = $img->getAttribute('src');
if (!$src) {
$src = $img->getAttribute('data-src');
if (!$src) {
continue;
}
}
$img->outertext = '<img src="' . $src . '">';
}
$categories = array();
foreach($post->find('.tags__tag') as $tag) {
if ($tag->getAttribute('data-tag')) {
$categories[] = $tag->innertext;
}
}
$title = $post->find('.story__title-link', 0);
$item = array();
$item['categories'] = $categories;
$item['author'] = $post->find('.user__nick', 0)->innertext;
$item['title'] = $title->plaintext;
$item['content'] = strip_tags(backgroundToImg($post->find('.story__content-inner', 0)->innertext), '<br><p><img>');
$item['uri'] = $title->href;
$item['timestamp'] = strtotime($time->getAttribute('datetime'));
$this->items[] = $item;
}
}
}

View File

@@ -25,6 +25,10 @@ class PinterestBridge extends FeedExpander {
)
);
public function getIcon() {
return 'https://s.pinimg.com/webapp/style/images/favicon-9f8f9adf.png';
}
public function collectData(){
switch($this->queriedContext) {
case 'By username and board':

View File

@@ -53,7 +53,7 @@ class PixivBridge extends BridgeAbstract {
$url = str_replace('_master1200', '', $url);
$url = str_replace('c/240x240/img-master/', 'img-original/', $url);
$path = CACHE_DIR . '/pixiv_img';
$path = PATH_CACHE . 'pixiv_img/';
if(!is_dir($path))
mkdir($path, 0755, true);

View File

@@ -5,6 +5,10 @@ class RadioMelodieBridge extends BridgeAbstract {
const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie';
const MAINTAINER = 'sysadminstory';
public function getIcon() {
return self::URI . 'img/favicon.png';
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI . 'actu')
or returnServerError('Could not request Radio Melodie.');

View File

@@ -7,10 +7,14 @@ class RainbowSixSiegeBridge extends BridgeAbstract {
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Latest articles from the Rainbow Six Siege blog';
public function getIcon() {
return 'https://ubistatic19-a.akamaihd.net/resource/en-us/game/rainbow6/siege-v3/r6s-favicon_316592.ico';
}
public function collectData(){
$dlUrl = 'https://prod-tridionservice.ubisoft.com/live/v1/News/Latest?templateId=tcm%3A152-7677';
$dlUrl .= '8-32&pageIndex=0&pageSize=10&language=en-US&detailPageId=tcm%3A152-194572-64';
$dlUrl .= '&keywordList=175426&siteId=undefined&useSeoFriendlyUrl=true';
$dlUrl .= '8-32&pageIndex=0&pageSize=10&language=en-US&detailPageId=tcm%3A150-194572-64';
$dlUrl .= '&keywordList=233416%2C316144%2C233418%2C233417&siteId=undefined&useSeoFriendlyUrl=true';
$jsonString = getContents($dlUrl) or returnServerError('Error while downloading the website content');
$json = json_decode($jsonString, true);

View File

@@ -1,25 +1,50 @@
<?php
class Rue89Bridge extends FeedExpander {
class Rue89Bridge extends BridgeAbstract {
const MAINTAINER = 'pit-fgfjiudghdf';
const MAINTAINER = 'teromene';
const NAME = 'Rue89';
const URI = 'http://rue89.nouvelobs.com/';
const DESCRIPTION = 'Returns the 5 newest posts from Rue89 (full text)';
const URI = 'https://www.nouvelobs.com/rue89/';
const DESCRIPTION = 'Returns the newest posts from Rue89';
protected function parseItem($item){
$item = parent::parseItem($item);
$url = 'http://api.rue89.nouvelobs.com/export/mobile2/node/'
. str_replace(' ', '', substr($item['uri'], -8))
. '/full';
$datas = json_decode(getContents($url), true);
$item['content'] = $datas['node']['body'];
return $item;
}
public function collectData() {
$this->collectExpandableDatas('http://api.rue89.nouvelobs.com/feed');
$jsonArticles = getContents('https://appdata.nouvelobs.com/rue89/feed.json')
or die('Unable to query Rue89 !');
$articles = json_decode($jsonArticles)->items;
foreach($articles as $article) {
$this->items[] = $this->getArticle($article);
}
}
private function getArticle($articleInfo) {
$articleJson = getContents($articleInfo->json_url) or die('Unable to get article !');
$article = json_decode($articleJson);
$item = array();
$item['title'] = $article->title;
$item['uri'] = $article->url;
if($article->content_premium !== null) {
$item['content'] = $article->content_premium;
} else {
$item['content'] = $article->content;
}
$item['timestamp'] = $article->date_publi;
$item['author'] = $article->author->show_name;
$item['enclosures'] = array();
foreach($article->images as $image) {
$item['enclosures'][] = $image->url;
}
$item['categories'] = array();
foreach($article->categories as $category) {
$item['categories'][] = $category->title;
}
return $item;
}
}

View File

@@ -1,88 +0,0 @@
<?php
class SexactuBridge extends BridgeAbstract {
const MAINTAINER = 'Riduidel';
const NAME = 'Sexactu';
const AUTHOR = 'Maïa Mazaurette';
const URI = 'http://www.gqmagazine.fr';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Sexactu via rss-bridge';
const REPLACED_ATTRIBUTES = array(
'href' => 'href',
'src' => 'src',
'data-original' => 'src'
);
public function getURI(){
return self::URI . '/sexactu';
}
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request ' . $this->getURI());
$sexactu = $html->find('.container_sexactu', 0);
$rowList = $sexactu->find('.row');
foreach($rowList as $row) {
// only use first list as second one only contains pages numbers
$title = $row->find('.title', 0);
if($title) {
$item = array();
$item['author'] = self::AUTHOR;
$item['title'] = $title->plaintext;
$urlAttribute = 'data-href';
$uri = $title->$urlAttribute;
if($uri === false)
continue;
if(substr($uri, 0, 1) === 'h') { // absolute uri
$item['uri'] = $uri;
} else if(substr($uri, 0, 1) === '/') { // domain relative url
$item['uri'] = self::URI . $uri;
} else {
$item['uri'] = $this->getURI() . $uri;
}
$article = $this->loadFullArticle($item['uri']);
$item['content'] = $this->replaceUriInHtmlElement($article->find('.article_content', 0));
$publicationDate = $article->find('time[itemprop=datePublished]', 0);
$short_date = $publicationDate->datetime;
$item['timestamp'] = strtotime($short_date);
} else {
// Sometimes we get rubbish, ignore.
continue;
}
$this->items[] = $item;
}
}
/**
* Loads the full article and returns the contents
* @param $uri The article URI
* @return The article content
*/
private function loadFullArticle($uri){
$html = getSimpleHTMLDOMCached($uri);
$content = $html->find('#article', 0);
if($content) {
return $content;
}
return null;
}
/**
* Replaces all relative URIs with absolute ones
* @param $element A simplehtmldom element
* @return The $element->innertext with all URIs replaced
*/
private function replaceUriInHtmlElement($element){
$returned = $element->innertext;
foreach (self::REPLACED_ATTRIBUTES as $initial => $final) {
$returned = str_replace($initial . '="/', $final . '="' . self::URI . '/', $returned);
}
return $returned;
}
}

View File

@@ -34,13 +34,13 @@ class SoundCloudBridge extends BridgeAbstract {
for($i = 0; $i < 10; $i++) {
$item = array();
$item['author'] = $tracks[$i]->user->username . ' - ' . $tracks[$i]->title;
$item['author'] = $tracks[$i]->user->username;
$item['title'] = $tracks[$i]->user->username . ' - ' . $tracks[$i]->title;
$item['content'] = '<audio src="'
. $tracks[$i]->uri
$item['timestamp'] = strtotime($tracks[$i]->created_at);
$item['content'] = $tracks[$i]->description;
$item['enclosures'] = array($tracks[$i]->uri
. '/stream?client_id='
. self::CLIENT_ID
. '">';
. self::CLIENT_ID);
$item['id'] = self::URI
. urlencode($this->getInput('u'))

View File

@@ -13,6 +13,10 @@ class SupInfoBridge extends BridgeAbstract {
)
));
public function getIcon() {
return self::URI . '/favicon.png';
}
public function collectData() {
if(empty($this->getInput('tag'))) {

View File

@@ -14,6 +14,10 @@ class TagBoardBridge extends BridgeAbstract {
)
));
public function getIcon() {
return 'https://static.tagboard.com/public/favicon-32x32.png';
}
public function collectData(){
$link = 'https://post-cache.tagboard.com/search/' . $this->getInput('u');

View File

@@ -21,6 +21,10 @@ class TebeoBridge extends FeedExpander {
)
));
public function getIcon() {
return self::URI . 'images/header_logo.png';
}
public function collectData(){
$url = self::URI . '/le-replay/' . $this->getInput('cat');
$html = getSimpleHTMLDOM($url)

View File

@@ -3,7 +3,7 @@ class ThePirateBayBridge extends BridgeAbstract {
const MAINTAINER = 'mitsukarenai';
const NAME = 'The Pirate Bay';
const URI = 'https://thepiratebay.org/';
const URI = 'https://thepiratebay.wf/';
const DESCRIPTION = 'Returns results for the keywords. You can put several
list of keywords by separating them with a semicolon (e.g. "one show;another
show"). Category based search needs the category number as input. User based

View File

@@ -158,6 +158,10 @@ class TheTVDBBridge extends BridgeAbstract {
}
}
public function getIcon() {
return self::URI . 'application/themes/thetvdb/images/logo.png';
}
public function collectData(){
$serie_id = $this->getInput('serie_id');
$nbepisode = $this->getInput('nb_episode');

View File

@@ -0,0 +1,41 @@
<?php
class TheYeteeBridge extends BridgeAbstract {
const MAINTAINER = 'Monsieur Poutounours';
const NAME = 'TheYetee';
const URI = 'https://theyetee.com';
const CACHE_TIMEOUT = 14400; // 4 h
const DESCRIPTION = 'Fetch daily shirts from The Yetee';
public function collectData(){
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request The Yetee.');
$div = $html->find('.hero-col');
foreach($div as $element) {
$item = array();
$item['enclosures'] = array();
$title = $element->find('h2', 0)->plaintext;
$item['title'] = $title;
$author = trim($element->find('div[class=credit]', 0)->plaintext);
$item['author'] = $author;
$uri = $element->find('div[class=controls] a', 0)->href;
$item['uri'] = static::URI . $uri;
$content = '<p>' . $element->find('section[class=product-listing-info] p', -1)->plaintext . '</p>';
$photos = $element->find('a[class=js-modaal-gallery] img');
foreach($photos as $photo) {
$content = $content . "<br /><img src='$photo->src' />";
$item['enclosures'][] = $photo->src;
}
$item['content'] = $content;
$this->items[] = $item;
}
}
}

View File

@@ -0,0 +1,167 @@
<?php
class ThingiverseBridge extends BridgeAbstract {
const NAME = 'Thingiverse Search';
const URI = 'https://thingiverse.com';
const DESCRIPTION = 'Returns feeds for search results';
const MAINTAINER = 'AntoineTurmel';
const PARAMETERS = array(
array(
'query' => array(
'name' => 'Search query',
'type' => 'text',
'required' => true,
'title' => 'Insert your search term here',
'exampleValue' => 'Enter your search term'
),
'sortby' => array(
'name' => 'Sort by',
'type' => 'list',
'required' => false,
'values' => array(
'Relevant' => 'relevant',
'Text' => 'text',
'Popular' => 'popular',
'# of Makes' => 'makes',
'Newest' => 'newest',
),
'defaultValue' => 'newest'
),
'category' => array(
'name' => 'Category',
'type' => 'list',
'required' => false,
'values' => array(
'Any' => '',
'3D Printing' => '73',
'Art' => '63',
'Fashion' => '64',
'Gadgets' => '65',
'Hobby' => '66',
'Household' => '67',
'Learning' => '69',
'Models' => '70',
'Tools' => '71',
'Toys &amp; Games' => '72',
'2D Art' => '144',
'Art Tools' => '75',
'Coins &amp; Badges' => '143',
'Interactive Art' => '78',
'Math Art' => '79',
'Scans &amp; Replicas' => '145',
'Sculptures' => '80',
'Signs &amp; Logos' => '76',
'Accessories' => '81',
'Bracelets' => '82',
'Costume' => '142',
'Earrings' => '139',
'Glasses' => '83',
'Jewelry' => '84',
'Keychains' => '130',
'Rings' => '85',
'Audio' => '141',
'Camera' => '86',
'Computer' => '87',
'Mobile Phone' => '88',
'Tablet' => '90',
'Video Games' => '91',
'Automotive' => '155',
'DIY' => '93',
'Electronics' => '92',
'Music' => '94',
'R/C Vehicles' => '95',
'Robotics' => '96',
'Sport &amp; Outdoors' => '140',
'Bathroom' => '147',
'Containers' => '146',
'Decor' => '97',
'Household Supplies' => '99',
'Kitchen &amp; Dining' => '100',
'Office' => '101',
'Organization' => '102',
'Outdoor &amp; Garden' => '98',
'Pets' => '103',
'Replacement Parts' => '153',
'Biology' => '106',
'Engineering' => '104',
'Math' => '105',
'Physics &amp; Astronomy' => '148',
'Animals' => '107',
'Buildings &amp; Structures' => '108',
'Creatures' => '109',
'Food &amp; Drink' => '110',
'Model Furniture' => '111',
'Model Robots' => '115',
'People' => '112',
'Props' => '114',
'Vehicles' => '116',
'Hand Tools' => '118',
'Machine Tools' => '117',
'Parts' => '119',
'Tool Holders &amp; Boxes' => '120',
'Chess' => '151',
'Construction Toys' => '121',
'Dice' => '122',
'Games' => '123',
'Mechanical Toys' => '124',
'Playsets' => '113',
'Puzzles' => '125',
'Toy &amp; Game Accessories' => '149',
'3D Printer Accessories' => '127',
'3D Printer Extruders' => '152',
'3D Printer Parts' => '128',
'3D Printers' => '126',
'3D Printing Tests' => '129',
),
'defaultValue' => ''
),
'showimage' => array(
'name' => 'Show image in content',
'type' => 'checkbox',
'required' => false,
'title' => 'Activate to show the image in the content',
'defaultValue' => 'checked'
)
)
);
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Failed to receive ' . $this->getURI());
$results = $html->find('div.thing-card');
foreach($results as $result) {
$item = array();
$item['title'] = $result->find('span.ellipsis', 0);
$item['uri'] = self::URI . $result->find('a', 1)->href;
$item['author'] = $result->find('span.item-creator', 0);
$item['content'] = '';
$image = $result->find('img.card-img', 0)->src;
if($this->getInput('showimage')) {
$item['content'] .= '<img src="' . $image . '">';
}
$item['enclosures'] = array($image);
$this->items[] = $item;
}
}
public function getURI(){
if(!is_null($this->getInput('query'))) {
$uri = self::URI . '/search?q=' . urlencode($this->getInput('query'));
$uri .= '&sort=' . $this->getInput('sortby');
$uri .= '&category_id=' . $this->getInput('category');
return $uri;
}
return parent::getURI();
}
}

View File

@@ -66,6 +66,41 @@ class TwitterBridge extends BridgeAbstract {
)
);
public function detectParameters($url){
$params = array();
// By keyword or hashtag (search)
$regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/search.*(\?|&)q=([^\/&?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['q'] = urldecode($matches[4]);
return $params;
}
// By hashtag
$regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/hashtag\/([^\/?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['q'] = urldecode($matches[3]);
return $params;
}
// By list
$regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)\/lists\/([^\/?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['user'] = urldecode($matches[3]);
$params['list'] = urldecode($matches[4]);
return $params;
}
// By username
$regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['u'] = urldecode($matches[3]);
return $params;
}
return null;
}
public function getName(){
switch($this->queriedContext) {
case 'By keyword or hashtag':
@@ -144,9 +179,9 @@ class TwitterBridge extends BridgeAbstract {
$item = array();
// extract username and sanitize
$item['username'] = $tweet->getAttribute('data-screen-name');
$item['username'] = htmlspecialchars_decode($tweet->getAttribute('data-screen-name'), ENT_QUOTES);
// extract fullname (pseudonym)
$item['fullname'] = $tweet->getAttribute('data-name');
$item['fullname'] = htmlspecialchars_decode($tweet->getAttribute('data-name'), ENT_QUOTES);
// get author
$item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
// get avatar link
@@ -158,7 +193,8 @@ class TwitterBridge extends BridgeAbstract {
// extract tweet timestamp
$item['timestamp'] = $tweet->find('span.js-short-timestamp', 0)->getAttribute('data-time');
// generate the title
$item['title'] = strip_tags($this->fixAnchorSpacing($tweet->find('p.js-tweet-text', 0), '<a>'));
$item['title'] = strip_tags($this->fixAnchorSpacing(htmlspecialchars_decode(
$tweet->find('p.js-tweet-text', 0), ENT_QUOTES), '<a>'));
switch($this->queriedContext) {
case 'By list':
@@ -258,16 +294,17 @@ EOD;
}
$item['content'] = <<<EOD
{$item['content']}
<hr>
<div style="display: inline-block; vertical-align: top;">
<blockquote>{$cleanedQuotedTweet}</blockquote>
</div>
<div style="display: block; vertical-align: top;">
<blockquote>{$quotedImage_html}</blockquote>
</div>
<hr>
{$item['content']}
EOD;
}
$item['content'] = htmlspecialchars_decode($item['content'], ENT_QUOTES);
// put out
$this->items[] = $item;

View File

@@ -16,6 +16,11 @@ class WhydBridge extends BridgeAbstract {
private $userName = '';
public function getIcon() {
return self::URI . 'assets/favicons/
32-6b62a9f14d5e1a9213090d8f00f286bba3a6022381a76390d1d0926493b12593.png?v=6';
}
public function collectData(){
$html = '';
if(strlen(preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))) == 24) {

View File

@@ -93,7 +93,7 @@ class WordPressBridge extends FeedExpander {
}
try{
$this->collectExpandableDatas($this->getURI() . '/feed/atom/');
} catch (HttpException $e) {
} catch (Exception $e) {
$this->collectExpandableDatas($this->getURI() . '/?feed=atom');
}

View File

@@ -74,10 +74,10 @@ class WordPressPluginUpdateBridge extends BridgeAbstract {
}
private function getCachedDate($url){
debugMessage('getting pubdate from url ' . $url . '');
Debug::log('getting pubdate from url ' . $url . '');
// Initialize cache
$cache = Cache::create('FileCache');
$cache->setPath(CACHE_DIR . '/pages');
$cache->setPath(PATH_CACHE . 'pages/');
$params = [$url];
$cache->setParameters($params);
// Get cachefile timestamp

462
bridges/XenForoBridge.php Normal file
View File

@@ -0,0 +1,462 @@
<?php
/**
* This bridge generates feeds for threads from forums running XenForo version 2
*
* Examples:
* - https://xenforo.com/community/
* - http://www.ign.com/boards/
*
* Notice: XenForo does provide RSS feeds for forums. For example:
* - https://xenforo.com/community/forums/-/index.rss
*
* For more information on XenForo, visit
* - https://xenforo.com/
* - https://en.wikipedia.org/wiki/XenForo
*/
class XenForoBridge extends BridgeAbstract {
// Bridge specific constants
const CONTEXT_THREAD = 'Thread';
const XENFORO_VERSION_1 = '1.0';
const XENFORO_VERSION_2 = '2.0';
// RSS-Bridge constants
const NAME = 'XenForo Bridge';
const URI = 'https://xenforo.com/';
const DESCRIPTION = 'Generates feeds for threads in forums powered by XenForo';
const MAINTAINER = 'logmanoriginal';
const PARAMETERS = array(
self::CONTEXT_THREAD => array(
'url' => array(
'name' => 'Thread URL',
'type' => 'text',
'required' => true,
'title' => 'Insert URL to the thread for which the feed should be generated',
'exampleValue' => 'https://xenforo.com/community/threads/guide-to-suggestions.2285/'
)
),
'global' => array(
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'required' => false,
'title' => 'Specify maximum number of elements to return in the feed',
'defaultValue' => 10
)
)
);
const CACHE_TIMEOUT = 7200; // 10 minutes
private $title = '';
private $threadurl = '';
private $version; // Holds the XenForo version
public function getName() {
switch($this->queriedContext) {
case self::CONTEXT_THREAD: return $this->title . ' - ' . static::NAME;
}
return parent::getName();
}
public function getURI() {
switch($this->queriedContext) {
case self::CONTEXT_THREAD: return $this->threadurl;
}
return parent::getURI();
}
public function collectData() {
$this->threadurl = filter_var(
$this->getInput('url'),
FILTER_VALIDATE_URL,
FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED | FILTER_FLAG_PATH_REQUIRED);
if($this->threadurl === false) {
returnClientError('The URL you provided is invalid!');
}
$urlparts = parse_url($this->threadurl, PHP_URL_SCHEME);
// Scheme must be "http" or "https"
if(preg_match('/http[s]{0,1}/', parse_url($this->threadurl, PHP_URL_SCHEME)) == false) {
returnClientError('The URL you provided doesn\'t specify a valid scheme (http or https)!');
}
// Path cannot be root (../)
if(parse_url($this->threadurl, PHP_URL_PATH) === '/') {
returnClientError('The URL you provided doesn\'t link to a valid thread (root path)!');
}
// XenForo adds a thread ID to the URL, like "...-thread.454934283". It must be present
if(preg_match('/.+\.\d+[\/]{0,1}/', parse_URL($this->threadurl, PHP_URL_PATH)) == false) {
returnClientError('The URL you provided doesn\'t link to a valid thread (ID missing)!');
}
// We want to start at the first page in the thread. XenForo uses "../page-n" syntax
// to identify pages (except for the first page).
// Notice: XenForo uses the concept of "sentinels" to find and replace parts in the
// URL. Technically forum hosts can change the syntax!
if(preg_match('/.+\/(page-\d+.*)$/', $this->threadurl, $matches) != false) {
// before: https://xenforo.com/community/threads/guide-to-suggestions.2285/page-5
// after : https://xenforo.com/community/threads/guide-to-suggestions.2285/
$this->threadurl = str_replace($matches[1], '', $this->threadurl);
}
$html = getSimpleHTMLDOMCached($this->threadurl)
or returnServerError('Failed loading data from "' . $this->threadurl . '"!');
$html = defaultLinkTo($html, $this->threadurl);
// 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)) {
$this->version = self::XENFORO_VERSION_2;
} else {
returnServerError('This forum is currently not supported!');
}
switch($this->version) {
case self::XENFORO_VERSION_1:
$titleBar = $mainContent->find('div.titleBar h1', 0)
or returnServerError('Error finding title bar!');
$this->title = $titleBar->plaintext;
// Store items from current page (we'll use $this->items as LIFO buffer)
$this->extractThreadPostsV1($html, $this->threadurl);
$this->extractPagesV1($html);
break;
case self::XENFORO_VERSION_2:
$titleBar = $mainContent->find('div[class="p-title"] h1', 0)
or returnServerError('Error finding title bar!');
$this->title = $titleBar->plaintext;
$this->extractThreadPostsV2($html, $this->threadurl);
$this->extractPagesV2($html);
break;
}
while(count($this->items) > $this->getInput('limit')) {
array_shift($this->items);
}
}
/**
* Extracts thread posts
* @param $html A simplehtmldom object
* @param $url The url from which $html was loaded
*/
private function extractThreadPostsV1($html, $url) {
$lang = $html->find('html', 0)->lang;
// Posts are contained in an "ol"
$messageList = $html->find('#messageList li')
or returnServerError('Error finding message list!');
foreach($messageList as $post) {
if(!isset($post->attr['id'])) { // Skip ads
continue;
}
$item = array();
$item['uri'] = $url . '#' . $post->getAttribute('id');
$content = $post->find('.messageContent article', 0);
// Add some style to quotes
foreach($content->find('.bbCodeQuote') as $quote) {
$quote->style = '
color: #495566;
background-color: rgb(248,251,253);
border: 1px solid rgb(111, 140, 180);
border-color: rgb(111, 140, 180);
font-style: italic;';
}
// Remove script tags
foreach($content->find('script') as $script) {
$script->outertext = '';
}
$item['content'] = $content->innertext;
// Remove quotes (for the title)
foreach($content->find('.bbCodeQuote') as $quote) {
$quote->innertext = '';
}
$title = trim($content->plaintext);
if(strlen($title) > 70) {
$item['title'] = substr($title, 0, strpos($title, ' ', 70)) . '...';
} else {
$item['title'] = $title;
}
/**
* Timestamps are presented in two forms:
*
* 1) short version (for older posts?)
* <span
* class="DateTime"
* title="22 Oct. 2018 at 23:47"
* >22 Oct. 2018</span>
*
* This form has to be interpreted depending on the current language.
*
* 2) long version (for newer posts?)
* <abbr
* class="DateTime"
* data-time="1541008785"
* data-diff="310694"
* data-datestring="31 Oct. 2018"
* data-timestring="18:59"
* title="31 Oct. 2018 at 18:59"
* >Wednesday at 18:59</abbr>
*
* This form has the timestamp embedded (data-time)
*/
if($timestamp = $post->find('abbr.DateTime', 0)) { // long version (preffered)
$item['timestamp'] = $timestamp->{'data-time'};
} elseif($timestamp = $post->find('span.DateTime', 0)) { // short version
$item['timestamp'] = $this->fixDate($timestamp->title, $lang);
}
$item['author'] = $post->getAttribute('data-author');
// Bridge specific properties
$item['id'] = $post->getAttribute('id');
$this->items[] = $item;
}
}
private function extractThreadPostsV2($html, $url) {
$lang = $html->find('html', 0)->lang;
$messageList = $html->find('div[class="block-body"] article')
or returnServerError('Error finding message list!');
foreach($messageList as $post) {
if(!isset($post->attr['id'])) { // Skip ads
continue;
}
$item = array();
$item['uri'] = $url . '#' . $post->getAttribute('id');
$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);
$item['author'] = $post->getAttribute('data-author');
$item['content'] = $post->find('div[class="message-content"] article', 0);
// Bridge specific properties
$item['id'] = $post->getAttribute('id');
$this->items[] = $item;
}
}
private function extractPagesV1($html) {
// A navigation bar becomes available if the number of posts grows too
// high. When this happens we need to load further pages (from last backwards)
if(($pageNav = $html->find('div.PageNav', 0))) {
$lastpage = $pageNav->{'data-last'};
$baseurl = $pageNav->{'data-baseurl'};
$sentinel = $pageNav->{'data-sentinel'};
$hosturl = parse_url($this->threadurl, PHP_URL_SCHEME)
. '://'
. parse_url($this->threadurl, PHP_URL_HOST)
. '/';
$page = $lastpage;
// Load at least the last page
do {
$pageurl = $hosturl . str_replace($sentinel, $lastpage, $baseurl);
// We can optimize performance by caching all but the last page
if($page != $lastpage) {
$html = getSimpleHTMLDOMCached($pageurl)
or returnServerError('Error loading contents from ' . $pageurl . '!');
} else {
$html = getSimpleHTMLDOM($pageurl)
or returnServerError('Error loading contents from ' . $pageurl . '!');
}
$html = defaultLinkTo($html, $hosturl);
$this->extractThreadPostsV1($html, $pageurl);
$page--;
} while (count($this->items) < $this->getInput('limit') && $page != 1);
}
}
private function extractPagesV2($html) {
// A navigation bar becomes available if the number of posts grows too
// high. When this happens we need to load further pages (from last backwards)
if(($pageNav = $html->find('div.pageNav', 0))) {
foreach($pageNav->find('li') as $nav) {
$lastpage = $nav->plaintext;
}
// Manually extract baseurl and inject sentinel
$baseurl = $pageNav->find('li a', -1)->href;
$baseurl = str_replace('page-' . $lastpage, 'page-{{sentinel}}', $baseurl);
$sentinel = '{{sentinel}}';
$hosturl = parse_url($this->threadurl, PHP_URL_SCHEME)
. '://'
. parse_url($this->threadurl, PHP_URL_HOST);
$page = $lastpage;
// Load at least the last page
do {
$pageurl = $hosturl . str_replace($sentinel, $lastpage, $baseurl);
// We can optimize performance by caching all but the last page
if($page != $lastpage) {
$html = getSimpleHTMLDOMCached($pageurl)
or returnServerError('Error loading contents from ' . $pageurl . '!');
} else {
$html = getSimpleHTMLDOM($pageurl)
or returnServerError('Error loading contents from ' . $pageurl . '!');
}
$html = defaultLinkTo($html, $this->hosturl);
$this->extractThreadPostsV2($html, $this->pageurl);
$page--;
} while (count($this->items) < $this->getInput('limit') && $page != 1);
}
}
/**
* Fixes dates depending on the choosen language:
*
* de : dd.mm.yy
* en : dd.mm.yy
* it : dd/mm/yy
*
* Basically strtotime doesn't convert dates correctly due to formats
* being hard to interpret. So we use the DateTime object.
*
* We don't know the timezone, so just assume +00:00 (or whatever
* DateTime chooses)
*/
private function fixDate($date, $lang = 'en-US') {
$mnamesen = [
'January',
'Feburary',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
switch($lang) {
case 'en-US': // example: Jun 9, 2018 at 11:46 PM
$df = date_create_from_format('M d, Y \a\t H:i A', $date);
break;
case 'de-DE': // example: 19 Juli 2018 um 19:27 Uhr
$mnamesde = [
'Januar',
'Februar',
'März',
'April',
'Mai',
'Juni',
'Juli',
'August',
'September',
'Oktober',
'November',
'Dezember'
];
$mnamesdeshort = [
'Jan.',
'Feb.',
'Mär.',
'Apr.',
'Mai',
'Juni',
'Juli',
'Aug.',
'Sep.',
'Okt.',
'Nov.',
'Dez.'
];
$date = str_ireplace($mnamesde, $mnamesen, $date);
$date = str_ireplace($mnamesdeshort, $mnamesen, $date);
$df = date_create_from_format('d M Y \u\m H:i \U\h\r', $date);
break;
}
// Debug::log(date_format($df, 'U'));
return date_format($df, 'U');
}
}

View File

@@ -115,6 +115,7 @@ class YGGTorrentBridge extends BridgeAbstract {
$item = array();
$item['timestamp'] = $row->find('.hidden', 1)->plaintext;
$item['title'] = $row->find('a', 1)->plaintext;
$item['uri'] = $row->find('a', 1)->href;
$torrentData = $this->collectTorrentData($row->find('a', 1)->href);
$item['author'] = $torrentData['author'];
$item['content'] = $torrentData['content'];

View File

@@ -147,12 +147,19 @@ class YoutubeBridge extends BridgeAbstract {
$time = 0;
$vid = str_replace('/watch?v=', '', $element->find('a', 0)->href);
$vid = substr($vid, 0, strpos($vid, '&') ?: strlen($vid));
$title = $this->ytBridgeFixTitle($element->find($title_selector, 0)->plaintext);
$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('span[class="video-time"]', 0)->plaintext);
$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);
@@ -162,7 +169,6 @@ class YoutubeBridge extends BridgeAbstract {
continue;
}
if($title != '[Private Video]' && strpos($vid, 'googleads') === false) {
if ($add_parsed_items) {
$this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
@@ -170,7 +176,6 @@ class YoutubeBridge extends BridgeAbstract {
$count++;
}
}
}
return $count;
}
@@ -181,7 +186,9 @@ class YoutubeBridge extends BridgeAbstract {
private function ytGetSimpleHTMLDOM($url){
return getSimpleHTMLDOM($url,
$header = array(),
$header = array(
'Accept-Language: en-US'
),
$opts = array(),
$lowercase = true,
$forceTagsClosed = true,

View File

@@ -0,0 +1,95 @@
<?php
class ZoneTelechargementBridge extends BridgeAbstract {
/* This bridge was initally done for the Website Zone Telechargement,
* but the website changed it's name and URL.
* Therefore, the class name and filename does not correspond to the
* name of the bridge. This permits to keep the same RSS Feed URL.
*/
const NAME = 'Annuaire Telechargement';
const URI = 'https://www.annuaire-telechargement.com/';
const DESCRIPTION = 'Suivi de série sur Annuaire Telechargement';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
'Suivre la publication des épisodes d\'une série en cours de diffusion' => array(
'url' => array(
'name' => 'URL de la série',
'type' => 'text',
'required' => true,
'title' => 'URL d\'une série sans le https://www.annuaire-telechargement.com/',
'exampleValue' => 'telecharger-series/31079-halt-and-catch-fire-saison-4-french-hd720p.html'
)
)
);
public function getIcon() {
return 'https://www.annuaire-telechargement.com/templates/Default/images/favicon.ico';
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI . $this->getInput('url'))
or returnServerError('Could not request Zone Telechargement.');
// Get the TV show title
$qualityselector = 'div[style=font-size: 18px;margin: 10px auto;color:red;font-weight:bold;text-align:center;]';
$show = trim($html->find('div[class=smallsep]', 0)->next_sibling()->plaintext);
$quality = trim(explode("\n", $html->find($qualityselector, 0)->plaintext)[0]);
$this->showTitle = $show . ' ' . $quality;
// Get the post content
$linkshtml = $html->find('div[class=postinfo]', 0);
$episodes = array();
$list = $linkshtml->find('a');
// Construct the tabble of episodes using the links
foreach($list as $element) {
// Retrieve episode number from link text
$epnumber = explode(' ', $element->plaintext)[1];
$hoster = $this->findLinkHoster($element);
// Format the link and add the link to the corresponding episode table
$episodes[$epnumber][] = '<a href="' . $element->href . '">' . $hoster . ' - '
. $this->showTitle . ' Episode ' . $epnumber . '</a>';
}
// Finally construct the items array
foreach($episodes as $epnum => $episode) {
$item = array();
// Add every link available in the episode table separated by a <br/> tag
$item['content'] = implode('<br/>', $episode);
$item['title'] = $this->showTitle . ' Episode ' . $epnum;
// As RSS Bridge use the URI as GUID they need to be unique : adding a md5 hash of the title element
// should geneerate unique URI to prevent confusion for RSS readers
$item['uri'] = self::URI . $this->getInput('url') . '#' . hash('md5', $item['title']);
// Insert the episode at the beginning of the item list, to show the newest episode first
array_unshift($this->items, $item);
}
}
public function getName() {
switch($this->queriedContext) {
case 'Suivre la publication des épisodes d\'une série en cours de diffusion':
return $this->showTitle . ' - ' . self::NAME;
break;
default:
return self::NAME;
}
}
private function findLinkHoster($element) {
// The hoster name is one level higher than the link tag : get the parent element
$element = $element->parent();
//echo "PARENT : $element \n";
$continue = true;
// Walk through all elements in the reverse order until finding the one with a div and that is not a <br/>
while(!($element->find('div', 0) != null && $element->tag != 'br')) {
$element = $element->prev_sibling();
}
// Return the text of the div : it's the file hoster name !
return $element->find('div', 0)->plaintext;
}
}

View File

@@ -11,6 +11,12 @@
; false = disabled (default)
custom_timeout = false
[admin]
; Advertise an email address where people can reach the administrator.
; This address is displayed on the main page, visible to everyone!
; "" = Disabled (default)
email = ""
[proxy]
; Sets the proxy url (i.e. "tcp://192.168.0.0:32")

View File

@@ -11,11 +11,11 @@ class AtomFormat extends FormatAbstract{
$httpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
$httpInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
$serverRequestUri = $this->xml_encode($_SERVER['REQUEST_URI']);
$serverRequestUri = isset($_SERVER['REQUEST_URI']) ? $this->xml_encode($_SERVER['REQUEST_URI']) : '';
$extraInfos = $this->getExtraInfos();
$title = $this->xml_encode($extraInfos['name']);
$uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : 'https://github.com/RSS-Bridge/rss-bridge';
$uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
$uriparts = parse_url($uri);
if(!empty($extraInfos['icon'])) {

View File

@@ -85,6 +85,8 @@ EOD;
<meta charset="{$charset}">
<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}" />
<meta name="robots" content="noindex, follow">
</head>
<body>

View File

@@ -10,7 +10,7 @@ class MrssFormat extends FormatAbstract {
$httpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
$httpInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
$serverRequestUri = $this->xml_encode($_SERVER['REQUEST_URI']);
$serverRequestUri = isset($_SERVER['REQUEST_URI']) ? $this->xml_encode($_SERVER['REQUEST_URI']) : '';
$extraInfos = $this->getExtraInfos();
$title = $this->xml_encode($extraInfos['name']);
@@ -18,7 +18,7 @@ class MrssFormat extends FormatAbstract {
if(!empty($extraInfos['uri'])) {
$uri = $this->xml_encode($extraInfos['uri']);
} else {
$uri = 'https://github.com/RSS-Bridge/rss-bridge';
$uri = REPOSITORY;
}
$uriparts = parse_url($uri);
@@ -79,6 +79,8 @@ EOD;
$charset = $this->getCharset();
/* xml attributes need to have certain characters escaped to be w3c compliant */
$imageTitle = htmlspecialchars($title, ENT_COMPAT);
/* Data are prepared, now let's begin the "MAGIE !!!" */
$toReturn = <<<EOD
<?xml version="1.0" encoding="{$charset}"?>
@@ -90,7 +92,7 @@ xmlns:atom="http://www.w3.org/2005/Atom">
<title>{$title}</title>
<link>http{$https}://{$httpHost}{$httpInfo}/</link>
<description>{$title}</description>
<image url="{$icon}" title="{$title}" link="{$uri}"/>
<image url="{$icon}" title="{$imageTitle}" link="{$uri}"/>
<atom:link rel="alternate" type="text/html" href="{$uri}" />
<atom:link rel="self" href="http{$https}://{$httpHost}{$serverRequestUri}" />
{$items}

283
index.php
View File

@@ -1,40 +1,5 @@
<?php
/*
Create a file named 'DEBUG' for enabling debug mode.
For further security, you may put whitelisted IP addresses in the file,
one IP per line. Empty file allows anyone(!).
Debugging allows displaying PHP error messages and bypasses the cache: this
can allow a malicious client to retrieve data about your server and hammer
a provider throught your rss-bridge instance.
*/
if(file_exists('DEBUG')) {
$debug_whitelist = trim(file_get_contents('DEBUG'));
$debug_enabled = empty($debug_whitelist)
|| in_array($_SERVER['REMOTE_ADDR'],
explode("\n", str_replace("\r", '', $debug_whitelist)
)
);
if($debug_enabled) {
ini_set('display_errors', '1');
error_reporting(E_ALL);
define('DEBUG', true);
if (empty($debug_whitelist)) {
define('DEBUG_INSECURE', true);
}
}
}
require_once __DIR__ . '/lib/RssBridge.php';
define('PHP_VERSION_REQUIRED', '5.6.0');
// Specify directory for cached files (using FileCache)
define('CACHE_DIR', __DIR__ . '/cache');
// Specify path for whitelist file
define('WHITELIST_FILE', __DIR__ . '/whitelist.txt');
require_once __DIR__ . '/lib/rssbridge.php';
Configuration::verifyInstallation();
Configuration::loadConfiguration();
@@ -54,13 +19,15 @@ if (isset($argv)) {
$params = $_GET;
}
// FIXME : beta test UA spoofing, please report any blacklisting by PHP-fopen-unfriendly websites
define('USER_AGENT',
'Mozilla/5.0 (X11; Linux x86_64; rv:30.0) Gecko/20121202 Firefox/30.0(rss-bridge/'
. Configuration::$VERSION
. ';+'
. REPOSITORY
. ')'
);
$userAgent = 'Mozilla/5.0(X11; Linux x86_64; rv:30.0)';
$userAgent .= ' Gecko/20121202 Firefox/30.0(rss-bridge/0.1;';
$userAgent .= '+https://github.com/RSS-Bridge/rss-bridge)';
ini_set('user_agent', $userAgent);
ini_set('user_agent', USER_AGENT);
// default whitelist
$whitelist_default = array(
@@ -69,7 +36,7 @@ $whitelist_default = array(
'DansTonChatBridge',
'DuckDuckGoBridge',
'FacebookBridge',
'FlickrExploreBridge',
'FlickrBridge',
'GooglePlusPostBridge',
'GoogleSearchBridge',
'IdenticaBridge',
@@ -83,26 +50,7 @@ $whitelist_default = array(
try {
Bridge::setDir(__DIR__ . '/bridges/');
Format::setDir(__DIR__ . '/formats/');
Cache::setDir(__DIR__ . '/caches/');
if(!file_exists(WHITELIST_FILE)) {
$whitelist_selection = $whitelist_default;
$whitelist_write = implode("\n", $whitelist_default);
file_put_contents(WHITELIST_FILE, $whitelist_write);
} else {
$whitelist_file_content = file_get_contents(WHITELIST_FILE);
if($whitelist_file_content != "*\n") {
$whitelist_selection = explode("\n", $whitelist_file_content);
} else {
$whitelist_selection = Bridge::listBridges();
}
// Prepare for case-insensitive match
$whitelist_selection = array_map('strtolower', $whitelist_selection);
}
Bridge::setWhitelist($whitelist_default);
$showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
$action = array_key_exists('action', $params) ? $params['action'] : null;
@@ -115,7 +63,7 @@ try {
$list->bridges = array();
$list->total = 0;
foreach(Bridge::listBridges() as $bridgeName) {
foreach(Bridge::getBridgeNames() as $bridgeName) {
$bridge = Bridge::create($bridgeName);
@@ -129,7 +77,7 @@ try {
}
$status = Bridge::isWhitelisted($whitelist_selection, strtolower($bridgeName)) ? 'active' : 'inactive';
$status = Bridge::isWhitelisted($bridgeName) ? 'active' : 'inactive';
$list->bridges[$bridgeName] = array(
'status' => $status,
@@ -148,13 +96,44 @@ try {
header('Content-Type: application/json');
echo json_encode($list, JSON_PRETTY_PRINT);
} elseif($action === 'display' && !empty($bridge)) {
// DEPRECATED: 'nameBridge' scheme is replaced by 'name' in bridge parameter values
// this is to keep compatibility until futher complete removal
if(($pos = strpos($bridge, 'Bridge')) === (strlen($bridge) - strlen('Bridge'))) {
$bridge = substr($bridge, 0, $pos);
} elseif($action === 'detect') {
$targetURL = $params['url']
or returnClientError('You must specify a url!');
$format = $params['format']
or returnClientError('You must specify a format!');
foreach(Bridge::getBridgeNames() as $bridgeName) {
if(!Bridge::isWhitelisted($bridgeName)) {
continue;
}
$bridge = Bridge::create($bridgeName);
if($bridge === false) {
continue;
}
$bridgeParams = $bridge->detectParameters($targetURL);
if(is_null($bridgeParams)) {
continue;
}
$bridgeParams['bridge'] = $bridgeName;
$bridgeParams['format'] = $format;
header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
die();
}
returnClientError('No bridge found for given URL: ' . $targetURL);
} elseif($action === 'display' && !empty($bridge)) {
$format = $params['format']
or returnClientError('You must specify a format!');
@@ -165,8 +144,8 @@ try {
}
// whitelist control
if(!Bridge::isWhitelisted($whitelist_selection, strtolower($bridge))) {
throw new \HttpException('This bridge is not whitelisted', 401);
if(!Bridge::isWhitelisted($bridge)) {
throw new \Exception('This bridge is not whitelisted', 401);
die;
}
@@ -178,67 +157,161 @@ try {
define('NOPROXY', true);
}
// Custom cache timeout
// Cache timeout
$cache_timeout = -1;
if(array_key_exists('_cache_timeout', $params)) {
if(!CUSTOM_CACHE_TIMEOUT) {
throw new \HttpException('This server doesn\'t support "_cache_timeout"!');
unset($params['_cache_timeout']);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($params);
header('Location: ' . $uri, true, 301);
die();
}
$cache_timeout = filter_var($params['_cache_timeout'], FILTER_VALIDATE_INT);
} else {
$cache_timeout = $bridge->getCacheTimeout();
}
// Remove parameters that don't concern bridges
$bridge_params = array_diff_key(
$params,
array_fill_keys(
array(
'action',
'bridge',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
// Remove parameters that don't concern caches
$cache_params = array_diff_key(
$params,
array_fill_keys(
array(
'action',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
// Initialize cache
$cache = Cache::create('FileCache');
$cache->setPath(CACHE_DIR);
$cache->setPath(PATH_CACHE);
$cache->purgeCache(86400); // 24 hours
$cache->setParameters($params);
$cache->setParameters($cache_params);
unset($params['action']);
unset($params['bridge']);
unset($params['format']);
unset($params['_noproxy']);
unset($params['_cache_timeout']);
$items = array();
$infos = array();
$mtime = $cache->getTime();
if($mtime !== false
&& (time() - $cache_timeout < $mtime)
&& !Debug::isEnabled()) { // Load cached data
// Send "Not Modified" response if client supports it
// Implementation based on https://stackoverflow.com/a/10847262
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if($mtime <= $stime) { // Cached data is older or same
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304);
die();
}
}
$cached = $cache->loadData();
if(isset($cached['items']) && isset($cached['extraInfos'])) {
$items = $cached['items'];
$infos = $cached['extraInfos'];
}
} else { // Collect new data
// Load cache & data
try {
$bridge->setCache($cache);
$bridge->setCacheTimeout($cache_timeout);
$bridge->dieIfNotModified();
$bridge->setDatas($params);
$bridge->setDatas($bridge_params);
$bridge->collectData();
$items = $bridge->getItems();
$infos = array(
'name' => $bridge->getName(),
'uri' => $bridge->getURI(),
'icon' => $bridge->getIcon()
);
} catch(Error $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildBridgeException($e, $bridge));
error_log($e);
$item = array();
// Create "new" error message every 24 hours
$params['_error_time'] = urlencode((int)(time() / 86400));
// Error 0 is a special case (i.e. "trying to get property of non-object")
if($e->getCode() === 0) {
$item['title'] = 'Bridge encountered an unexpected situation! (' . $params['_error_time'] . ')';
} else {
$item['title'] = 'Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')';
}
$item['uri'] = (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?' . http_build_query($params);
$item['timestamp'] = time();
$item['content'] = buildBridgeException($e, $bridge);
$items[] = $item;
} catch(Exception $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildBridgeException($e, $bridge));
error_log($e);
$item = array();
// Create "new" error message every 24 hours
$params['_error_time'] = urlencode((int)(time() / 86400));
$item['uri'] = (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?' . http_build_query($params);
$item['title'] = 'Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')';
$item['timestamp'] = time();
$item['content'] = buildBridgeException($e, $bridge);
$items[] = $item;
}
// Store data in cache
$cache->saveData(array(
'items' => $items,
'extraInfos' => $infos
));
}
// Data transformation
try {
$format = Format::create($format);
$format->setItems($bridge->getItems());
$format->setExtraInfos($bridge->getExtraInfos());
$format->setLastModified($bridge->getCacheTime());
$format->setItems($items);
$format->setExtraInfos($infos);
$format->setLastModified($cache->getTime());
$format->display();
} catch(Error $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
} catch(Exception $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildBridgeException($e, $bridge));
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
}
} else {
echo BridgeList::create($whitelist_selection, $showInactive);
echo BridgeList::create($showInactive);
}
} catch(HttpException $e) {
http_response_code($e->getCode());
header('Content-Type: text/plain');
die($e->getMessage());
} catch(\Exception $e) {
error_log($e);
header('Content-Type: text/plain', true, $e->getCode());
die($e->getMessage());
}

View File

@@ -1,12 +1,61 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Authentication module for RSS-Bridge.
*
* This class implements an authentication module for RSS-Bridge, utilizing the
* HTTP authentication capabilities of PHP.
*
* _Notice_: Authentication via HTTP does not prevent users from accessing files
* on your server. If your server supports `.htaccess`, you should globally restrict
* access to files instead.
*
* @link https://php.net/manual/en/features.http-auth.php HTTP authentication with PHP
* @link https://httpd.apache.org/docs/2.4/howto/htaccess.html Apache HTTP Server
* Tutorial: .htaccess files
*
* @todo Configuration parameters should be stored internally instead of accessing
* the configuration class directly.
* @todo Add functions to detect if a user is authenticated or not. This can be
* utilized for limiting access to authorized users only.
*/
class Authentication {
/**
* Throw an exception when trying to create a new instance of this class.
* Use {@see Authentication::showPromptIfNeeded()} instead!
*
* @throws \LogicException if called.
*/
public function __construct(){
throw new \LogicException('Use ' . __CLASS__ . '::showPromptIfNeeded()!');
}
/**
* Requests the user for login credentials if necessary.
*
* Responds to an authentication request or returns the `WWW-Authenticate`
* header if authentication is enabled in the configuration of RSS-Bridge
* (`[authentication] enable = true`).
*
* @return void
*/
public static function showPromptIfNeeded() {
if(Configuration::getConfig('authentication', 'enable') === true) {
if(!Authentication::verifyPrompt()) {
header('WWW-Authenticate: Basic realm="RSS-Bridge"');
header('HTTP/1.0 401 Unauthorized');
header('WWW-Authenticate: Basic realm="RSS-Bridge"', true, 401);
die('Please authenticate in order to access this instance !');
}
@@ -14,6 +63,13 @@ class Authentication {
}
/**
* Verifies if an authentication request was received and compares the
* provided username and password to the configuration of RSS-Bridge
* (`[authentication] username` and `[authentication] password`).
*
* @return bool True if authentication succeeded.
*/
public static function verifyPrompt() {
if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {

View File

@@ -1,88 +1,296 @@
<?php
require_once(__DIR__ . '/BridgeInterface.php');
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Factory class responsible for creating bridge objects from a given working
* directory, limited by a whitelist.
*
* This class is capable of:
* - Locating bridge classes in the specified working directory (see {@see Bridge::$workingDir})
* - Filtering bridges based on a whitelist (see {@see Bridge::$whitelist})
* - Creating new bridge instances based on the bridge's name (see {@see Bridge::create()})
*
* The following example illustrates the intended use for this class.
*
* ```PHP
* require_once __DIR__ . '/rssbridge.php';
*
* // Step 1: Set the working directory
* Bridge::setWorkingDir(__DIR__ . '/../bridges/');
*
* // Step 2: Add bridges to the whitelist
* Bridge::setWhitelist(array('GitHubIssue', 'GoogleSearch', 'Facebook', 'Twitter'));
*
* // Step 3: Create a new instance of a bridge (based on the name)
* $bridge = Bridge::create('GitHubIssue');
* ```
*/
class Bridge {
static protected $dirBridge;
/**
* 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;
/**
* Holds a list of whitelisted bridges.
*
* Do not access this property directly!
* Use {@see Bridge::getWhitelist()} instead.
*
* @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('Please use ' . __CLASS__ . '::create for new object.');
throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create bridge objects!');
}
/**
* Create a new bridge object
* @param string $nameBridge Defined bridge name you want use
* @return Bridge object dedicated
* Creates a new bridge object from the working directory.
*
* @throws \InvalidArgumentException if the requested bridge name is invalid.
* @throws \Exception if the requested bridge doesn't exist in the working
* directory.
* @param string $name Name of the bridge object.
* @return object|bool The bridge object or false if the class is not instantiable.
*/
static public function create($nameBridge){
if(!preg_match('@^[A-Z][a-zA-Z0-9-]*$@', $nameBridge)) {
$message = <<<EOD
'nameBridge' must start with one uppercase character followed or not by
alphanumeric or dash characters!
EOD;
throw new \InvalidArgumentException($message);
public static function create($name){
if(!self::isBridgeName($name)) {
throw new \InvalidArgumentException('Bridge name invalid!');
}
$nameBridge = $nameBridge . 'Bridge';
$pathBridge = self::getDir() . $nameBridge . '.php';
$name = self::sanitizeBridgeName($name) . 'Bridge';
$filePath = self::getWorkingDir() . $name . '.php';
if(!file_exists($pathBridge)) {
throw new \Exception('The bridge you looking for does not exist. It should be at path '
. $pathBridge);
if(!file_exists($filePath)) {
throw new \Exception('Bridge file ' . $filePath . ' does not exist!');
}
require_once $pathBridge;
require_once $filePath;
if((new ReflectionClass($nameBridge))->isInstantiable()) {
return new $nameBridge();
if((new \ReflectionClass($name))->isInstantiable()) {
return new $name();
}
return false;
}
static public function setDir($dirBridge){
if(!is_string($dirBridge)) {
throw new \InvalidArgumentException('Dir bridge must be a string.');
/**
* 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($dirBridge)) {
throw new \Exception('Dir bridge does not exist.');
if(!file_exists($dir)) {
throw new \Exception('Working directory does not exist!');
}
self::$dirBridge = $dirBridge;
if(!is_dir($dir)) {
throw new \InvalidArgumentException('Working directory is not a directory!');
}
static public function getDir(){
if(is_null(self::$dirBridge)) {
throw new \LogicException(__CLASS__ . ' class need to know bridge path !');
}
return self::$dirBridge;
self::$workingDir = realpath($dir) . '/';
}
/**
* Lists the available bridges.
* @return array List of the bridges
* 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.
*/
static public function listBridges(){
$listBridge = array();
$dirFiles = scandir(self::getDir());
public static function getWorkingDir(){
if(is_null(self::$workingDir)) {
throw new \LogicException('Working directory is not set!');
}
if($dirFiles !== false) {
foreach($dirFiles as $fileName) {
if(preg_match('@^([^.]+)Bridge\.php$@U', $fileName, $out)) {
$listBridge[] = $out[1];
return self::$workingDir;
}
/**
* Returns true if the provided name is a valid bridge name.
*
* A valid bridge name starts with a capital letter ([A-Z]), followed by
* zero or more alphanumeric characters or hyphen ([A-Za-z0-9-]).
*
* @param string $name The bridge name.
* @return bool true if the name is a valid bridge name, false otherwise.
*/
public static function isBridgeName($name){
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
/**
* Returns the list of bridge names from the working directory.
*
* The list is cached internally to allow for successive calls.
*
* @return array List of bridge names
*/
public static function getBridgeNames(){
static $bridgeNames = array(); // Initialized on first call
if(empty($bridgeNames)) {
$files = scandir(self::getWorkingDir());
if($files !== false) {
foreach($files as $file) {
if(preg_match('/^([^.]+)Bridge\.php$/U', $file, $out)) {
$bridgeNames[] = $out[1];
}
}
}
}
return $listBridge;
return $bridgeNames;
}
static public function isWhitelisted($whitelist, $name){
return in_array($name, $whitelist)
|| in_array($name . '.php', $whitelist)
|| in_array($name . 'bridge', $whitelist) // DEPRECATED
|| in_array($name . 'bridge.php', $whitelist) // DEPRECATED
|| (count($whitelist) === 1 && trim($whitelist[0]) === '*');
/**
* Checks if a bridge is whitelisted.
*
* @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());
}
/**
* Returns the whitelist.
*
* On first call this function reads the whitelist from {@see WHITELIST}.
* * 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.
*
* Use {@see Bridge::setWhitelist()} to specify a default whitelist **before**
* calling this function! The list is cached internally to allow for
* successive calls. If {@see Bridge::setWhitelist()} gets called after this
* function, the whitelist is **not** updated again!
*
* @return array Array of whitelisted bridges
*/
public static function getWhitelist() {
static $firstCall = true; // Initialized on first call
if($firstCall) {
// Create initial whitelist or load from disk
if (!file_exists(WHITELIST) && !empty(self::$whitelist)) {
file_put_contents(WHITELIST, implode("\n", self::$whitelist));
} else {
$contents = trim(file_get_contents(WHITELIST));
if($contents === '*') { // Whitelist all bridges
self::$whitelist = self::getBridgeNames();
} else {
self::$whitelist = array_map('self::sanitizeBridgeName', explode("\n", $contents));
}
}
}
return self::$whitelist;
}
/**
* Sets the (default) whitelist.
*
* If this function is called **before** {@see Bridge::getWhitelist()}, the
* provided whitelist will be replaced by a custom whitelist specified in
* {@see WHITELIST} (if it exists).
*
* If this function is called **after** {@see Bridge::getWhitelist()}, the
* provided whitelist is taken as is (not updated by the custom whitelist
* again).
*
* @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);
}
/**
* Returns the sanitized bridge name.
*
* The bridge name can be specified in various ways:
* * The PHP file name (i.e. `GitHubIssueBridge.php`)
* * The PHP file name without file extension (i.e. `GitHubIssueBridge`)
* * The bridge name (i.e. `GitHubIssue`)
*
* Casing is ignored (i.e. `GITHUBISSUE` and `githubissue` are the same).
*
* A bridge file matching the given bridge name must exist in the working
* directory!
*
* @param string $name The bridge name
* @return string|null The sanitized bridge name if the provided name is
* valid, null otherwise.
*/
protected static function sanitizeBridgeName($name) {
if(is_string($name)) {
// Trim trailing '.php' if exists
if(preg_match('/(.+)(?:\.php)/', $name, $matches)) {
$name = $matches[1];
}
// Trim trailing 'Bridge' if exists
if(preg_match('/(.+)(?:Bridge)/i', $name, $matches)) {
$name = $matches[1];
}
// 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];
}
Debug::log('Invalid bridge name specified: "' . $name . '"!');
}
return null; // Bad parameter
}
}

View File

@@ -1,46 +1,112 @@
<?php
require_once(__DIR__ . '/BridgeInterface.php');
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* An abstract class for bridges
*
* This class implements {@see BridgeInterface} with most common functions in
* order to reduce code duplication. Bridges should inherit from this class
* instead of implementing the interface manually.
*
* @todo Move constants to the interface (this is supported by PHP)
* @todo Change visibility of constants to protected
* @todo Return `self` on more functions to allow chaining
* @todo Add specification for PARAMETERS ()
* @todo Add specification for $items
*/
abstract class BridgeAbstract implements BridgeInterface {
/**
* Name of the bridge
*
* Use {@see BridgeAbstract::getName()} to read this parameter
*/
const NAME = 'Unnamed bridge';
/**
* URI to the site the bridge is intended to be used for.
*
* Use {@see BridgeAbstract::getURI()} to read this parameter
*/
const URI = '';
/**
* A brief description of what the bridge can do
*
* Use {@see BridgeAbstract::getDescription()} to read this parameter
*/
const DESCRIPTION = 'No description provided';
/**
* The name of the maintainer. Multiple maintainers can be separated by comma
*
* Use {@see BridgeAbstract::getMaintainer()} to read this parameter
*/
const MAINTAINER = 'No maintainer';
/**
* The default cache timeout for the bridge
*
* Use {@see BridgeAbstract::getCacheTimeout()} to read this parameter
*/
const CACHE_TIMEOUT = 3600;
/**
* Parameters for the bridge
*
* Use {@see BridgeAbstract::getParameters()} to read this parameter
*/
const PARAMETERS = array();
protected $cache;
protected $extraInfos;
/**
* Holds the list of items collected by the bridge
*
* Items must be collected by {@see BridgeInterface::collectData()}
*
* Use {@see BridgeAbstract::getItems()} to access items.
*
* @var array
*/
protected $items = array();
/**
* Holds the list of input parameters used by the bridge
*
* Do not access this parameter directly!
* Use {@see BridgeAbstract::setInputs()} and {@see BridgeAbstract::getInput()} instead!
*
* @var array
*/
protected $inputs = array();
/**
* Holds the name of the queried context
*
* @var string
*/
protected $queriedContext = '';
protected $cacheTimeout;
/**
* Return cachable datas (extrainfos and items) stored in the bridge
* @return mixed
*/
public function getCachable(){
return array(
'items' => $this->getItems(),
'extraInfos' => $this->getExtraInfos()
);
}
/**
* Return items stored in the bridge
* @return mixed
*/
/** {@inheritdoc} */
public function getItems(){
return $this->items;
}
/**
* Sets the input values for a given context. Existing values are
* overwritten.
* Sets the input values for a given context.
*
* @param array $inputs Associative array of inputs
* @param string $context The context name
* @param string $queriedContext The context name
* @return void
*/
protected function setInputs(array $inputs, $queriedContext){
// Import and assign all inputs to their context
@@ -117,90 +183,43 @@ abstract class BridgeAbstract implements BridgeInterface {
}
/**
* Returns the name of the context matching the provided inputs
* Set inputs for the bridge
*
* @param array $inputs Associative array of inputs
* @return mixed Returns the context name or null if no match was found
*/
protected function getQueriedContext(array $inputs){
$queriedContexts = array();
// Detect matching context
foreach(static::PARAMETERS as $context => $set) {
$queriedContexts[$context] = null;
// Check if all parameters of the context are satisfied
foreach($set as $id => $properties) {
if(isset($inputs[$id]) && !empty($inputs[$id])) {
$queriedContexts[$context] = true;
} elseif(isset($properties['required'])
&& $properties['required'] === true) {
$queriedContexts[$context] = false;
break;
}
}
}
// Abort if one of the globally required parameters is not satisfied
if(array_key_exists('global', static::PARAMETERS)
&& $queriedContexts['global'] === false) {
return null;
}
unset($queriedContexts['global']);
switch(array_sum($queriedContexts)) {
case 0: // Found no match, is there a context without parameters?
foreach($queriedContexts as $context => $queried) {
if(is_null($queried)) {
return $context;
}
}
return null;
case 1: // Found unique match
return array_search(true, $queriedContexts);
default: return false;
}
}
/**
* Defined datas with parameters depending choose bridge
* Note : you can define a cache with "setCache"
* @param array array with expected bridge paramters
* Returns errors and aborts execution if the provided input parameters are
* invalid.
*
* @param array List of input parameters. Each element in this list must
* relate to an item in {@see BridgeAbstract::PARAMETERS}
* @return void
*/
public function setDatas(array $inputs){
if(!is_null($this->cache)) {
$time = $this->cache->getTime();
if($time !== false
&& (time() - $this->getCacheTimeout() < $time)
&& (!defined('DEBUG') || DEBUG !== true)) {
$cached = $this->cache->loadData();
if(isset($cached['items']) && isset($cached['extraInfos'])) {
$this->items = $cached['items'];
$this->extraInfos = $cached['extraInfos'];
return;
}
}
}
if(empty(static::PARAMETERS)) {
if(!empty($inputs)) {
returnClientError('Invalid parameters value(s)');
}
$this->collectData();
if(!is_null($this->cache)) {
$this->cache->saveData($this->getCachable());
}
return;
}
if(!validateData($inputs, static::PARAMETERS)) {
returnClientError('Invalid parameters value(s)');
$validator = new ParameterValidator();
if(!$validator->validateData($inputs, static::PARAMETERS)) {
$parameters = array_map(
function($i){ return $i['name']; }, // Just display parameter names
$validator->getInvalidParameters()
);
returnClientError(
'Invalid parameters value(s): '
. implode(', ', $parameters)
);
}
// Guess the paramter context from input data
$this->queriedContext = $this->getQueriedContext($inputs);
$this->queriedContext = $validator->getQueriedContext($inputs, static::PARAMETERS);
if(is_null($this->queriedContext)) {
returnClientError('Required parameter(s) missing');
} elseif($this->queriedContext === false) {
@@ -209,18 +228,13 @@ abstract class BridgeAbstract implements BridgeInterface {
$this->setInputs($inputs, $this->queriedContext);
$this->collectData();
if(!is_null($this->cache)) {
$this->cache->saveData($this->getCachable());
}
}
/**
* Returns the value for the provided input
*
* @param string $input The input name
* @return mixed Returns the input value or null if the input is not defined
* @return mixed|null The input value or null if the input is not defined
*/
protected function getInput($input){
if(!isset($this->inputs[$this->queriedContext][$input]['value'])) {
@@ -229,90 +243,52 @@ abstract class BridgeAbstract implements BridgeInterface {
return $this->inputs[$this->queriedContext][$input]['value'];
}
/** {@inheritdoc} */
public function getDescription(){
return static::DESCRIPTION;
}
/** {@inheritdoc} */
public function getMaintainer(){
return static::MAINTAINER;
}
/** {@inheritdoc} */
public function getName(){
// Return cached name when bridge is using cached data
if(isset($this->extraInfos)) {
return $this->extraInfos['name'];
}
return static::NAME;
}
/** {@inheritdoc} */
public function getIcon(){
// Return cached icon when bridge is using cached data
if(isset($this->extraInfos)) {
return $this->extraInfos['icon'];
}
return '';
}
/** {@inheritdoc} */
public function getParameters(){
return static::PARAMETERS;
}
/** {@inheritdoc} */
public function getURI(){
// Return cached uri when bridge is using cached data
if(isset($this->extraInfos)) {
return $this->extraInfos['uri'];
}
return static::URI;
}
public function getExtraInfos(){
return array(
'name' => $this->getName(),
'uri' => $this->getURI(),
'icon' => $this->getIcon()
);
}
public function setCache(\CacheInterface $cache){
$this->cache = $cache;
}
public function setCacheTimeout($timeout){
if(is_numeric($timeout) && ($timeout < 1 || $timeout > 86400)) {
$this->cacheTimeout = static::CACHE_TIMEOUT;
return;
}
$this->cacheTimeout = $timeout;
}
/** {@inheritdoc} */
public function getCacheTimeout(){
return isset($this->cacheTimeout) ? $this->cacheTimeout : static::CACHE_TIMEOUT;
return static::CACHE_TIMEOUT;
}
public function getCacheTime(){
return !is_null($this->cache) ? $this->cache->getTime() : false;
}
public function dieIfNotModified(){
if ((defined('DEBUG') && DEBUG === true)) return; // disabled in debug mode
$if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false;
if (!$if_modified_since) return; // If-Modified-Since value is required
$last_modified = $this->getCacheTime();
if (!$last_modified) return; // did not detect cache time
if (time() - $this->getCacheTimeout() > $last_modified) return; // cache timeout
$last_modified = (gmdate('D, d M Y H:i:s ', $last_modified) . 'GMT');
if ($if_modified_since == $last_modified) {
header('HTTP/1.1 304 Not Modified');
die();
/** {@inheritdoc} */
public function detectParameters($url){
$regex = '/^(https?:\/\/)?(www\.)?(.+?)(\/)?$/';
if(empty(static::PARAMETERS)
&& preg_match($regex, $url, $urlMatches) > 0
&& preg_match($regex, static::URI, $bridgeUriMatches) > 0
&& $urlMatches[3] === $bridgeUriMatches[3]) {
return array();
} else {
return null;
}
}
}

View File

@@ -1,6 +1,32 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* A generator class for a single bridge card on the home page of RSS-Bridge.
*
* This class generates the HTML content for a single bridge card for the home
* page of RSS-Bridge.
*
* @todo Return error if a caller creates an object of this class.
*/
final class BridgeCard {
/**
* Build a HTML document string of buttons for each of the provided formats
*
* @param array $formats A list of format names
* @return string The document string
*/
private static function buildFormatButtons($formats) {
$buttons = '';
@@ -16,6 +42,13 @@ final class BridgeCard {
return $buttons;
}
/**
* Get the form header for a bridge card
*
* @param string $bridgeName The bridge name
* @param bool $isHttps If disabled, adds a warning to the form
* @return string The form header
*/
private static function getFormHeader($bridgeName, $isHttps = false) {
$form = <<<EOD
<form method="GET" action="?">
@@ -31,13 +64,28 @@ This bridge is not fetching its content through a secure connection</div>';
return $form;
}
/**
* Get the form body for a bridge
*
* @param string $bridgeName The bridge name
* @param array $formats A list of supported formats
* @param bool $isActive Indicates if a bridge is enabled or not
* @param bool $isHttps Indicates if a bridge uses HTTPS or not
* @param string $parameterName Sets the bridge context for the current form
* @param array $parameters The bridge parameters
* @return string The form body
*/
private static function getForm($bridgeName,
$formats,
$isActive = false,
$isHttps = false,
$parameterName = '',
$parameters = array()) {
$form = BridgeCard::getFormHeader($bridgeName, $isHttps);
$form = self::getFormHeader($bridgeName, $isHttps);
if(count($parameters) > 0) {
$form .= '<div class="parameters">';
foreach($parameters as $id => $inputEntry) {
if(!isset($inputEntry['exampleValue']))
@@ -57,22 +105,26 @@ This bridge is not fetching its content through a secure connection</div>';
. $idArg
. '">'
. filter_var($inputEntry['name'], FILTER_SANITIZE_STRING)
. ' : </label>'
. '</label>'
. PHP_EOL;
if(!isset($inputEntry['type']) || $inputEntry['type'] === 'text') {
$form .= BridgeCard::getTextInput($inputEntry, $idArg, $id);
$form .= self::getTextInput($inputEntry, $idArg, $id);
} elseif($inputEntry['type'] === 'number') {
$form .= BridgeCard::getNumberInput($inputEntry, $idArg, $id);
$form .= self::getNumberInput($inputEntry, $idArg, $id);
} else if($inputEntry['type'] === 'list') {
$form .= BridgeCard::getListInput($inputEntry, $idArg, $id);
$form .= self::getListInput($inputEntry, $idArg, $id);
} elseif($inputEntry['type'] === 'checkbox') {
$form .= BridgeCard::getCheckboxInput($inputEntry, $idArg, $id);
$form .= self::getCheckboxInput($inputEntry, $idArg, $id);
}
}
$form .= '</div>';
}
if($isActive) {
$form .= BridgeCard::buildFormatButtons($formats);
$form .= self::buildFormatButtons($formats);
} else {
$form .= '<span style="font-weight: bold;">Inactive</span>';
}
@@ -80,6 +132,12 @@ This bridge is not fetching its content through a secure connection</div>';
return $form . '</form>' . PHP_EOL;
}
/**
* Get input field attributes
*
* @param array $entry The current entry
* @return string The input field attributes
*/
private static function getInputAttributes($entry) {
$retVal = '';
@@ -95,9 +153,17 @@ This bridge is not fetching its content through a secure connection</div>';
return $retVal;
}
/**
* Get text input
*
* @param array $entry The current entry
* @param string $id The field ID
* @param string $name The field name
* @return string The text input field
*/
private static function getTextInput($entry, $id, $name) {
return '<input '
. BridgeCard::getInputAttributes($entry)
. self::getInputAttributes($entry)
. ' id="'
. $id
. '" type="text" value="'
@@ -106,13 +172,21 @@ This bridge is not fetching its content through a secure connection</div>';
. filter_var($entry['exampleValue'], FILTER_SANITIZE_STRING)
. '" name="'
. $name
. '" /><br>'
. '" />'
. PHP_EOL;
}
/**
* Get number input
*
* @param array $entry The current entry
* @param string $id The field ID
* @param string $name The field name
* @return string The number input field
*/
private static function getNumberInput($entry, $id, $name) {
return '<input '
. BridgeCard::getInputAttributes($entry)
. self::getInputAttributes($entry)
. ' id="'
. $id
. '" type="number" value="'
@@ -121,13 +195,21 @@ This bridge is not fetching its content through a secure connection</div>';
. filter_var($entry['exampleValue'], FILTER_SANITIZE_NUMBER_INT)
. '" name="'
. $name
. '" /><br>'
. '" />'
. PHP_EOL;
}
/**
* Get list input
*
* @param array $entry The current entry
* @param string $id The field ID
* @param string $name The field name
* @return string The list input field
*/
private static function getListInput($entry, $id, $name) {
$list = '<select '
. BridgeCard::getInputAttributes($entry)
. self::getInputAttributes($entry)
. ' id="'
. $id
. '" name="'
@@ -172,24 +254,40 @@ This bridge is not fetching its content through a secure connection</div>';
}
}
$list .= '</select><br>';
$list .= '</select>';
return $list;
}
/**
* Get checkbox input
*
* @param array $entry The current entry
* @param string $id The field ID
* @param string $name The field name
* @return string The checkbox input field
*/
private static function getCheckboxInput($entry, $id, $name) {
return '<input '
. BridgeCard::getInputAttributes($entry)
. self::getInputAttributes($entry)
. ' id="'
. $id
. '" type="checkbox" name="'
. $name
. '" '
. ($entry['defaultValue'] === 'checked' ?: '')
. ' /><br>'
. ($entry['defaultValue'] === 'checked' ? 'checked' : '')
. ' />'
. PHP_EOL;
}
/**
* Gets a single bridge card
*
* @param string $bridgeName The bridge name
* @param array $formats A list of formats
* @param bool $isActive Indicates if the bridge is active or not
* @return string The bridge card
*/
static function displayBridgeCard($bridgeName, $formats, $isActive = true){
$bridge = Bridge::create($bridgeName);
@@ -232,7 +330,7 @@ CARD;
if(count($parameters) === 0
|| count($parameters) === 1 && array_key_exists('global', $parameters)) {
$card .= BridgeCard::getForm($bridgeName, $formats, $isActive, $isHttps);
$card .= self::getForm($bridgeName, $formats, $isActive, $isHttps);
} else {
@@ -246,7 +344,7 @@ CARD;
if(!is_numeric($parameterName))
$card .= '<h5>' . $parameterName . '</h5>' . PHP_EOL;
$card .= BridgeCard::getForm($bridgeName, $formats, $isActive, $isHttps, $parameterName, $parameter);
$card .= self::getForm($bridgeName, $formats, $isActive, $isHttps, $parameterName, $parameter);
}
}

View File

@@ -1,4 +1,57 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* The bridge interface
*
* A bridge is a class that is responsible for collecting and transforming data
* from one hosting provider into an internal representation of feed data, that
* can later be transformed into different feed formats (see {@see FormatInterface}).
*
* For this purpose, all bridges need to perform three common operations:
*
* 1. Collect data from a remote site.
* 2. Extract the required contents.
* 3. Add the contents to the internal data structure.
*
* Bridges can optionally specify parameters to customize bridge behavior based
* on user input. For example, a user could specify how many items to return in
* the feed and where to get them.
*
* In order to present a bridge on the home page, and for the purpose of bridge
* specific behaviour, additional information must be provided by the bridge:
*
* * **Name**
* The name of the bridge that can be displayed to users.
*
* * **Description**
* A brief description for the bridge that can be displayed to users.
*
* * **URI**
* A link to the hosting provider.
*
* * **Maintainer**
* The GitHub username of the bridge maintainer
*
* * **Parameters**
* A list of parameters for customization
*
* * **Icon**
* A link to the favicon of the hosting provider
*
* * **Cache timeout**
* The default cache timeout for the bridge.
*/
interface BridgeInterface {
/**
@@ -6,13 +59,6 @@ interface BridgeInterface {
*/
public function collectData();
/**
* Returns an array of cachable elements
*
* @return array Associative array of cachable elements
*/
public function getCachable();
/**
* Returns the description
*
@@ -20,13 +66,6 @@ interface BridgeInterface {
*/
public function getDescription();
/**
* Return an array of extra information
*
* @return array Associative array of extra information
*/
public function getExtraInfos();
/**
* Returns an array of collected items
*
@@ -69,26 +108,18 @@ interface BridgeInterface {
*/
public function getURI();
/**
* Sets the cache instance
*
* @param object CacheInterface The cache instance
*/
public function setCache(\CacheInterface $cache);
/**
* Sets the timeout for clearing the cache files. The timeout must be
* specified between 1..86400 seconds (max. 24 hours). The default timeout
* (specified by the bridge maintainer) applies for invalid values.
*
* @param int $timeout The cache timeout in seconds
*/
public function setCacheTimeout($timeout);
/**
* Returns the cache timeout
*
* @return int Cache timeout
*/
public function getCacheTimeout();
/**
* Returns parameters from given URL or null if URL is not applicable
*
* @param string $url URL to extract parameters from
* @return array|null List of bridge parameters or null if detection failed.
*/
public function detectParameters($url);
}

View File

@@ -1,6 +1,31 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* A generator class for the home page of RSS-Bridge.
*
* This class generates the HTML content for displaying all bridges on the home
* page of RSS-Bridge.
*
* @todo Return error if a caller creates an object of this class.
*/
final class BridgeList {
/**
* Get the document head
*
* @return string The document head
*/
private static function getHead() {
return <<<EOD
<head>
@@ -22,20 +47,29 @@ final class BridgeList {
EOD;
}
private static function getBridges($whitelist, $showInactive, &$totalBridges, &$totalActiveBridges) {
/**
* Get the document body for all bridge cards
*
* @param bool $showInactive Inactive bridges are visible on the home page if
* enabled.
* @param int $totalBridges (ref) Returns the total number of bridges.
* @param int $totalActiveBridges (ref) Returns the number of active bridges.
* @return string The document body for all bridge cards.
*/
private static function getBridges($showInactive, &$totalBridges, &$totalActiveBridges) {
$body = '';
$totalActiveBridges = 0;
$inactiveBridges = '';
$bridgeList = Bridge::listBridges();
$formats = Format::searchInformation();
$bridgeList = Bridge::getBridgeNames();
$formats = Format::getFormatNames();
$totalBridges = count($bridgeList);
foreach($bridgeList as $bridgeName) {
if(Bridge::isWhitelisted($whitelist, strtolower($bridgeName))) {
if(Bridge::isWhitelisted($bridgeName)) {
$body .= BridgeCard::displayBridgeCard($bridgeName, $formats);
$totalActiveBridges++;
@@ -54,11 +88,16 @@ EOD;
return $body;
}
/**
* Get the document header
*
* @return string The document header
*/
private static function getHeader() {
$warning = '';
if(defined('DEBUG') && DEBUG === true) {
if(defined('DEBUG_INSECURE') && DEBUG_INSECURE === true) {
if(Debug::isEnabled()) {
if(!Debug::isSecure()) {
$warning .= <<<EOD
<section class="critical-warning">Warning : Debug mode is active from any location,
make sure only you can access RSS-Bridge.</section>
@@ -80,6 +119,11 @@ EOD;
EOD;
}
/**
* Get the searchbar
*
* @return string The searchbar
*/
private static function getSearchbar() {
$query = filter_input(INPUT_GET, 'q');
@@ -93,9 +137,31 @@ EOD;
EOD;
}
/**
* Get the document footer
*
* @param int $totalBridges The total number of bridges, shown in the footer
* @param int $totalActiveBridges The total number of active bridges, shown
* in the footer.
* @param bool $showInactive Sets the 'Show active'/'Show inactive' text in
* the footer.
* @return string The document footer
*/
private static function getFooter($totalBridges, $totalActiveBridges, $showInactive) {
$version = Configuration::getVersion();
$email = Configuration::getConfig('admin', 'email');
$admininfo = '';
if (!empty($email)) {
$admininfo = <<<EOD
<br />
<span>
You may email the administrator of this RSS-Bridge instance
at <a href="mailto:{$email}">{$email}</a>
</span>
EOD;
}
$inactive = '';
if($totalActiveBridges !== $totalBridges) {
@@ -114,11 +180,19 @@ EOD;
<p class="version">{$version}</p>
{$totalActiveBridges}/{$totalBridges} active bridges.<br>
{$inactive}
{$admininfo}
</section>
EOD;
}
static function create($whitelist, $showInactive = true) {
/**
* Create the entire home page
*
* @param bool $showInactive Inactive bridges are displayed on the home page,
* if enabled.
* @return string The home page
*/
static function create($showInactive = true) {
$totalBridges = 0;
$totalActiveBridges = 0;
@@ -128,7 +202,7 @@ EOD;
. '<body onload="search()">'
. BridgeList::getHeader()
. BridgeList::getSearchbar()
. BridgeList::getBridges($whitelist, $showInactive, $totalBridges, $totalActiveBridges)
. BridgeList::getBridges($showInactive, $totalBridges, $totalActiveBridges)
. BridgeList::getFooter($totalBridges, $totalActiveBridges, $showInactive)
. '</body></html>';

View File

@@ -1,53 +1,140 @@
<?php
require_once(__DIR__ . '/CacheInterface.php');
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Factory class responsible for creating cache objects from a given working
* directory.
*
* This class is capable of:
* - Locating cache classes in the specified working directory (see {@see Cache::$workingDir})
* - Creating new cache instances based on the cache's name (see {@see Cache::create()})
*
* The following example illustrates the intended use for this class.
*
* ```PHP
* require_once __DIR__ . '/rssbridge.php';
*
* // Step 1: Set the working directory
* Cache::setWorkingDir(__DIR__ . '/../caches/');
*
* // Step 2: Create a new instance of a cache object (based on the name)
* $cache = Cache::create('FileCache');
* ```
*/
class Cache {
static protected $dirCache;
/**
* 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('Please use ' . __CLASS__ . '::create for new object.');
throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create cache objects!');
}
static public function create($nameCache){
if(!static::isValidNameCache($nameCache)) {
throw new \InvalidArgumentException('Name cache must be at least one
uppercase follow or not by alphanumeric or dash characters.');
/**
* Creates a new cache object from the working directory.
*
* @throws \InvalidArgumentException if the requested cache name is invalid.
* @throws \Exception if the requested cache file doesn't exist in the
* working directory.
* @param string $name Name of the cache object.
* @return object|bool The cache object or false if the class is not instantiable.
*/
public static function create($name){
if(!self::isCacheName($name)) {
throw new \InvalidArgumentException('Cache name invalid!');
}
$pathCache = self::getDir() . $nameCache . '.php';
$filePath = self::getWorkingDir() . $name . '.php';
if(!file_exists($pathCache)) {
throw new \Exception('The cache you looking for does not exist.');
if(!file_exists($filePath)) {
throw new \Exception('Cache file ' . $filePath . ' does not exist!');
}
require_once $pathCache;
require_once $filePath;
return new $nameCache();
if((new \ReflectionClass($name))->isInstantiable()) {
return new $name();
}
static public function setDir($dirCache){
if(!is_string($dirCache)) {
throw new \InvalidArgumentException('Dir cache must be a string.');
return false;
}
if(!file_exists($dirCache)) {
throw new \Exception('Dir cache does not exist.');
/**
* 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!');
}
self::$dirCache = $dirCache;
if(!file_exists($dir)) {
throw new \Exception('Working directory does not exist!');
}
static public function getDir(){
$dirCache = self::$dirCache;
if(is_null($dirCache)) {
throw new \LogicException(__CLASS__ . ' class need to know cache path !');
if(!is_dir($dir)) {
throw new \InvalidArgumentException('Working directory is not a directory!');
}
return $dirCache;
self::$workingDir = realpath($dir) . '/';
}
static public function isValidNameCache($nameCache){
return preg_match('@^[A-Z][a-zA-Z0-9-]*$@', $nameCache);
/**
* 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.
*
* A valid cache name starts with a capital letter ([A-Z]), followed by
* zero or more alphanumeric characters or hyphen ([A-Za-z0-9-]).
*
* @param string $name The cache name.
* @return bool true if the name is a valid cache name, false otherwise.
*/
public static function isCacheName($name){
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
}

View File

@@ -1,7 +1,51 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* The cache interface
*
* @todo Add missing function to the interface
* @todo Explain parameters and return values in more detail
* @todo Return self more often (to allow call chaining)
*/
interface CacheInterface {
/**
* Loads data from cache
*
* @return mixed The cache data
*/
public function loadData();
/**
* Stores data to the cache
*
* @param mixed $datas The data to store
* @return self The cache object
*/
public function saveData($datas);
/**
* Returns the timestamp for the curent cache file
*
* @return int Timestamp
*/
public function getTime();
/**
* Removes any data that is older than the specified duration from cache
*
* @param int $duration The cache duration in seconds
*/
public function purgeCache($duration);
}

View File

@@ -1,15 +1,86 @@
<?php
class Configuration {
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
public static $VERSION = '2018-09-09';
/**
* Configuration module for RSS-Bridge.
*
* This class implements a configuration module for RSS-Bridge.
*/
final class Configuration {
public static $config = null;
/**
* Holds the current release version of RSS-Bridge.
*
* Do not access this property directly!
* Use {@see Configuration::getVersion()} instead.
*
* @var string
*
* @todo Replace this property by a constant.
*/
public static $VERSION = '2018-12-11';
/**
* Holds the configuration data.
*
* Do not access this property directly!
* Use {@see Configuration::getConfig()} instead.
*
* @var array|null
*/
private static $config = null;
/**
* Throw an exception when trying to create a new instance of this class.
*
* @throws \LogicException if called.
*/
public function __construct(){
throw new \LogicException('Can\'t create object of this class!');
}
/**
* Verifies the current installation of RSS-Bridge and PHP.
*
* Returns an error message and aborts execution if the installation does
* not satisfy the requirements of RSS-Bridge.
*
* **Requirements**
* - PHP 5.6.0 or higher
* - `openssl` extension
* - `libxml` extension
* - `mbstring` extension
* - `simplexml` extension
* - `curl` extension
* - `json` extension
* - The cache folder specified by {@see PATH_CACHE} requires write permission
* - The whitelist file specified by {@see WHITELIST} requires write permission
*
* @link http://php.net/supported-versions.php PHP Supported Versions
* @link http://php.net/manual/en/book.openssl.php OpenSSL
* @link http://php.net/manual/en/book.libxml.php libxml
* @link http://php.net/manual/en/book.mbstring.php Multibyte String (mbstring)
* @link http://php.net/manual/en/book.simplexml.php SimpleXML
* @link http://php.net/manual/en/book.curl.php Client URL Library (curl)
* @link http://php.net/manual/en/book.json.php JavaScript Object Notation (json)
*
* @return void
*/
public static function verifyInstallation() {
// Check PHP version
if(version_compare(PHP_VERSION, PHP_VERSION_REQUIRED) === -1)
die('RSS-Bridge requires at least PHP version ' . PHP_VERSION_REQUIRED . '!');
if(version_compare(PHP_VERSION, '5.6.0') === -1)
die('RSS-Bridge requires at least PHP version 5.6.0!');
// extensions check
if(!extension_loaded('openssl'))
@@ -31,27 +102,53 @@ class Configuration {
die('"json" extension not loaded. Please check "php.ini"');
// Check cache folder permissions (write permissions required)
if(!is_writable(CACHE_DIR))
die('RSS-Bridge does not have write permissions for ' . CACHE_DIR . '!');
if(!is_writable(PATH_CACHE))
die('RSS-Bridge does not have write permissions for ' . PATH_CACHE . '!');
// Check whitelist file permissions (only in DEBUG mode)
if(!file_exists(WHITELIST_FILE) && !is_writable(dirname(WHITELIST_FILE)))
die('RSS-Bridge does not have write permissions for ' . WHITELIST_FILE . '!');
// Check whitelist file permissions
if(!file_exists(WHITELIST) && !is_writable(dirname(WHITELIST)))
die('RSS-Bridge does not have write permissions for ' . WHITELIST . '!');
}
/**
* Loads the configuration from disk and checks if the parameters are valid.
*
* Returns an error message and aborts execution if the configuration is invalid.
*
* The RSS-Bridge configuration is split into two files:
* - `config.default.ini.php`: The default configuration file that ships with
* every release of RSS-Bridge (do not modify this file!).
* - `config.ini.php`: The local configuration file that can be modified by
* server administrators.
*
* The files must be located at {@see PATH_ROOT}
*
* 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
* 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).
*
* The configuration files must be placed in the root folder of RSS-Bridge
* (next to `index.php`).
*
* _Notice_: The configuration is stored in {@see Configuration::$config}.
*
* @return void
*/
public static function loadConfiguration() {
if(!file_exists('config.default.ini.php'))
if(!file_exists(PATH_ROOT . 'config.default.ini.php'))
die('The default configuration file "config.default.ini.php" is missing!');
Configuration::$config = parse_ini_file('config.default.ini.php', true, INI_SCANNER_TYPED);
Configuration::$config = parse_ini_file(PATH_ROOT . 'config.default.ini.php', true, INI_SCANNER_TYPED);
if(!Configuration::$config)
die('Error parsing config.default.ini.php');
if(file_exists('config.ini.php')) {
if(file_exists(PATH_ROOT . 'config.ini.php')) {
// Replace default configuration with custom settings
foreach(parse_ini_file('config.ini.php', true, INI_SCANNER_TYPED) as $header => $section) {
foreach(parse_ini_file(PATH_ROOT . 'config.ini.php', 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])) {
@@ -64,22 +161,27 @@ class Configuration {
if(!is_string(self::getConfig('proxy', 'url')))
die('Parameter [proxy] => "url" is not a valid string! Please check "config.ini.php"!');
if(!empty(self::getConfig('proxy', 'url')))
if(!empty(self::getConfig('proxy', 'url'))) {
/** URL of the proxy server */
define('PROXY_URL', self::getConfig('proxy', 'url'));
}
if(!is_bool(self::getConfig('proxy', 'by_bridge')))
die('Parameter [proxy] => "by_bridge" is not a valid Boolean! Please check "config.ini.php"!');
/** 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"!');
/** Name of the proxy server */
define('PROXY_NAME', self::getConfig('proxy', 'name'));
if(!is_bool(self::getConfig('cache', 'custom_timeout')))
die('Parameter [cache] => "custom_timeout" is not a valid Boolean! Please check "config.ini.php"!');
/** 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')))
@@ -91,23 +193,44 @@ class Configuration {
if(!is_string(self::getConfig('authentication', 'password')))
die('Parameter [authentication] => "password" is not a valid string! Please check "config.ini.php"!');
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"!');
}
public static function getConfig($category, $key) {
/**
* Returns the value of a parameter identified by section and key.
*
* @param string $section The section name.
* @param string $key The property name (key).
* @return mixed|null The parameter value.
*/
public static function getConfig($section, $key) {
if(array_key_exists($category, self::$config) && array_key_exists($key, self::$config[$category])) {
return self::$config[$category][$key];
if(array_key_exists($section, self::$config) && array_key_exists($key, self::$config[$section])) {
return self::$config[$section][$key];
}
return null;
}
/**
* Returns the current version string of RSS-Bridge.
*
* This function returns the contents of {@see Configuration::$VERSION} for
* regular installations and the git branch name and commit id for instances
* running in a git environment.
*
* @return string The version string.
*/
public static function getVersion() {
$headFile = '.git/HEAD';
$headFile = PATH_ROOT . '.git/HEAD';
if(file_exists($headFile)) {
// '@' is used to mute open_basedir warning
if(@is_readable($headFile)) {
$revisionHashFile = '.git/' . substr(file_get_contents($headFile), 5, -1);
$branchName = explode('/', $revisionHashFile)[3];

121
lib/Debug.php Normal file
View File

@@ -0,0 +1,121 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Implements functions for debugging purposes. Debugging can be enabled by
* placing a file named DEBUG in {@see PATH_ROOT}.
*
* The file specifies a whitelist of IP addresses on which debug mode will be
* enabled. An empty file enables debug mode for everyone (highly discouraged
* for public servers!). Each line in the file specifies one client in the
* whitelist. For example:
*
* * `192.168.1.72`
* * `127.0.0.1`
* * `::1`
*
* Notice: If you are running RSS-Bridge on your local machine, you need to add
* localhost (either `127.0.0.1` for IPv4 or `::1` for IPv6) to your whitelist!
*
* Warning: In debug mode your server may display sensitive information! For
* security reasons it is recommended to whitelist only specific IP addresses.
*/
class Debug {
/**
* Indicates if debug mode is enabled.
*
* Do not access this property directly!
* Use {@see Debug::isEnabled()} instead.
*
* @var bool
*/
private static $enabled = false;
/**
* Indicates if debug mode is secure.
*
* Do not access this property directly!
* Use {@see Debug::isSecure()} instead.
*
* @var bool
*/
private static $secure = false;
/**
* Returns true if debug mode is enabled
*
* If debug mode is enabled, sets `display_errors = 1` and `error_reporting = E_ALL`
*
* @return bool True if enabled.
*/
public static function isEnabled() {
static $firstCall = true; // Initialized on first call
if($firstCall && file_exists(PATH_ROOT . 'DEBUG')) {
$debug_whitelist = trim(file_get_contents(PATH_ROOT . 'DEBUG'));
self::$enabled = empty($debug_whitelist) || in_array($_SERVER['REMOTE_ADDR'],
explode("\n", str_replace("\r", '', $debug_whitelist)
)
);
if(self::$enabled) {
ini_set('display_errors', '1');
error_reporting(E_ALL);
self::$secure = !empty($debug_whitelist);
}
$firstCall = false; // Skip check on next call
}
return self::$enabled;
}
/**
* Returns true if debug mode is enabled only for specific IP addresses.
*
* Notice: The security flag is set by {@see Debug::isEnabled()}. If this
* function is called before {@see Debug::isEnabled()}, the default value is
* false!
*
* @return bool True if debug mode is secure
*/
public static function isSecure() {
return self::$secure;
}
/**
* Adds a debug message to error_log if debug mode is enabled
*
* @param string $text The message to add to error_log
*/
public static function log($text) {
if(!self::isEnabled()) {
return;
}
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
$calling = end($backtrace);
$message = $calling['file'] . ':'
. $calling['line'] . ' class '
. (isset($calling['class']) ? $calling['class'] : '<no-class>') . '->'
. $calling['function'] . ' - '
. $text;
error_log($message);
}
}

View File

@@ -1,17 +1,28 @@
<?php
class HttpException extends \Exception{}
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Returns an URL that automatically populates a new issue on GitHub based
* on the information provided
*
* @param $title string Sets the title of the issue
* @param $body string Sets the body of the issue (GitHub markdown applies)
* @param $labels mixed (optional) Specifies labels to add to the issue
* @param $maintainer string (optional) Specifies the maintainer for the issue.
* @param string $title string Sets the title of the issue
* @param string $body string Sets the body of the issue (GitHub markdown applies)
* @param string $labels mixed (optional) Specifies labels to add to the issue
* @param string $maintainer string (optional) Specifies the maintainer for the issue.
* The maintainer only applies if part of the development team!
* @return string Returns a qualified URL to a new issue with populated conent.
* Returns null if title or body is null or empty
* @return string|null A qualified URL to a new issue with populated conent or null.
*
* @todo This function belongs inside a class
*/
function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null){
if(!isset($title) || !isset($body) || empty($title) || empty($body)) {
@@ -19,7 +30,8 @@ function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null
}
// Add title and body
$uri = 'https://github.com/rss-bridge/rss-bridge/issues/new?title='
$uri = REPOSITORY
. 'issues/new?title='
. urlencode($title)
. '&body='
. urlencode($body);
@@ -48,10 +60,11 @@ function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null
/**
* Returns the exception message as HTML string
*
* @param $e Exception The exception to show
* @param $bridge object The bridge object
* @return string Returns the exception as HTML string. Returns null if the
* provided parameter are invalid
* @param object $e Exception The exception to show
* @param object $bridge object The bridge object
* @return string|null Returns the exception as HTML string or null.
*
* @todo This function belongs inside a class
*/
function buildBridgeException($e, $bridge){
if(( !($e instanceof \Exception) && !($e instanceof \Error)) || !($bridge instanceof \BridgeInterface)) {
@@ -64,28 +77,33 @@ function buildBridgeException($e, $bridge){
$body = 'Error message: `'
. $e->getMessage()
. "`\nQuery string: `"
. $_SERVER['QUERY_STRING']
. (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '')
. "`\nVersion: `"
. Configuration::getVersion()
. '`';
$body_html = nl2br($body);
$link = buildGitHubIssueQuery($title, $body, 'bug report', $bridge->getMaintainer());
$header = buildHeader($e, $bridge);
$message = "<strong>{$bridge->getName()}</strong> was
unable to receive or process the remote website's content!";
$message = <<<EOD
<strong>{$bridge->getName()}</strong> was unable to receive or process the
remote website's content!<br>
{$body_html}
EOD;
$section = buildSection($e, $bridge, $message, $link);
return buildPage($title, $header, $section);
return $section;
}
/**
* Returns the exception message as HTML string
*
* @param $e Exception The exception to show
* @param $bridge object The bridge object
* @return string Returns the exception as HTML string. Returns null if the
* provided parameter are invalid
* @param object $e Exception The exception to show
* @param object $bridge object The bridge object
* @return string|null Returns the exception as HTML string or null.
*
* @todo This function belongs inside a class
*/
function buildTransformException($e, $bridge){
if(( !($e instanceof \Exception) && !($e instanceof \Error)) || !($bridge instanceof \BridgeInterface)) {
@@ -98,7 +116,8 @@ function buildTransformException($e, $bridge){
$body = 'Error message: `'
. $e->getMessage()
. "`\nQuery string: `"
. $_SERVER['QUERY_STRING'] . '`';
. (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '')
. '`';
$link = buildGitHubIssueQuery($title, $body, 'bug report', $bridge->getMaintainer());
$header = buildHeader($e, $bridge);
@@ -109,6 +128,15 @@ function buildTransformException($e, $bridge){
return buildPage($title, $header, $section);
}
/**
* Builds a new HTML header with data from a exception an a bridge
*
* @param object $e The exception object
* @param object $bridge The bridge object
* @return string The HTML header
*
* @todo This function belongs inside a class
*/
function buildHeader($e, $bridge){
return <<<EOD
<header>
@@ -119,6 +147,17 @@ function buildHeader($e, $bridge){
EOD;
}
/**
* Builds a new HTML section
*
* @param object $e The exception object
* @param object $bridge The bridge object
* @param string $message The message to display
* @param string $link The link to include in the anchor
* @return string The HTML section
*
* @todo This function belongs inside a class
*/
function buildSection($e, $bridge, $message, $link){
return <<<EOD
<section>
@@ -127,7 +166,7 @@ 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>Open a GitHub Issue if this error persists</li>
<li>Open a <a href="{$link}">GitHub Issue</a> if this error persists</li>
</ul>
</div>
<a href="{$link}" title="After clicking this button you can review
@@ -137,6 +176,16 @@ function buildSection($e, $bridge, $message, $link){
EOD;
}
/**
* Builds a new HTML page
*
* @param string $title The HTML title
* @param string $header The HTML header
* @param string $section The HTML section
* @return string The HTML page
*
* @todo This function belongs inside a class
*/
function buildPage($title, $header, $section){
return <<<EOD
<!DOCTYPE html>

View File

@@ -1,17 +1,86 @@
<?php
require_once(__DIR__ . '/BridgeInterface.php');
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* An abstract class for bridges that need to transform existing RSS or Atom
* feeds.
*
* This class extends {@see BridgeAbstract} with functions to extract contents
* from existing RSS or Atom feeds. Bridges that need to transform existing feeds
* should inherit from this class instead of {@see BridgeAbstract}.
*
* Bridges that extend this class don't need to concern themselves with getting
* contents from existing feeds, but can focus on adding additional contents
* (i.e. by downloading additional data), filtering or just transforming a feed
* into another format.
*
* @link http://www.rssboard.org/rss-0-9-1 RSS 0.91 Specification
* @link http://web.resource.org/rss/1.0/spec RDF Site Summary (RSS) 1.0
* @link http://www.rssboard.org/rss-specification RSS 2.0 Specification
* @link https://tools.ietf.org/html/rfc4287 The Atom Syndication Format
*
* @todo The parsing functions should all be private. This class is complicated
* enough without having to consider children overriding functions.
*/
abstract class FeedExpander extends BridgeAbstract {
private $name;
/** Indicates an RSS 1.0 feed */
const FEED_TYPE_RSS_1_0 = 'RSS_1_0';
/** Indicates an RSS 2.0 feed */
const FEED_TYPE_RSS_2_0 = 'RSS_2_0';
/** Indicates an Atom 1.0 feed */
const FEED_TYPE_ATOM_1_0 = 'ATOM_1_0';
/**
* Holds the title of the current feed
*
* @var string
*/
private $title;
/**
* Holds the URI of the feed
*
* @var string
*/
private $uri;
/**
* Holds the feed type during internal operations.
*
* @var string
*/
private $feedType;
/**
* Collects data from an existing feed.
*
* Children should call this function in {@see BridgeInterface::collectData()}
* to extract a feed.
*
* @param string $url URL to the feed.
* @param int $maxItems Maximum number of items to collect from the feed
* (`-1`: no limit).
* @return self
*/
public function collectExpandableDatas($url, $maxItems = -1){
if(empty($url)) {
returnServerError('There is no $url for this RSS expander');
}
debugMessage('Loading from ' . $url);
Debug::log('Loading from ' . $url);
/* Notice we do not use cache here on purpose:
* we want a fresh view of the RSS stream each time
@@ -20,34 +89,49 @@ abstract class FeedExpander extends BridgeAbstract {
or returnServerError('Could not request ' . $url);
$rssContent = simplexml_load_string(trim($content));
debugMessage('Detecting feed format/version');
Debug::log('Detecting feed format/version');
switch(true) {
case isset($rssContent->item[0]):
debugMessage('Detected RSS 1.0 format');
$this->feedType = 'RSS_1_0';
Debug::log('Detected RSS 1.0 format');
$this->feedType = self::FEED_TYPE_RSS_1_0;
break;
case isset($rssContent->channel[0]):
debugMessage('Detected RSS 0.9x or 2.0 format');
$this->feedType = 'RSS_2_0';
Debug::log('Detected RSS 0.9x or 2.0 format');
$this->feedType = self::FEED_TYPE_RSS_2_0;
break;
case isset($rssContent->entry[0]):
debugMessage('Detected ATOM format');
$this->feedType = 'ATOM_1_0';
Debug::log('Detected ATOM format');
$this->feedType = self::FEED_TYPE_ATOM_1_0;
break;
default:
debugMessage('Unknown feed format/version');
Debug::log('Unknown feed format/version');
returnServerError('The feed format is unknown!');
break;
}
debugMessage('Calling function "collect_' . $this->feedType . '_data"');
Debug::log('Calling function "collect_' . $this->feedType . '_data"');
$this->{'collect_' . $this->feedType . '_data'}($rssContent, $maxItems);
return $this;
}
/**
* Collect data from a RSS 1.0 compatible feed
*
* @link http://web.resource.org/rss/1.0/spec RDF Site Summary (RSS) 1.0
*
* @param string $rssContent The RSS content
* @param int $maxItems Maximum number of items to collect from the feed
* (`-1`: no limit).
* @return void
*
* @todo Instead of passing $maxItems to all functions, just add all items
* and remove excessive items later.
*/
protected function collect_RSS_1_0_data($rssContent, $maxItems){
$this->load_RSS_2_0_feed_data($rssContent->channel[0]);
foreach($rssContent->item as $item) {
debugMessage('parsing item ' . var_export($item, true));
Debug::log('parsing item ' . var_export($item, true));
$tmp_item = $this->parseItem($item);
if (!empty($tmp_item)) {
$this->items[] = $tmp_item;
@@ -56,15 +140,28 @@ abstract class FeedExpander extends BridgeAbstract {
}
}
/**
* Collect data from a RSS 2.0 compatible feed
*
* @link http://www.rssboard.org/rss-specification RSS 2.0 Specification
*
* @param object $rssContent The RSS content
* @param int $maxItems Maximum number of items to collect from the feed
* (`-1`: no limit).
* @return void
*
* @todo Instead of passing $maxItems to all functions, just add all items
* and remove excessive items later.
*/
protected function collect_RSS_2_0_data($rssContent, $maxItems){
$rssContent = $rssContent->channel[0];
debugMessage('RSS content is ===========\n'
Debug::log('RSS content is ===========\n'
. var_export($rssContent, true)
. '===========');
$this->load_RSS_2_0_feed_data($rssContent);
foreach($rssContent->item as $item) {
debugMessage('parsing item ' . var_export($item, true));
Debug::log('parsing item ' . var_export($item, true));
$tmp_item = $this->parseItem($item);
if (!empty($tmp_item)) {
$this->items[] = $tmp_item;
@@ -73,10 +170,23 @@ abstract class FeedExpander extends BridgeAbstract {
}
}
/**
* Collect data from a Atom 1.0 compatible feed
*
* @link https://tools.ietf.org/html/rfc4287 The Atom Syndication Format
*
* @param object $content The Atom content
* @param int $maxItems Maximum number of items to collect from the feed
* (`-1`: no limit).
* @return void
*
* @todo Instead of passing $maxItems to all functions, just add all items
* and remove excessive items later.
*/
protected function collect_ATOM_1_0_data($content, $maxItems){
$this->load_ATOM_feed_data($content);
foreach($content->entry as $item) {
debugMessage('parsing item ' . var_export($item, true));
Debug::log('parsing item ' . var_export($item, true));
$tmp_item = $this->parseItem($item);
if (!empty($tmp_item)) {
$this->items[] = $tmp_item;
@@ -85,18 +195,37 @@ abstract class FeedExpander extends BridgeAbstract {
}
}
/**
* Convert RSS 2.0 time to timestamp
*
* @param object $item A feed item
* @return int The timestamp
*/
protected function RSS_2_0_time_to_timestamp($item){
return DateTime::createFromFormat('D, d M Y H:i:s e', $item->pubDate)->getTimestamp();
}
// TODO set title, link, description, language, and so on
/**
* Load RSS 2.0 feed data into RSS-Bridge
*
* @param object $rssContent The RSS content
* @return void
*
* @todo set title, link, description, language, and so on
*/
protected function load_RSS_2_0_feed_data($rssContent){
$this->name = trim((string)$rssContent->title);
$this->title = trim((string)$rssContent->title);
$this->uri = trim((string)$rssContent->link);
}
/**
* Load Atom feed data into RSS-Bridge
*
* @param object $content The Atom content
* @return void
*/
protected function load_ATOM_feed_data($content){
$this->name = (string)$content->title;
$this->title = (string)$content->title;
// Find best link (only one, or first of 'alternate')
if(!isset($content->link)) {
@@ -114,6 +243,16 @@ abstract class FeedExpander extends BridgeAbstract {
}
}
/**
* Parse the contents of a single Atom feed item into a RSS-Bridge item for
* further transformation.
*
* @param object $feedItem A single feed item
* @return object The RSS-Bridge item
*
* @todo To reduce confusion, the RSS-Bridge item should maybe have a class
* of its own?
*/
protected function parseATOMItem($feedItem){
// Some ATOM entries also contain RSS 2.0 fields
$item = $this->parseRSS_2_0_Item($feedItem);
@@ -139,6 +278,16 @@ abstract class FeedExpander extends BridgeAbstract {
return $item;
}
/**
* Parse the contents of a single RSS 0.91 feed item into a RSS-Bridge item
* for further transformation.
*
* @param object $feedItem A single feed item
* @return object The RSS-Bridge item
*
* @todo To reduce confusion, the RSS-Bridge item should maybe have a class
* of its own?
*/
protected function parseRSS_0_9_1_Item($feedItem){
$item = array();
if(isset($feedItem->link)) $item['uri'] = (string)$feedItem->link;
@@ -150,6 +299,16 @@ abstract class FeedExpander extends BridgeAbstract {
return $item;
}
/**
* Parse the contents of a single RSS 1.0 feed item into a RSS-Bridge item
* for further transformation.
*
* @param object $feedItem A single feed item
* @return object The RSS-Bridge item
*
* @todo To reduce confusion, the RSS-Bridge item should maybe have a class
* of its own?
*/
protected function parseRSS_1_0_Item($feedItem){
// 1.0 adds optional elements around the 0.91 standard
$item = $this->parseRSS_0_9_1_Item($feedItem);
@@ -164,6 +323,16 @@ abstract class FeedExpander extends BridgeAbstract {
return $item;
}
/**
* Parse the contents of a single RSS 2.0 feed item into a RSS-Bridge item
* for further transformation.
*
* @param object $feedItem A single feed item
* @return object The RSS-Bridge item
*
* @todo To reduce confusion, the RSS-Bridge item should maybe have a class
* of its own?
*/
protected function parseRSS_2_0_Item($feedItem){
// Primary data is compatible to 0.91 with some additional data
$item = $this->parseRSS_0_9_1_Item($feedItem);
@@ -211,33 +380,38 @@ abstract class FeedExpander extends BridgeAbstract {
}
/**
* Method should return, from a source RSS item given by lastRSS, one of our Items objects
* @param $item the input rss item
* @return a RSS-Bridge Item, with (hopefully) the whole content)
* Parse the contents of a single feed item, depending on the current feed
* type, into a RSS-Bridge item.
*
* @param object $item The current feed item
* @return object A RSS-Bridge item, with (hopefully) the whole content
*/
protected function parseItem($item){
switch($this->feedType) {
case 'RSS_1_0':
case self::FEED_TYPE_RSS_1_0:
return $this->parseRSS_1_0_Item($item);
break;
case 'RSS_2_0':
case self::FEED_TYPE_RSS_2_0:
return $this->parseRSS_2_0_Item($item);
break;
case 'ATOM_1_0':
case self::FEED_TYPE_ATOM_1_0:
return $this->parseATOMItem($item);
break;
default: returnClientError('Unknown version ' . $this->getInput('version') . '!');
}
}
/** {@inheritdoc} */
public function getURI(){
return !empty($this->uri) ? $this->uri : parent::getURI();
}
/** {@inheritdoc} */
public function getName(){
return !empty($this->name) ? $this->name : parent::getName();
return !empty($this->title) ? $this->title : parent::getName();
}
/** {@inheritdoc} */
public function getIcon(){
return !empty($this->icon) ? $this->icon : parent::getIcon();
}

View File

@@ -1,73 +1,166 @@
<?php
require_once(__DIR__ . '/FormatInterface.php');
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Factory class responsible for creating format objects from a given working
* directory.
*
* This class is capable of:
* - Locating format classes in the specified working directory (see {@see Format::$workingDir})
* - Creating new format instances based on the format's name (see {@see Format::create()})
*
* The following example illustrates the intended use for this class.
*
* ```PHP
* require_once __DIR__ . '/rssbridge.php';
*
* // Step 1: Set the working directory
* Format::setWorkingDir(__DIR__ . '/../formats/');
*
* // Step 2: Create a new instance of a format object (based on the name)
* $format = Format::create('Atom');
* ```
*/
class Format {
static protected $dirFormat;
/**
* 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('Please use ' . __CLASS__ . '::create for new object.');
throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create cache objects!');
}
static public function create($nameFormat){
if(!preg_match('@^[A-Z][a-zA-Z]*$@', $nameFormat)) {
throw new \InvalidArgumentException('Name format must be at least
one uppercase follow or not by alphabetic characters.');
/**
* Creates a new format object from the working directory.
*
* @throws \InvalidArgumentException if the requested format name is invalid.
* @throws \Exception if the requested format file doesn't exist in the
* working directory.
* @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)) {
throw new \InvalidArgumentException('Format name invalid!');
}
$nameFormat = $nameFormat . 'Format';
$pathFormat = self::getDir() . $nameFormat . '.php';
$name = $name . 'Format';
$pathFormat = self::getWorkingDir() . $name . '.php';
if(!file_exists($pathFormat)) {
throw new \Exception('The format you looking for does not exist.');
throw new \Exception('Format file ' . $filePath . ' does not exist!');
}
require_once $pathFormat;
return new $nameFormat();
if((new \ReflectionClass($name))->isInstantiable()) {
return new $name();
}
static public function setDir($dirFormat){
if(!is_string($dirFormat)) {
throw new \InvalidArgumentException('Dir format must be a string.');
}
if(!file_exists($dirFormat)) {
throw new \Exception('Dir format does not exist.');
}
self::$dirFormat = $dirFormat;
}
static public function getDir(){
$dirFormat = self::$dirFormat;
if(is_null($dirFormat)) {
throw new \LogicException(__CLASS__ . ' class need to know format path !');
}
return $dirFormat;
return false;
}
/**
* Read format dir and catch informations about each format depending annotation
* @return array Informations about each format
* Sets the working directory.
*
* @param string $dir Path to a directory containing cache classes
* @throws \InvalidArgumentException if $dir is not a string.
* @throws \Exception if the working directory doesn't exist.
* @throws \InvalidArgumentException if $dir is not a directory.
* @return void
*/
static public function searchInformation(){
$pathDirFormat = self::getDir();
public static function setWorkingDir($dir){
self::$workingDir = null;
$listFormat = array();
if(!is_string($dir)) {
throw new \InvalidArgumentException('Dir format must be a string.');
}
$searchCommonPattern = array('name');
if(!file_exists($dir)) {
throw new \Exception('Working directory does not exist!');
}
$dirFiles = scandir($pathDirFormat);
if($dirFiles !== false) {
foreach($dirFiles as $fileName) {
if(preg_match('@^([^.]+)Format\.php$@U', $fileName, $out)) { // Is PHP file ?
$listFormat[] = $out[1];
if(!is_dir($dir)) {
throw new \InvalidArgumentException('Working directory is not a directory!');
}
self::$workingDir = realpath($dir) . '/';
}
/**
* Returns the working directory.
* The working directory must be set with {@see Format::setWorkingDir()}!
*
* @throws \LogicException if the working directory is not set.
* @return string The current working directory.
*/
public static function getWorkingDir(){
if(is_null(self::$workingDir)) {
throw new \LogicException('Working directory is not set!');
}
return self::$workingDir;
}
/**
* Returns true if the provided name is a valid format name.
*
* A valid format name starts with a capital letter ([A-Z]), followed by
* zero or more alphanumeric characters or hyphen ([A-Za-z0-9-]).
*
* @param string $name The format name.
* @return bool true if the name is a valid format name, false otherwise.
*/
public static function isFormatName($name){
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
/**
* Returns the list of format names from the working directory.
*
* The list is cached internally to allow for successive calls.
*
* @return array List of format names
*/
public static function getFormatNames(){
static $formatNames = array(); // Initialized on first call
if(empty($formatNames)) {
$files = scandir(self::getWorkingDir());
if($files !== false) {
foreach($files as $file) {
if(preg_match('/^([^.]+)Format\.php$/U', $file, $out)) {
$formatNames[] = $out[1];
}
}
}
}
return $listFormat;
return $formatNames;
}
}

View File

@@ -1,41 +1,103 @@
<?php
require_once(__DIR__ . '/FormatInterface.php');
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license https://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* An abstract class for format implementations
*
* This class implements {@see FormatInterface}
*/
abstract class FormatAbstract implements FormatInterface {
/** The default charset (UTF-8) */
const DEFAULT_CHARSET = 'UTF-8';
protected
$contentType,
$charset,
$items,
$lastModified,
$extraInfos;
/** @var string|null $contentType The content type */
protected $contentType = null;
/** @var string $charset The charset */
protected $charset;
/** @var array $items The items */
protected $items;
/**
* @var int $lastModified A timestamp to indicate the last modified time of
* the output data.
*/
protected $lastModified;
/** @var array $extraInfos The extra infos */
protected $extraInfos;
/**
* {@inheritdoc}
*
* @param string $charset {@inheritdoc}
*/
public function setCharset($charset){
$this->charset = $charset;
return $this;
}
/** {@inheritdoc} */
public function getCharset(){
$charset = $this->charset;
return is_null($charset) ? static::DEFAULT_CHARSET : $charset;
}
/**
* Set the content type
*
* @param string $contentType The content type
* @return self The format object
*/
protected function setContentType($contentType){
$this->contentType = $contentType;
return $this;
}
/**
* Set the last modified time
*
* @param int $lastModified The last modified time
* @return void
*/
public function setLastModified($lastModified){
$this->lastModified = $lastModified;
}
/**
* Send header with the currently specified content type
*
* @throws \LogicException if the content type is not set
* @throws \LogicException if the content type is not a string
*
* @return void
*/
protected function callContentType(){
if(empty($this->contentType))
throw new \LogicException('Content-Type is not set!');
if(!is_string($this->contentType))
throw new \LogicException('Content-Type must be a string!');
header('Content-Type: ' . $this->contentType);
}
/** {@inheritdoc} */
public function display(){
if ($this->lastModified) {
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $this->lastModified) . 'GMT');
@@ -45,12 +107,18 @@ abstract class FormatAbstract implements FormatInterface {
return $this;
}
/**
* {@inheritdoc}
*
* @param array $items {@inheritdoc}
*/
public function setItems(array $items){
$this->items = array_map(array($this, 'array_trim'), $items);
return $this;
}
/** {@inheritdoc} */
public function getItems(){
if(!is_array($this->items))
throw new \LogicException('Feed the ' . get_class($this) . ' with "setItems" method before !');
@@ -59,9 +127,9 @@ abstract class FormatAbstract implements FormatInterface {
}
/**
* Define common informations can be required by formats and set default value for unknown values
* @param array $extraInfos array with know informations (there isn't merge !!!)
* @return this
* {@inheritdoc}
*
* @param array $extraInfos {@inheritdoc}
*/
public function setExtraInfos(array $extraInfos = array()){
foreach(array('name', 'uri', 'icon') as $infoName) {
@@ -75,10 +143,7 @@ abstract class FormatAbstract implements FormatInterface {
return $this;
}
/**
* Return extra infos
* @return array See "setExtraInfos" detail method to know what extra are disponibles
*/
/** {@inheritdoc} */
public function getExtraInfos(){
if(is_null($this->extraInfos)) { // No extra info ?
$this->setExtraInfos(); // Define with default value
@@ -88,12 +153,17 @@ abstract class FormatAbstract implements FormatInterface {
}
/**
* Sanitized html while leaving it functionnal.
* The aim is to keep html as-is (with clickable hyperlinks)
* while reducing annoying and potentially dangerous things.
* Yes, I know sanitizing HTML 100% is an impossible task.
* Maybe we'll switch to http://htmlpurifier.org/
* or http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/index.php
* Sanitize HTML while leaving it functional.
*
* Keeps HTML as-is (with clickable hyperlinks) while reducing annoying and
* potentially dangerous things.
*
* @param string $html The HTML content
* @return string The sanitized HTML content
*
* @todo This belongs into `html.php`
* @todo Maybe switch to http://htmlpurifier.org/
* @todo Maybe switch to http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/index.php
*/
protected function sanitizeHtml($html)
{
@@ -104,6 +174,17 @@ abstract class FormatAbstract implements FormatInterface {
return $html;
}
/**
* Trim each element of an array
*
* This function applies `trim()` to all elements in the array, if the element
* is a valid string.
*
* @param array $elements The array to trim
* @return array The trimmed array
*
* @todo This is a utility function that doesn't belong here, find a new home.
*/
protected function array_trim($elements){
foreach($elements as $key => $value) {
if(is_string($value))

View File

@@ -1,11 +1,84 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* The format interface
*
* @todo Add missing function to the interface
* @todo Explain parameters and return values in more detail
* @todo Return self more often (to allow call chaining)
*/
interface FormatInterface {
/**
* Generate a string representation of the current data
*
* @return string The string representation
*/
public function stringify();
/**
* Display the current data to the user
*
* @return self The format object
*/
public function display();
/**
* Set items
*
* @param array $bridges The items
* @return self The format object
*
* @todo Rename parameter `$bridges` to `$items`
*/
public function setItems(array $bridges);
/**
* Return items
*
* @throws \LogicException if the items are not set
* @return array The items
*/
public function getItems();
/**
* Set extra information
*
* @param array $infos Extra information
* @return self The format object
*/
public function setExtraInfos(array $infos);
/**
* Return extra information
*
* @return array Extra information
*/
public function getExtraInfos();
/**
* Set charset
*
* @param string $charset The charset
* @return self The format object
*/
public function setCharset($charset);
/**
* Return current charset
*
* @return string The charset
*/
public function getCharset();
}

227
lib/ParameterValidator.php Normal file
View File

@@ -0,0 +1,227 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Validator for bridge parameters
*/
class ParameterValidator {
/**
* Holds the list of invalid parameters
*
* @var array
*/
private $invalid = array();
/**
* Add item to list of invalid parameters
*
* @param string $name The name of the parameter
* @param string $reason The reason for that parameter being invalid
* @return void
*/
private function addInvalidParameter($name, $reason){
$this->invalid[] = array(
'name' => $name,
'reason' => $reason
);
}
/**
* Return list of invalid parameters.
*
* Each element is an array of 'name' and 'reason'.
*
* @return array List of invalid parameters
*/
public function getInvalidParameters() {
return $this->invalid;
}
/**
* Validate value for a text input
*
* @param string $value The value of a text input
* @param string|null $pattern (optional) A regex pattern
* @return string|null The filtered value or null if the value is invalid
*/
private function validateTextValue($value, $pattern = null){
if(!is_null($pattern)) {
$filteredValue = filter_var($value,
FILTER_VALIDATE_REGEXP,
array('options' => array(
'regexp' => '/^' . $pattern . '$/'
)
));
} else {
$filteredValue = filter_var($value);
}
if($filteredValue === false)
return null;
return $filteredValue;
}
/**
* Validate value for a number input
*
* @param int $value The value of a number input
* @return int|null The filtered value or null if the value is invalid
*/
private function validateNumberValue($value){
$filteredValue = filter_var($value, FILTER_VALIDATE_INT);
if($filteredValue === false)
return null;
return $filteredValue;
}
/**
* Validate value for a checkbox
*
* @param bool $value The value of a checkbox
* @return bool The filtered value
*/
private function validateCheckboxValue($value){
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
/**
* Validate value for a list
*
* @param string $value The value of a list
* @param array $expectedValues A list of expected values
* @return string|null The filtered value or null if the value is invalid
*/
private function validateListValue($value, $expectedValues){
$filteredValue = filter_var($value);
if($filteredValue === false)
return null;
if(!in_array($filteredValue, $expectedValues)) { // Check sub-values?
foreach($expectedValues as $subName => $subValue) {
if(is_array($subValue) && in_array($filteredValue, $subValue))
return $filteredValue;
}
return null;
}
return $filteredValue;
}
/**
* Check if all required parameters are satisfied
*
* @param array $data (ref) A list of input values
* @param array $parameters The bridge parameters
* @return bool True if all parameters are satisfied
*/
public function validateData(&$data, $parameters){
if(!is_array($data))
return false;
foreach($data as $name => $value) {
$registered = false;
foreach($parameters as $context => $set) {
if(array_key_exists($name, $set)) {
$registered = true;
if(!isset($set[$name]['type'])) {
$set[$name]['type'] = 'text';
}
switch($set[$name]['type']) {
case 'number':
$data[$name] = $this->validateNumberValue($value);
break;
case 'checkbox':
$data[$name] = $this->validateCheckboxValue($value);
break;
case 'list':
$data[$name] = $this->validateListValue($value, $set[$name]['values']);
break;
default:
case 'text':
if(isset($set[$name]['pattern'])) {
$data[$name] = $this->validateTextValue($value, $set[$name]['pattern']);
} else {
$data[$name] = $this->validateTextValue($value);
}
break;
}
if(is_null($data[$name]) && isset($set[$name]['required']) && $set[$name]['required']) {
$this->addInvalidParameter($name, 'Parameter is invalid!');
}
}
}
if(!$registered) {
$this->addInvalidParameter($name, 'Parameter is not registered!');
}
}
return empty($this->invalid);
}
/**
* Get the name of the context matching the provided inputs
*
* @param array $data Associative array of user data
* @param array $parameters Array of bridge parameters
* @return string|null Returns the context name or null if no match was found
*/
public function getQueriedContext($data, $parameters){
$queriedContexts = array();
// Detect matching context
foreach($parameters as $context => $set) {
$queriedContexts[$context] = null;
// Check if all parameters of the context are satisfied
foreach($set as $id => $properties) {
if(isset($data[$id]) && !empty($data[$id])) {
$queriedContexts[$context] = true;
} elseif(isset($properties['required'])
&& $properties['required'] === true) {
$queriedContexts[$context] = false;
break;
}
}
}
// Abort if one of the globally required parameters is not satisfied
if(array_key_exists('global', $parameters)
&& $queriedContexts['global'] === false) {
return null;
}
unset($queriedContexts['global']);
switch(array_sum($queriedContexts)) {
case 0: // Found no match, is there a context without parameters?
foreach($queriedContexts as $context => $queried) {
if(is_null($queried)) {
return $context;
}
}
return null;
case 1: // Found unique match
return array_search(true, $queriedContexts);
default: return false;
}
}
}

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