Merge remote-tracking branch 'origin/master' into kt_bridge
This commit is contained in:
commit
d50f246dec
100 changed files with 3342 additions and 626 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -236,3 +236,7 @@ config.ini.php
|
||||||
|
|
||||||
#Builder
|
#Builder
|
||||||
.buildconfig
|
.buildconfig
|
||||||
|
|
||||||
|
#Auth
|
||||||
|
.htaccess
|
||||||
|
.htpasswd
|
||||||
|
|
42
.travis.yml
42
.travis.yml
|
@ -4,41 +4,43 @@ language: php
|
||||||
install:
|
install:
|
||||||
- composer global require dealerdirect/phpcodesniffer-composer-installer;
|
- composer global require dealerdirect/phpcodesniffer-composer-installer;
|
||||||
- composer global require phpcompatibility/php-compatibility;
|
- composer global require phpcompatibility/php-compatibility;
|
||||||
# Use PHPUnit 6 for unit tests (stable), requires PHP 7
|
- if [[ "$PHPUNIT" ]]; then
|
||||||
- if [[ $TRAVIS_PHP_VERSION == "7.0" ]]; then
|
composer global require phpunit/phpunit ^$PHPUNIT;
|
||||||
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
|
fi
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- phpenv rehash
|
- phpenv rehash
|
||||||
# Run PHP_CodeSniffer on all versions
|
# Run PHP_CodeSniffer on all versions
|
||||||
- ~/.config/composer/vendor/bin/phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
|
- ~/.config/composer/vendor/bin/phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
|
||||||
# Check PHP compatibility for the lowest supported version
|
# Check PHP compatibility for the lowest and highest supported version
|
||||||
- if [[ $TRAVIS_PHP_VERSION == "5.6" ]]; then
|
- if [[ $TRAVIS_PHP_VERSION == "5.6" || $TRAVIS_PHP_VERSION == "7.3" ]]; then
|
||||||
~/.config/composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --extensions=php -p;
|
~/.config/composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --extensions=php -p;
|
||||||
fi
|
fi
|
||||||
# Run unit tests (stable)
|
# Run unit tests on highest major version
|
||||||
- if [[ $TRAVIS_PHP_VERSION == "7.0" ]]; then
|
- if [[ ${TRAVIS_PHP_VERSION:0:1} == "7" ]]; then
|
||||||
phpunit --configuration=phpunit.xml --include-path=lib/;
|
~/.config/composer/vendor/bin/phpunit --configuration=phpunit.xml --include-path=lib/;
|
||||||
fi
|
|
||||||
# Run unit tests (latest/nightly)
|
|
||||||
# Check PHP compatibility for all versions, starting at the lowest supported version in order to detect breaking changes
|
|
||||||
- if [[ $TRAVIS_PHP_VERSION == "nightly" ]]; then
|
|
||||||
phpunit --configuration=phpunit.xml --include-path=lib/;
|
|
||||||
~/.config/composer/vendor/bin/phpcs . --standard=PHPCompatibility --extensions=php -p --runtime-set testVersion 5.6-;
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
php:
|
||||||
|
- 7.3
|
||||||
|
|
||||||
|
env:
|
||||||
|
- PHPUNIT=6
|
||||||
|
- PHPUNIT=7
|
||||||
|
- PHPUNIT=8
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- php: 5.6
|
- php: 5.6
|
||||||
|
env: PHPUNIT=
|
||||||
- php: 7.0
|
- php: 7.0
|
||||||
- php: nightly
|
- php: 7.1
|
||||||
|
- php: 7.2
|
||||||
|
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- php: nightly
|
- php: 7.3
|
||||||
|
env: PHPUNIT=7
|
||||||
|
- php: 7.3
|
||||||
|
env: PHPUNIT=8
|
||||||
|
|
64
README.md
64
README.md
|
@ -66,6 +66,7 @@ RSS-Bridge requires PHP 5.6 or higher with following extensions enabled:
|
||||||
- [`simplexml`](https://secure.php.net/manual/en/book.simplexml.php)
|
- [`simplexml`](https://secure.php.net/manual/en/book.simplexml.php)
|
||||||
- [`curl`](https://secure.php.net/manual/en/book.curl.php)
|
- [`curl`](https://secure.php.net/manual/en/book.curl.php)
|
||||||
- [`json`](https://secure.php.net/manual/en/book.json.php)
|
- [`json`](https://secure.php.net/manual/en/book.json.php)
|
||||||
|
- [`sqlite3`](http://php.net/manual/en/book.sqlite3.php) (only when using SQLiteCache)
|
||||||
|
|
||||||
Find more information on our [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
|
Find more information on our [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
|
||||||
|
|
||||||
|
@ -111,41 +112,16 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
|
||||||
-->
|
-->
|
||||||
|
|
||||||
* [16mhz](https://github.com/16mhz)
|
* [16mhz](https://github.com/16mhz)
|
||||||
|
* [adamchainz](https://github.com/adamchainz)
|
||||||
* [Ahiles3005](https://github.com/Ahiles3005)
|
* [Ahiles3005](https://github.com/Ahiles3005)
|
||||||
* [Albirew](https://github.com/Albirew)
|
* [Albirew](https://github.com/Albirew)
|
||||||
|
* [aledeg](https://github.com/aledeg)
|
||||||
|
* [alexAubin](https://github.com/alexAubin)
|
||||||
* [AmauryCarrade](https://github.com/AmauryCarrade)
|
* [AmauryCarrade](https://github.com/AmauryCarrade)
|
||||||
* [AntoineTurmel](https://github.com/AntoineTurmel)
|
* [AntoineTurmel](https://github.com/AntoineTurmel)
|
||||||
* [ArthurHoaro](https://github.com/ArthurHoaro)
|
* [ArthurHoaro](https://github.com/ArthurHoaro)
|
||||||
* [Astalaseven](https://github.com/Astalaseven)
|
* [Astalaseven](https://github.com/Astalaseven)
|
||||||
* [Astyan-42](https://github.com/Astyan-42)
|
* [Astyan-42](https://github.com/Astyan-42)
|
||||||
* [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)
|
|
||||||
* [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)
|
* [az5he6ch](https://github.com/az5he6ch)
|
||||||
* [b1nj](https://github.com/b1nj)
|
* [b1nj](https://github.com/b1nj)
|
||||||
* [benasse](https://github.com/benasse)
|
* [benasse](https://github.com/benasse)
|
||||||
|
@ -156,21 +132,37 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
|
||||||
* [corenting](https://github.com/corenting)
|
* [corenting](https://github.com/corenting)
|
||||||
* [couraudt](https://github.com/couraudt)
|
* [couraudt](https://github.com/couraudt)
|
||||||
* [da2x](https://github.com/da2x)
|
* [da2x](https://github.com/da2x)
|
||||||
|
* [Daiyousei](https://github.com/Daiyousei)
|
||||||
* [disk0x](https://github.com/disk0x)
|
* [disk0x](https://github.com/disk0x)
|
||||||
* [eMerzh](https://github.com/eMerzh)
|
* [Djuuu](https://github.com/Djuuu)
|
||||||
|
* [Draeli](https://github.com/Draeli)
|
||||||
* [em92](https://github.com/em92)
|
* [em92](https://github.com/em92)
|
||||||
|
* [eMerzh](https://github.com/eMerzh)
|
||||||
|
* [EtienneM](https://github.com/EtienneM)
|
||||||
* [fluffy-critter](https://github.com/fluffy-critter)
|
* [fluffy-critter](https://github.com/fluffy-critter)
|
||||||
|
* [Frenzie](https://github.com/Frenzie)
|
||||||
* [fulmeek](https://github.com/fulmeek)
|
* [fulmeek](https://github.com/fulmeek)
|
||||||
|
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
|
||||||
|
* [Glandos](https://github.com/Glandos)
|
||||||
|
* [GregThib](https://github.com/GregThib)
|
||||||
* [griffaurel](https://github.com/griffaurel)
|
* [griffaurel](https://github.com/griffaurel)
|
||||||
|
* [Grummfy](https://github.com/Grummfy)
|
||||||
* [hunhejj](https://github.com/hunhejj)
|
* [hunhejj](https://github.com/hunhejj)
|
||||||
* [j0k3r](https://github.com/j0k3r)
|
* [j0k3r](https://github.com/j0k3r)
|
||||||
|
* [JackNUMBER](https://github.com/JackNUMBER)
|
||||||
* [jdigilio](https://github.com/jdigilio)
|
* [jdigilio](https://github.com/jdigilio)
|
||||||
|
* [JeremyRand](https://github.com/JeremyRand)
|
||||||
|
* [Jocker666z](https://github.com/Jocker666z)
|
||||||
|
* [klimplant](https://github.com/klimplant)
|
||||||
* [kranack](https://github.com/kranack)
|
* [kranack](https://github.com/kranack)
|
||||||
* [kraoc](https://github.com/kraoc)
|
* [kraoc](https://github.com/kraoc)
|
||||||
|
* [l1n](https://github.com/l1n)
|
||||||
* [laBecasse](https://github.com/laBecasse)
|
* [laBecasse](https://github.com/laBecasse)
|
||||||
* [lagaisse](https://github.com/lagaisse)
|
* [lagaisse](https://github.com/lagaisse)
|
||||||
* [lalannev](https://github.com/lalannev)
|
* [lalannev](https://github.com/lalannev)
|
||||||
* [ldidry](https://github.com/ldidry)
|
* [ldidry](https://github.com/ldidry)
|
||||||
|
* [Limero](https://github.com/Limero)
|
||||||
|
* [LogMANOriginal](https://github.com/LogMANOriginal)
|
||||||
* [lorenzos](https://github.com/lorenzos)
|
* [lorenzos](https://github.com/lorenzos)
|
||||||
* [m0zes](https://github.com/m0zes)
|
* [m0zes](https://github.com/m0zes)
|
||||||
* [matthewseal](https://github.com/matthewseal)
|
* [matthewseal](https://github.com/matthewseal)
|
||||||
|
@ -180,28 +172,40 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
|
||||||
* [metaMMA](https://github.com/metaMMA)
|
* [metaMMA](https://github.com/metaMMA)
|
||||||
* [mickael-bertrand](https://github.com/mickael-bertrand)
|
* [mickael-bertrand](https://github.com/mickael-bertrand)
|
||||||
* [mitsukarenai](https://github.com/mitsukarenai)
|
* [mitsukarenai](https://github.com/mitsukarenai)
|
||||||
|
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
|
||||||
* [mr-flibble](https://github.com/mr-flibble)
|
* [mr-flibble](https://github.com/mr-flibble)
|
||||||
* [mro](https://github.com/mro)
|
* [mro](https://github.com/mro)
|
||||||
* [mxmehl](https://github.com/mxmehl)
|
* [mxmehl](https://github.com/mxmehl)
|
||||||
* [nel50n](https://github.com/nel50n)
|
* [nel50n](https://github.com/nel50n)
|
||||||
* [niawag](https://github.com/niawag)
|
* [niawag](https://github.com/niawag)
|
||||||
|
* [Nono-m0le](https://github.com/Nono-m0le)
|
||||||
|
* [ORelio](https://github.com/ORelio)
|
||||||
|
* [PaulVayssiere](https://github.com/PaulVayssiere)
|
||||||
* [pellaeon](https://github.com/pellaeon)
|
* [pellaeon](https://github.com/pellaeon)
|
||||||
|
* [Piranhaplant](https://github.com/Piranhaplant)
|
||||||
* [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
|
* [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
|
||||||
* [pitchoule](https://github.com/pitchoule)
|
* [pitchoule](https://github.com/pitchoule)
|
||||||
* [pmaziere](https://github.com/pmaziere)
|
* [pmaziere](https://github.com/pmaziere)
|
||||||
* [prysme01](https://github.com/prysme01)
|
* [prysme01](https://github.com/prysme01)
|
||||||
* [quentinus95](https://github.com/quentinus95)
|
* [quentinus95](https://github.com/quentinus95)
|
||||||
* [qwertygc](https://github.com/qwertygc)
|
|
||||||
* [regisenguehard](https://github.com/regisenguehard)
|
* [regisenguehard](https://github.com/regisenguehard)
|
||||||
|
* [Riduidel](https://github.com/Riduidel)
|
||||||
* [rogerdc](https://github.com/rogerdc)
|
* [rogerdc](https://github.com/rogerdc)
|
||||||
|
* [Roliga](https://github.com/Roliga)
|
||||||
* [sebsauvage](https://github.com/sebsauvage)
|
* [sebsauvage](https://github.com/sebsauvage)
|
||||||
|
* [somini](https://github.com/somini)
|
||||||
|
* [squeek502](https://github.com/squeek502)
|
||||||
|
* [Strubbl](https://github.com/Strubbl)
|
||||||
* [sublimz](https://github.com/sublimz)
|
* [sublimz](https://github.com/sublimz)
|
||||||
* [sysadminstory](https://github.com/sysadminstory)
|
* [sysadminstory](https://github.com/sysadminstory)
|
||||||
* [tameroski](https://github.com/tameroski)
|
* [tameroski](https://github.com/tameroski)
|
||||||
* [teromene](https://github.com/teromene)
|
* [teromene](https://github.com/teromene)
|
||||||
|
* [TheRadialActive](https://github.com/TheRadialActive)
|
||||||
* [triatic](https://github.com/triatic)
|
* [triatic](https://github.com/triatic)
|
||||||
|
* [WalterBarrett](https://github.com/WalterBarrett)
|
||||||
* [wtuuju](https://github.com/wtuuju)
|
* [wtuuju](https://github.com/wtuuju)
|
||||||
* [yardenac](https://github.com/yardenac)
|
* [yardenac](https://github.com/yardenac)
|
||||||
|
* [ZeNairolf](https://github.com/ZeNairolf)
|
||||||
|
|
||||||
Licenses
|
Licenses
|
||||||
===
|
===
|
||||||
|
|
50
actions/DetectAction.php
Normal file
50
actions/DetectAction.php
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<?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
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DetectAction extends ActionAbstract {
|
||||||
|
public function execute() {
|
||||||
|
$targetURL = $this->userData['url']
|
||||||
|
or returnClientError('You must specify a url!');
|
||||||
|
|
||||||
|
$format = $this->userData['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);
|
||||||
|
}
|
||||||
|
}
|
234
actions/DisplayAction.php
Normal file
234
actions/DisplayAction.php
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
<?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
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DisplayAction extends ActionAbstract {
|
||||||
|
public function execute() {
|
||||||
|
$bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null;
|
||||||
|
|
||||||
|
$format = $this->userData['format']
|
||||||
|
or returnClientError('You must specify a format!');
|
||||||
|
|
||||||
|
// DEPRECATED: 'nameFormat' scheme is replaced by 'name' in format parameter values
|
||||||
|
// this is to keep compatibility until futher complete removal
|
||||||
|
if(($pos = strpos($format, 'Format')) === (strlen($format) - strlen('Format'))) {
|
||||||
|
$format = substr($format, 0, $pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// whitelist control
|
||||||
|
if(!Bridge::isWhitelisted($bridge)) {
|
||||||
|
throw new \Exception('This bridge is not whitelisted', 401);
|
||||||
|
die;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data retrieval
|
||||||
|
$bridge = Bridge::create($bridge);
|
||||||
|
|
||||||
|
$noproxy = array_key_exists('_noproxy', $this->userData)
|
||||||
|
&& filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN);
|
||||||
|
|
||||||
|
if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
|
||||||
|
define('NOPROXY', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache timeout
|
||||||
|
$cache_timeout = -1;
|
||||||
|
if(array_key_exists('_cache_timeout', $this->userData)) {
|
||||||
|
|
||||||
|
if(!CUSTOM_CACHE_TIMEOUT) {
|
||||||
|
unset($this->userData['_cache_timeout']);
|
||||||
|
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($this->userData);
|
||||||
|
header('Location: ' . $uri, true, 301);
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache_timeout = filter_var($this->userData['_cache_timeout'], FILTER_VALIDATE_INT);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$cache_timeout = $bridge->getCacheTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove parameters that don't concern bridges
|
||||||
|
$bridge_params = array_diff_key(
|
||||||
|
$this->userData,
|
||||||
|
array_fill_keys(
|
||||||
|
array(
|
||||||
|
'action',
|
||||||
|
'bridge',
|
||||||
|
'format',
|
||||||
|
'_noproxy',
|
||||||
|
'_cache_timeout',
|
||||||
|
'_error_time'
|
||||||
|
), '')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove parameters that don't concern caches
|
||||||
|
$cache_params = array_diff_key(
|
||||||
|
$this->userData,
|
||||||
|
array_fill_keys(
|
||||||
|
array(
|
||||||
|
'action',
|
||||||
|
'format',
|
||||||
|
'_noproxy',
|
||||||
|
'_cache_timeout',
|
||||||
|
'_error_time'
|
||||||
|
), '')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize cache
|
||||||
|
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
|
||||||
|
$cache->setPath(PATH_CACHE);
|
||||||
|
$cache->purgeCache(86400); // 24 hours
|
||||||
|
$cache->setParameters($cache_params);
|
||||||
|
|
||||||
|
$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'])) {
|
||||||
|
foreach($cached['items'] as $item) {
|
||||||
|
$items[] = new \FeedItem($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
$infos = $cached['extraInfos'];
|
||||||
|
}
|
||||||
|
|
||||||
|
} else { // Collect new data
|
||||||
|
|
||||||
|
try {
|
||||||
|
$bridge->setDatas($bridge_params);
|
||||||
|
$bridge->collectData();
|
||||||
|
|
||||||
|
$items = $bridge->getItems();
|
||||||
|
|
||||||
|
// Transform "legacy" items to FeedItems if necessary.
|
||||||
|
// Remove this code when support for "legacy" items ends!
|
||||||
|
if(isset($items[0]) && is_array($items[0])) {
|
||||||
|
$feedItems = array();
|
||||||
|
|
||||||
|
foreach($items as $item) {
|
||||||
|
$feedItems[] = new \FeedItem($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $feedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
$infos = array(
|
||||||
|
'name' => $bridge->getName(),
|
||||||
|
'uri' => $bridge->getURI(),
|
||||||
|
'icon' => $bridge->getIcon()
|
||||||
|
);
|
||||||
|
} catch(Error $e) {
|
||||||
|
error_log($e);
|
||||||
|
|
||||||
|
$item = new \FeedItem();
|
||||||
|
|
||||||
|
// Create "new" error message every 24 hours
|
||||||
|
$this->userData['_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->setTitle(
|
||||||
|
'Bridge encountered an unexpected situation! ('
|
||||||
|
. $this->userData['_error_time']
|
||||||
|
. ')'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$item->setTitle(
|
||||||
|
'Bridge returned error '
|
||||||
|
. $e->getCode()
|
||||||
|
. '! ('
|
||||||
|
. $this->userData['_error_time']
|
||||||
|
. ')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$item->setURI(
|
||||||
|
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
|
||||||
|
. '?'
|
||||||
|
. http_build_query($this->userData)
|
||||||
|
);
|
||||||
|
|
||||||
|
$item->setTimestamp(time());
|
||||||
|
$item->setContent(buildBridgeException($e, $bridge));
|
||||||
|
|
||||||
|
$items[] = $item;
|
||||||
|
} catch(Exception $e) {
|
||||||
|
error_log($e);
|
||||||
|
|
||||||
|
$item = new \FeedItem();
|
||||||
|
|
||||||
|
// Create "new" error message every 24 hours
|
||||||
|
$this->userData['_error_time'] = urlencode((int)(time() / 86400));
|
||||||
|
|
||||||
|
$item->setURI(
|
||||||
|
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
|
||||||
|
. '?'
|
||||||
|
. http_build_query($this->userData)
|
||||||
|
);
|
||||||
|
|
||||||
|
$item->setTitle(
|
||||||
|
'Bridge returned error '
|
||||||
|
. $e->getCode()
|
||||||
|
. '! ('
|
||||||
|
. $this->userData['_error_time']
|
||||||
|
. ')'
|
||||||
|
);
|
||||||
|
$item->setTimestamp(time());
|
||||||
|
$item->setContent(buildBridgeException($e, $bridge));
|
||||||
|
|
||||||
|
$items[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store data in cache
|
||||||
|
$cache->saveData(array(
|
||||||
|
'items' => array_map(function($i){ return $i->toArray(); }, $items),
|
||||||
|
'extraInfos' => $infos
|
||||||
|
));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data transformation
|
||||||
|
try {
|
||||||
|
$format = Format::create($format);
|
||||||
|
$format->setItems($items);
|
||||||
|
$format->setExtraInfos($infos);
|
||||||
|
$format->setLastModified($cache->getTime());
|
||||||
|
$format->display();
|
||||||
|
} catch(Error $e) {
|
||||||
|
error_log($e);
|
||||||
|
header('Content-Type: text/html', true, $e->getCode());
|
||||||
|
die(buildTransformException($e, $bridge));
|
||||||
|
} catch(Exception $e) {
|
||||||
|
error_log($e);
|
||||||
|
header('Content-Type: text/html', true, $e->getCode());
|
||||||
|
die(buildTransformException($e, $bridge));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
actions/ListAction.php
Normal file
53
actions/ListAction.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?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
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ListAction extends ActionAbstract {
|
||||||
|
public function execute() {
|
||||||
|
$list = new StdClass();
|
||||||
|
$list->bridges = array();
|
||||||
|
$list->total = 0;
|
||||||
|
|
||||||
|
foreach(Bridge::getBridgeNames() as $bridgeName) {
|
||||||
|
|
||||||
|
$bridge = Bridge::create($bridgeName);
|
||||||
|
|
||||||
|
if($bridge === false) { // Broken bridge, show as inactive
|
||||||
|
|
||||||
|
$list->bridges[$bridgeName] = array(
|
||||||
|
'status' => 'inactive'
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = Bridge::isWhitelisted($bridgeName) ? 'active' : 'inactive';
|
||||||
|
|
||||||
|
$list->bridges[$bridgeName] = array(
|
||||||
|
'status' => $status,
|
||||||
|
'uri' => $bridge->getURI(),
|
||||||
|
'name' => $bridge->getName(),
|
||||||
|
'icon' => $bridge->getIcon(),
|
||||||
|
'parameters' => $bridge->getParameters(),
|
||||||
|
'maintainer' => $bridge->getMaintainer(),
|
||||||
|
'description' => $bridge->getDescription()
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$list->total = count($list->bridges);
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($list, JSON_PRETTY_PRINT);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ class AllocineFRBridge extends BridgeAbstract {
|
||||||
'category' => array(
|
'category' => array(
|
||||||
'name' => 'category',
|
'name' => 'category',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'exampleValue' => 'Faux Raccord',
|
'exampleValue' => 'Faux Raccord',
|
||||||
'title' => 'Select your category',
|
'title' => 'Select your category',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
|
|
|
@ -16,7 +16,6 @@ class AmazonBridge extends BridgeAbstract {
|
||||||
'sort' => array(
|
'sort' => array(
|
||||||
'name' => 'Sort by',
|
'name' => 'Sort by',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => false,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Relevance' => 'relevanceblender',
|
'Relevance' => 'relevanceblender',
|
||||||
'Price: Low to High' => 'price-asc-rank',
|
'Price: Low to High' => 'price-asc-rank',
|
||||||
|
@ -29,7 +28,6 @@ class AmazonBridge extends BridgeAbstract {
|
||||||
'tld' => array(
|
'tld' => array(
|
||||||
'name' => 'Country',
|
'name' => 'Country',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Australia' => 'com.au',
|
'Australia' => 'com.au',
|
||||||
'Brazil' => 'com.br',
|
'Brazil' => 'com.br',
|
||||||
|
|
|
@ -19,7 +19,6 @@ class AmazonPriceTrackerBridge extends BridgeAbstract {
|
||||||
'tld' => array(
|
'tld' => array(
|
||||||
'name' => 'Country',
|
'name' => 'Country',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Australia' => 'com.au',
|
'Australia' => 'com.au',
|
||||||
'Brazil' => 'com.br',
|
'Brazil' => 'com.br',
|
||||||
|
|
62
bridges/AppleMusicBridge.php
Normal file
62
bridges/AppleMusicBridge.php
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class AppleMusicBridge extends BridgeAbstract {
|
||||||
|
const NAME = 'Apple Music';
|
||||||
|
const URI = 'https://www.apple.com';
|
||||||
|
const DESCRIPTION = 'Fetches the latest releases from an artist';
|
||||||
|
const MAINTAINER = 'Limero';
|
||||||
|
const PARAMETERS = [[
|
||||||
|
'url' => [
|
||||||
|
'name' => 'Artist URL',
|
||||||
|
'exampleValue' => 'https://itunes.apple.com/us/artist/dunderpatrullen/329796274',
|
||||||
|
'required' => true,
|
||||||
|
],
|
||||||
|
'imgSize' => [
|
||||||
|
'name' => 'Image size for thumbnails (in px)',
|
||||||
|
'type' => 'number',
|
||||||
|
'defaultValue' => 512,
|
||||||
|
'required' => true,
|
||||||
|
]
|
||||||
|
]];
|
||||||
|
const CACHE_TIMEOUT = 21600; // 6 hours
|
||||||
|
|
||||||
|
public function collectData() {
|
||||||
|
$url = $this->getInput('url');
|
||||||
|
$html = getSimpleHTMLDOM($url)
|
||||||
|
or returnServerError('Could not request: ' . $url);
|
||||||
|
|
||||||
|
$imgSize = $this->getInput('imgSize');
|
||||||
|
|
||||||
|
// Grab the json data from the page
|
||||||
|
$html = $html->find('script[id=shoebox-ember-data-store]', 0);
|
||||||
|
$html = strstr($html, '{');
|
||||||
|
$html = substr($html, 0, -9);
|
||||||
|
$json = json_decode($html);
|
||||||
|
|
||||||
|
// Loop through each object
|
||||||
|
foreach ($json->included as $obj) {
|
||||||
|
if ($obj->type === 'lockup/album') {
|
||||||
|
$this->items[] = [
|
||||||
|
'title' => $obj->attributes->artistName . ' - ' . $obj->attributes->name,
|
||||||
|
'uri' => $obj->attributes->url,
|
||||||
|
'timestamp' => $obj->attributes->releaseDate,
|
||||||
|
'enclosures' => $obj->relationships->artwork->data->id,
|
||||||
|
];
|
||||||
|
} elseif ($obj->type === 'image') {
|
||||||
|
$images[$obj->id] = $obj->attributes->url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the images to each item
|
||||||
|
foreach ($this->items as &$item) {
|
||||||
|
$item['enclosures'] = [
|
||||||
|
str_replace('{w}x{h}bb.{f}', $imgSize . 'x0w.jpg', $images[$item['enclosures']]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the order to put the latest albums first
|
||||||
|
usort($this->items, function($a, $b){
|
||||||
|
return $a['timestamp'] < $b['timestamp'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
72
bridges/AsahiShimbunAJWBridge.php
Normal file
72
bridges/AsahiShimbunAJWBridge.php
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
class AsahiShimbunAJWBridge extends BridgeAbstract {
|
||||||
|
const NAME = 'Asahi Shimbun AJW';
|
||||||
|
const BASE_URI = 'http://www.asahi.com';
|
||||||
|
const URI = self::BASE_URI . '/ajw/';
|
||||||
|
const DESCRIPTION = 'Asahi Shimbun - Asia & Japan Watch';
|
||||||
|
const MAINTAINER = 'somini';
|
||||||
|
const PARAMETERS = array(
|
||||||
|
array(
|
||||||
|
'section' => array(
|
||||||
|
'type' => 'list',
|
||||||
|
'name' => 'Section',
|
||||||
|
'values' => array(
|
||||||
|
'Japan » Social Affairs' => 'japan/social',
|
||||||
|
'Japan » People' => 'japan/people',
|
||||||
|
'Japan » 3/11 Disaster' => 'japan/0311disaster',
|
||||||
|
'Japan » Sci & Tech' => 'japan/sci_tech',
|
||||||
|
'Politics' => 'politics',
|
||||||
|
'Business' => 'business',
|
||||||
|
'Culture » Style' => 'culture/style',
|
||||||
|
'Culture » Movies' => 'culture/movies',
|
||||||
|
'Culture » Manga & Anime' => 'culture/manga_anime',
|
||||||
|
'Asia » China' => 'asia/china',
|
||||||
|
'Asia » Korean Peninsula' => 'asia/korean_peninsula',
|
||||||
|
'Asia » Around Asia' => 'asia/around_asia',
|
||||||
|
'Opinion » Editorial' => 'opinion/editorial',
|
||||||
|
'Opinion » Vox Populi' => 'opinion/vox',
|
||||||
|
),
|
||||||
|
'defaultValue' => 'Politics',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private function getSectionURI($section) {
|
||||||
|
return self::getURI() . $section . '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collectData() {
|
||||||
|
$html = getSimpleHTMLDOM($this->getSectionURI($this->getInput('section')))
|
||||||
|
or returnServerError('Could not load content');
|
||||||
|
|
||||||
|
foreach($html->find('#MainInner li a') as $element) {
|
||||||
|
if ($element->parent()->class == 'HeadlineTopImage-S') {
|
||||||
|
Debug::log('Skip Headline, it is repeated below');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$item = array();
|
||||||
|
|
||||||
|
$item['uri'] = self::BASE_URI . $element->href;
|
||||||
|
$e_lead = $element->find('span.Lead', 0);
|
||||||
|
if ($e_lead) {
|
||||||
|
$item['content'] = $e_lead->innertext;
|
||||||
|
$e_lead->outertext = '';
|
||||||
|
} else {
|
||||||
|
$item['content'] = $element->innertext;
|
||||||
|
}
|
||||||
|
$e_date = $element->find('span.EnDate', 0);
|
||||||
|
if ($e_date) {
|
||||||
|
$item['timestamp'] = strtotime($e_date->innertext);
|
||||||
|
$e_date->outertext = '';
|
||||||
|
}
|
||||||
|
$e_video = $element->find('span.EnVideo', 0);
|
||||||
|
if ($e_video) {
|
||||||
|
$e_video->outertext = '';
|
||||||
|
$element->innertext = "VIDEO: $element->innertext";
|
||||||
|
}
|
||||||
|
$item['title'] = $element->innertext;
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,63 +3,195 @@
|
||||||
class AutoJMBridge extends BridgeAbstract {
|
class AutoJMBridge extends BridgeAbstract {
|
||||||
|
|
||||||
const NAME = 'AutoJM';
|
const NAME = 'AutoJM';
|
||||||
const URI = 'http://www.autojm.fr/';
|
const URI = 'https://www.autojm.fr/';
|
||||||
const DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages';
|
const DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages';
|
||||||
const MAINTAINER = 'sysadminstory';
|
const MAINTAINER = 'sysadminstory';
|
||||||
const PARAMETERS = array(
|
const PARAMETERS = array(
|
||||||
'Afficher les offres de véhicules disponible en fonction des critères du site AutoJM' => array(
|
'Afficher les offres de véhicules disponible en fonction des critères du site AutoJM' => array(
|
||||||
'url' => array(
|
'url' => array(
|
||||||
'name' => 'URL de la recherche',
|
'name' => 'URL du modèle',
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/',
|
'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/',
|
||||||
'exampleValue' => 'gammes/index/398?order_by=finition_asc&energie[]=3&transmission[]=2&dispo=all'
|
'exampleValue' => 'achat-voitures-neuves-peugeot-nouvelle-308-5p'
|
||||||
|
),
|
||||||
|
'isDispo' => array(
|
||||||
|
'name' => 'Disponibilité',
|
||||||
|
'type' => 'list',
|
||||||
|
'values' => array(
|
||||||
|
'-' => '',
|
||||||
|
'En stock' => 1,
|
||||||
|
'Sur commande' => 0
|
||||||
|
),
|
||||||
|
'title' => 'Critère de disponibilité'
|
||||||
|
),
|
||||||
|
'energy' => array(
|
||||||
|
'name' => 'Carburant',
|
||||||
|
'type' => 'list',
|
||||||
|
'values' => array(
|
||||||
|
'-' => '',
|
||||||
|
'Diesel' => 1,
|
||||||
|
'Essence' => 3,
|
||||||
|
'Hybride' => 5
|
||||||
|
),
|
||||||
|
'title' => 'Carburant'
|
||||||
|
),
|
||||||
|
'transmission' => array(
|
||||||
|
'name' => 'Transmission',
|
||||||
|
'type' => 'list',
|
||||||
|
'values' => array(
|
||||||
|
'-' => '',
|
||||||
|
'Automatique' => 1,
|
||||||
|
'Manuelle' => 2
|
||||||
|
),
|
||||||
|
'title' => 'Transmission'
|
||||||
|
),
|
||||||
|
'priceMin' => array(
|
||||||
|
'name' => 'Prix minimum',
|
||||||
|
'type' => 'number',
|
||||||
|
'required' => false,
|
||||||
|
'title' => 'Prix minimum du véhicule',
|
||||||
|
'exampleValue' => '10000',
|
||||||
|
'defaultValue' => '0'
|
||||||
|
),
|
||||||
|
'priceMax' => array(
|
||||||
|
'name' => 'Prix maximum',
|
||||||
|
'type' => 'number',
|
||||||
|
'required' => false,
|
||||||
|
'title' => 'Prix maximum du véhicule',
|
||||||
|
'exampleValue' => '15000',
|
||||||
|
'defaultValue' => '150000'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const CACHE_TIMEOUT = 3600;
|
const CACHE_TIMEOUT = 3600;
|
||||||
|
|
||||||
public function getIcon() {
|
public function getIcon() {
|
||||||
return self::URI . 'assets/images/favicon.ico';
|
return self::URI . 'favicon.ico';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function collectData() {
|
public function getName() {
|
||||||
$html = getSimpleHTMLDOM(self::URI . $this->getInput('url'))
|
switch($this->queriedContext) {
|
||||||
or returnServerError('Could not request AutoJM.');
|
case 'Afficher les offres de véhicules disponible en fonction des critères du site AutoJM':
|
||||||
$list = $html->find('div[class*=ligne_modele]');
|
$html = getSimpleHTMLDOMCached(self::URI . $this->getInput('url'), 86400);
|
||||||
foreach($list as $element) {
|
$name = html_entity_decode($html->find('title', 0)->plaintext);
|
||||||
$image = $element->find('img[class=width-100]', 0)->src;
|
return $name;
|
||||||
$serie = $element->find('div[class=serie]', 0)->find('span', 0)->plaintext;
|
break;
|
||||||
$url = $element->find('div[class=serie]', 0)->find('a[class=btn_ligne color-black]', 0)->href;
|
default:
|
||||||
if($element->find('div[class*=hasStock-info]', 0) != null) {
|
return parent::getName();
|
||||||
$dispo = 'Disponible';
|
|
||||||
} else {
|
|
||||||
$dispo = 'Sur commande';
|
|
||||||
}
|
|
||||||
$carburant = str_replace('dispo |', '', $element->find('div[class=carburant]', 0)->plaintext);
|
|
||||||
$transmission = $element->find('div[class*=bv]', 0)->plaintext;
|
|
||||||
$places = $element->find('div[class*=places]', 0)->plaintext;
|
|
||||||
$portes = $element->find('div[class*=nb_portes]', 0)->plaintext;
|
|
||||||
$carosserie = $element->find('div[class*=coloris]', 0)->plaintext;
|
|
||||||
$remise = $element->find('div[class*=remise]', 0)->plaintext;
|
|
||||||
$prix = $element->find('div[class*=prixjm]', 0)->plaintext;
|
|
||||||
|
|
||||||
$item = array();
|
|
||||||
$item['uri'] = $url;
|
|
||||||
$item['title'] = $serie;
|
|
||||||
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />' . $serie . '</p>';
|
|
||||||
$item['content'] .= '<ul><li>Disponibilité : ' . $dispo . '</li>';
|
|
||||||
$item['content'] .= '<li>Carburant : ' . $carburant . '</li>';
|
|
||||||
$item['content'] .= '<li>Transmission : ' . $transmission . '</li>';
|
|
||||||
$item['content'] .= '<li>Nombre de places : ' . $places . '</li>';
|
|
||||||
$item['content'] .= '<li>Nombre de portes : ' . $portes . '</li>';
|
|
||||||
$item['content'] .= '<li>Série : ' . $serie . '</li>';
|
|
||||||
$item['content'] .= '<li>Carosserie : ' . $carosserie . '</li>';
|
|
||||||
$item['content'] .= '<li>Remise : ' . $remise . '</li>';
|
|
||||||
$item['content'] .= '<li>Prix : ' . $prix . '</li></ul>';
|
|
||||||
|
|
||||||
$this->items[] = $item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function collectData() {
|
||||||
|
|
||||||
|
$model_url = self::URI . $this->getInput('url');
|
||||||
|
|
||||||
|
// Get the session cookies and the form token
|
||||||
|
$this->getInitialParameters($model_url);
|
||||||
|
|
||||||
|
// Build the form
|
||||||
|
$post_data = array(
|
||||||
|
'form[isDispo]' => $this->getInput('isDispo'),
|
||||||
|
'form[energy]' => $this->getInput('energy'),
|
||||||
|
'form[transmission]' => $this->getInput('transmission'),
|
||||||
|
'form[priceMin]' => $this->getInput('priceMin'),
|
||||||
|
'form[priceMin]' => $this->getInput('priceMin'),
|
||||||
|
'form[_token]' => $this->token
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set the Form request content type
|
||||||
|
$header = array(
|
||||||
|
'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set the curl options (POST query and content, and session cookies
|
||||||
|
$curl_opts = array(
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => http_build_query($post_data),
|
||||||
|
CURLOPT_COOKIE => $this->cookies
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the JSON content of the form
|
||||||
|
$json = getContents($model_url, $header, $curl_opts)
|
||||||
|
or returnServerError('Could not request AutoJM.');
|
||||||
|
|
||||||
|
// Extract the HTML content from the JSON result
|
||||||
|
$data = json_decode($json);
|
||||||
|
$html = str_get_html($data->content);
|
||||||
|
|
||||||
|
// Go through every finisha of the model
|
||||||
|
$list = $html->find('h2');
|
||||||
|
foreach ($list as $finish) {
|
||||||
|
$finish_name = $finish->plaintext;
|
||||||
|
$motorizations = $finish->next_sibling()->find('li');
|
||||||
|
foreach ($motorizations as $element) {
|
||||||
|
$image = $element->find('div[class=block-product-image]', 0)->{'data-ga-banner'};
|
||||||
|
$serie = $element->find('span[class=model]', 0)->plaintext;
|
||||||
|
$url = self::URI . substr($element->find('a', 0)->href, 1);
|
||||||
|
if ($element->find('span[class*=block-product-nbModel]', 0) != null) {
|
||||||
|
$availability = 'En Stock';
|
||||||
|
} else {
|
||||||
|
$availability = 'Sur commande';
|
||||||
|
}
|
||||||
|
$discount_html = $element->find('span[class*=tag--promo]', 0);
|
||||||
|
if ($discount_html != null) {
|
||||||
|
$discount = $discount_html->plaintext;
|
||||||
|
} else {
|
||||||
|
$discount = 'inconnue';
|
||||||
|
}
|
||||||
|
$price = $element->find('span[class=price red h1]', 0)->plaintext;
|
||||||
|
$item = array();
|
||||||
|
$item['title'] = $finish_name . ' ' . $serie;
|
||||||
|
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />'
|
||||||
|
. $finish_name . ' ' . $serie . '</p>';
|
||||||
|
$item['content'] .= '<ul><li>Disponibilité : ' . $availability . '</li>';
|
||||||
|
$item['content'] .= '<li>Série : ' . $serie . '</li>';
|
||||||
|
$item['content'] .= '<li>Remise : ' . $discount . '</li>';
|
||||||
|
$item['content'] .= '<li>Prix : ' . $price . '</li></ul>';
|
||||||
|
|
||||||
|
// Add a fictionnal anchor to the RSS element URL, based on the item content ;
|
||||||
|
// As the URL could be identical even if the price change, some RSS reader will not show those offers as new items
|
||||||
|
$item['uri'] = $url . '#' . md5($item['content']);
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the session cookie and the form token
|
||||||
|
*
|
||||||
|
* @param string $pageURL The URL from which to get the values
|
||||||
|
*/
|
||||||
|
private function getInitialParameters($pageURL) {
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $pageURL);
|
||||||
|
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
$data = curl_exec($ch);
|
||||||
|
|
||||||
|
// Separate the response header and the content
|
||||||
|
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||||
|
$header = substr($data, 0, $headerSize);
|
||||||
|
$content = substr($data, $headerSize);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
// Extract the cookies from the headers
|
||||||
|
$cookies = '';
|
||||||
|
$http_response_header = explode("\r\n", $header);
|
||||||
|
foreach ($http_response_header as $hdr) {
|
||||||
|
if (strpos($hdr, 'Set-Cookie') !== false) {
|
||||||
|
$cLine = explode(':', $hdr)[1];
|
||||||
|
$cLine = explode(';', $cLine)[0];
|
||||||
|
$cookies .= ';' . $cLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->cookies = trim(substr($cookies, 1));
|
||||||
|
|
||||||
|
// Get the token from the content
|
||||||
|
$html = str_get_html($content);
|
||||||
|
$token = $html->find('input[type=hidden][id=form__token]', 0);
|
||||||
|
$this->token = $token->value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract {
|
||||||
'exampleValue' => '12345'
|
'exampleValue' => '12345'
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
|
const LIMIT_COLS = 5;
|
||||||
const LIMIT_ITEMS = 10;
|
const LIMIT_ITEMS = 10;
|
||||||
|
|
||||||
private $feedName = '';
|
private $feedName = '';
|
||||||
|
@ -20,21 +21,21 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract {
|
||||||
$html = getSimpleHTMLDOM($this->getURI())
|
$html = getSimpleHTMLDOM($this->getURI())
|
||||||
or returnServerError('Series not found');
|
or returnServerError('Series not found');
|
||||||
|
|
||||||
$objTitle = $html->find('td[class="text pad"]', 1);
|
// content is an unstructured pile of divs, ugly to parse
|
||||||
if ($objTitle)
|
$cols = $html->find('div#main_content div.row > div.text');
|
||||||
$this->feedName = $objTitle->plaintext;
|
if (!$cols)
|
||||||
|
|
||||||
$itemlist = $html->find('td#main_content table table table tr');
|
|
||||||
if (!$itemlist)
|
|
||||||
returnServerError('No releases');
|
returnServerError('No releases');
|
||||||
|
|
||||||
$limit = self::LIMIT_ITEMS;
|
$rows = array_slice(
|
||||||
foreach($itemlist as $element) {
|
array_chunk($cols, self::LIMIT_COLS), 0, self::LIMIT_ITEMS
|
||||||
$cols = $element->find('td[class="text pad"]');
|
);
|
||||||
if (!$cols)
|
|
||||||
continue;
|
if (isset($rows[0][1])) {
|
||||||
if ($limit <= 0)
|
$this->feedName = $this->filterHTML($rows[0][1]->plaintext);
|
||||||
break;
|
}
|
||||||
|
|
||||||
|
foreach($rows as $cols) {
|
||||||
|
if (count($cols) < self::LIMIT_COLS) continue;
|
||||||
|
|
||||||
$item = array();
|
$item = array();
|
||||||
$title = array();
|
$title = array();
|
||||||
|
@ -47,8 +48,8 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract {
|
||||||
|
|
||||||
$objTitle = $cols[1];
|
$objTitle = $cols[1];
|
||||||
if ($objTitle) {
|
if ($objTitle) {
|
||||||
$title[] = html_entity_decode($objTitle->plaintext);
|
$title[] = $this->filterHTML($objTitle->plaintext);
|
||||||
$item['content'] .= '<p>Series: ' . $objTitle->innertext . '</p>';
|
$item['content'] .= '<p>Series: ' . $this->filterText($objTitle->innertext) . '</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$objVolume = $cols[2];
|
$objVolume = $cols[2];
|
||||||
|
@ -61,18 +62,15 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract {
|
||||||
|
|
||||||
$objAuthor = $cols[4];
|
$objAuthor = $cols[4];
|
||||||
if ($objAuthor && !empty($objAuthor->plaintext)) {
|
if ($objAuthor && !empty($objAuthor->plaintext)) {
|
||||||
$item['author'] = html_entity_decode($objAuthor->plaintext);
|
$item['author'] = $this->filterHTML($objAuthor->plaintext);
|
||||||
$item['content'] .= '<p>Groups: ' . $objAuthor->innertext . '</p>';
|
$item['content'] .= '<p>Groups: ' . $this->filterText($objAuthor->innertext) . '</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$item['title'] = implode(' ', $title);
|
$item['title'] = implode(' ', $title);
|
||||||
$item['uri'] = $this->getURI() . '#' . hash('sha1', $item['title']);
|
$item['uri'] = $this->getURI();
|
||||||
|
$item['uid'] = hash('sha1', $item['title']);
|
||||||
|
|
||||||
$this->items[] = $item;
|
$this->items[] = $item;
|
||||||
|
|
||||||
if(count($this->items) >= $limit) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,4 +88,12 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract {
|
||||||
}
|
}
|
||||||
return parent::getName();
|
return parent::getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function filterText($text) {
|
||||||
|
return rtrim($text, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function filterHTML($text) {
|
||||||
|
return $this->filterText(html_entity_decode($text));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,48 +13,72 @@ class BandcampBridge extends BridgeAbstract {
|
||||||
'required' => true
|
'required' => true
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
|
const IMGURI = 'https://f4.bcbits.com/';
|
||||||
|
const IMGSIZE_300PX = 23;
|
||||||
|
const IMGSIZE_700PX = 16;
|
||||||
|
|
||||||
public function getIcon() {
|
public function getIcon() {
|
||||||
return 'https://s4.bcbits.com/img/bc_favicon.ico';
|
return 'https://s4.bcbits.com/img/bc_favicon.ico';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function collectData(){
|
public function collectData(){
|
||||||
$html = getSimpleHTMLDOM($this->getURI())
|
$url = self::URI . 'api/hub/1/dig_deeper';
|
||||||
or returnServerError('No results for this query.');
|
$data = $this->buildRequestJson();
|
||||||
|
$header = array(
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Content-Length: ' . strlen($data)
|
||||||
|
);
|
||||||
|
$opts = array(
|
||||||
|
CURLOPT_CUSTOMREQUEST => 'POST',
|
||||||
|
CURLOPT_POSTFIELDS => $data
|
||||||
|
);
|
||||||
|
$content = getContents($url, $header, $opts)
|
||||||
|
or returnServerError('Could not complete request to: ' . $url);
|
||||||
|
|
||||||
foreach($html->find('li.item') as $release) {
|
$json = json_decode($content);
|
||||||
$script = $release->find('div.art', 0)->getAttribute('onclick');
|
|
||||||
$uri = ltrim($script, "return 'url(");
|
|
||||||
$uri = rtrim($uri, "')");
|
|
||||||
|
|
||||||
$item = array();
|
if ($json->ok !== true) {
|
||||||
$item['author'] = $release->find('div.itemsubtext', 0)->plaintext
|
returnServerError('Invalid response');
|
||||||
. ' - '
|
}
|
||||||
. $release->find('div.itemtext', 0)->plaintext;
|
|
||||||
|
|
||||||
$item['title'] = $release->find('div.itemsubtext', 0)->plaintext
|
foreach ($json->items as $entry) {
|
||||||
. ' - '
|
$url = $entry->tralbum_url;
|
||||||
. $release->find('div.itemtext', 0)->plaintext;
|
$artist = $entry->artist;
|
||||||
|
$title = $entry->title;
|
||||||
|
// e.g. record label is the releaser, but not the artist
|
||||||
|
$releaser = $entry->band_name !== $entry->artist ? $entry->band_name : null;
|
||||||
|
|
||||||
$item['content'] = '<img src="'
|
$full_title = $artist . ' - ' . $title;
|
||||||
. $uri
|
$full_artist = $artist;
|
||||||
. '"/><br/>'
|
if (isset($releaser)) {
|
||||||
. $release->find('div.itemsubtext', 0)->plaintext
|
$full_title .= ' (' . $releaser . ')';
|
||||||
. ' - '
|
$full_artist .= ' (' . $releaser . ')';
|
||||||
. $release->find('div.itemtext', 0)->plaintext;
|
}
|
||||||
|
$small_img = $this->getImageUrl($entry->art_id, self::IMGSIZE_300PX);
|
||||||
|
$img = $this->getImageUrl($entry->art_id, self::IMGSIZE_700PX);
|
||||||
|
|
||||||
$item['id'] = $release->find('a', 0)->getAttribute('href');
|
$item = array(
|
||||||
$item['uri'] = $release->find('a', 0)->getAttribute('href');
|
'uri' => $url,
|
||||||
|
'author' => $full_artist,
|
||||||
|
'title' => $full_title
|
||||||
|
);
|
||||||
|
$item['content'] = "<img src='$small_img' /><br/>$full_title";
|
||||||
|
$item['enclosures'] = array($img);
|
||||||
$this->items[] = $item;
|
$this->items[] = $item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getURI(){
|
private function buildRequestJson(){
|
||||||
if(!is_null($this->getInput('tag'))) {
|
$requestJson = array(
|
||||||
return self::URI . 'tag/' . urlencode($this->getInput('tag')) . '?sort_field=date';
|
'tag' => $this->getInput('tag'),
|
||||||
}
|
'page' => 1,
|
||||||
|
'sort' => 'date'
|
||||||
|
);
|
||||||
|
return json_encode($requestJson);
|
||||||
|
}
|
||||||
|
|
||||||
return parent::getURI();
|
private function getImageUrl($id, $size){
|
||||||
|
return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getName(){
|
public function getName(){
|
||||||
|
|
119
bridges/BingSearchBridge.php
Normal file
119
bridges/BingSearchBridge.php
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class BingSearchBridge extends BridgeAbstract
|
||||||
|
{
|
||||||
|
const NAME = 'Bing search';
|
||||||
|
const URI = 'https://www.bing.com/';
|
||||||
|
const DESCRIPTION = 'Return images from bing search discover';
|
||||||
|
const MAINTAINER = 'DnAp';
|
||||||
|
const PARAMETERS = array(
|
||||||
|
'Image Discover' => array(
|
||||||
|
'category' => array(
|
||||||
|
'name' => 'Categories',
|
||||||
|
'type' => 'list',
|
||||||
|
'values' => self::IMAGE_DISCOVER_CATEGORIES
|
||||||
|
),
|
||||||
|
'image_size' => array(
|
||||||
|
'name' => 'Image size',
|
||||||
|
'type' => 'list',
|
||||||
|
'values' => array(
|
||||||
|
'Small' => 'turl',
|
||||||
|
'Full size' => 'imgurl'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const IMAGE_DISCOVER_CATEGORIES = array(
|
||||||
|
'Abstract' => 'abstract',
|
||||||
|
'Animals' => 'animals',
|
||||||
|
'Anime' => 'anime',
|
||||||
|
'Architecture' => 'architecture',
|
||||||
|
'Arts and Crafts' => 'arts-and-crafts',
|
||||||
|
'Beauty' => 'beauty',
|
||||||
|
'Cars and Motorcycles' => 'cars-and-motorcycles',
|
||||||
|
'Cats' => 'cats',
|
||||||
|
'Celebrities' => 'celebrities',
|
||||||
|
'Comics' => 'comics',
|
||||||
|
'DIY' => 'diy',
|
||||||
|
'Dogs' => 'dogs',
|
||||||
|
'Fitness' => 'fitness',
|
||||||
|
'Food and Drink' => 'food-and-drink',
|
||||||
|
'Funny' => 'funny',
|
||||||
|
'Gadgets' => 'gadgets',
|
||||||
|
'Gardening' => 'gardening',
|
||||||
|
'Geeky' => 'geeky',
|
||||||
|
'Hairstyles' => 'hairstyles',
|
||||||
|
'Home Decor' => 'home-decor',
|
||||||
|
'Marine Life' => 'marine-life',
|
||||||
|
'Men\'s Fashion' => 'men%27s-fashion',
|
||||||
|
'Nature' => 'nature',
|
||||||
|
'Outdoors' => 'outdoors',
|
||||||
|
'Parenting' => 'parenting',
|
||||||
|
'Phone Wallpapers' => 'phone-wallpapers',
|
||||||
|
'Photography' => 'photography',
|
||||||
|
'Quotes' => 'quotes',
|
||||||
|
'Recipes' => 'recipes',
|
||||||
|
'Snow' => 'snow',
|
||||||
|
'Tattoos' => 'tattoos',
|
||||||
|
'Travel' => 'travel',
|
||||||
|
'Video Games' => 'video-games',
|
||||||
|
'Weddings' => 'weddings',
|
||||||
|
'Women\'s Fashion' => 'women%27s-fashion',
|
||||||
|
);
|
||||||
|
|
||||||
|
public function getIcon()
|
||||||
|
{
|
||||||
|
return 'https://www.bing.com/sa/simg/bing_p_rr_teal_min.ico';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collectData()
|
||||||
|
{
|
||||||
|
$this->items = $this->imageDiscover($this->getInput('category'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
if ($this->getInput('category')) {
|
||||||
|
if (self::IMAGE_DISCOVER_CATEGORIES[$this->getInput('categories')] !== null) {
|
||||||
|
$category = self::IMAGE_DISCOVER_CATEGORIES[$this->getInput('categories')];
|
||||||
|
} else {
|
||||||
|
$category = 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Best ' . $category . ' - Bing Image Discover';
|
||||||
|
}
|
||||||
|
return parent::getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function imageDiscover($category)
|
||||||
|
{
|
||||||
|
$html = getSimpleHTMLDOM(self::URI . '/discover/' . $category)
|
||||||
|
or returnServerError('Could not request ' . self::NAME);
|
||||||
|
$sizeKey = $this->getInput('image_size');
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
foreach ($html->find('a.iusc') as $element) {
|
||||||
|
$data = json_decode(htmlspecialchars_decode($element->getAttribute('m')), true);
|
||||||
|
|
||||||
|
$item = array();
|
||||||
|
$item['title'] = basename(rtrim($data['imgurl'], '/'));
|
||||||
|
$item['uri'] = $data['imgurl'];
|
||||||
|
$item['content'] = '<a href="' . $data['imgurl'] . '">
|
||||||
|
<img src="' . $data[$sizeKey] . '" alt="' . $item['title'] . '"></a>
|
||||||
|
<p>Source: <a href="' . $this->curUrl($data['surl']) . '"> </a></p>';
|
||||||
|
$item['enclosures'] = $data['imgurl'];
|
||||||
|
|
||||||
|
$items[] = $item;
|
||||||
|
}
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function curUrl($url)
|
||||||
|
{
|
||||||
|
if (strlen($url) <= 80) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
return substr($url, 0, 80) . '...';
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,6 @@ class BundesbankBridge extends BridgeAbstract {
|
||||||
self::PARAM_LANG => array(
|
self::PARAM_LANG => array(
|
||||||
'name' => 'Language',
|
'name' => 'Language',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'defaultValue' => self::LANG_DE,
|
'defaultValue' => self::LANG_DE,
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'English' => self::LANG_EN,
|
'English' => self::LANG_EN,
|
||||||
|
|
134
bridges/CachetBridge.php
Normal file
134
bridges/CachetBridge.php
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class CachetBridge extends BridgeAbstract {
|
||||||
|
const NAME = 'Cachet Bridge';
|
||||||
|
const URI = 'https://cachethq.io/';
|
||||||
|
const DESCRIPTION = 'Returns status updates from any Cachet installation';
|
||||||
|
const MAINTAINER = 'klimplant';
|
||||||
|
const PARAMETERS = array(
|
||||||
|
array(
|
||||||
|
'host' => array(
|
||||||
|
'name' => 'Cachet installation',
|
||||||
|
'type' => 'text',
|
||||||
|
'required' => true,
|
||||||
|
'title' => 'The URL of the Cachet installation',
|
||||||
|
'exampleValue' => 'https://demo.cachethq.io/',
|
||||||
|
), 'additional_info' => array(
|
||||||
|
'name' => 'Additional Timestamps',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'title' => 'Whether to include the given timestamps'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const CACHE_TIMEOUT = 300;
|
||||||
|
|
||||||
|
private $componentCache = [];
|
||||||
|
|
||||||
|
public function getURI() {
|
||||||
|
return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the ping request to the cache API
|
||||||
|
*
|
||||||
|
* @param string $ping
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
private function validatePing($ping) {
|
||||||
|
$ping = json_decode($ping);
|
||||||
|
if ($ping === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $ping->data === 'Pong!';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the component name of a cachat component
|
||||||
|
*
|
||||||
|
* @param integer $id
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getComponentName($id) {
|
||||||
|
if ($id === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (array_key_exists($id, $this->componentCache)) {
|
||||||
|
return $this->componentCache[$id];
|
||||||
|
}
|
||||||
|
|
||||||
|
$component = getContents($this->getURI() . '/api/v1/components/' . $id);
|
||||||
|
$component = json_decode($component);
|
||||||
|
if ($component === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $component->data->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collectData() {
|
||||||
|
$ping = getContents(urljoin($this->getURI(), '/api/v1/ping'));
|
||||||
|
if (!$this->validatePing($ping)) {
|
||||||
|
returnClientError('Provided URI is invalid!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = urljoin($this->getURI(), '/api/v1/incidents?sort=id&order=desc');
|
||||||
|
$incidents = getContents($url);
|
||||||
|
$incidents = json_decode($incidents);
|
||||||
|
if ($incidents === null) {
|
||||||
|
returnClientError('/api/v1/incidents returned no valid json');
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($incidents->data, function ($a, $b) {
|
||||||
|
$timeA = strtotime($a->updated_at);
|
||||||
|
$timeB = strtotime($b->updated_at);
|
||||||
|
return $timeA > $timeB ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ($incidents->data as $incident) {
|
||||||
|
|
||||||
|
if (isset($incident->permalink)) {
|
||||||
|
$permalink = $incident->permalink;
|
||||||
|
} else {
|
||||||
|
$permalink = urljoin($this->getURI(), '/incident/' . $incident->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = $incident->human_status . ': ' . $incident->name;
|
||||||
|
$message = '';
|
||||||
|
if ($this->getInput('additional_info')) {
|
||||||
|
if (isset($incident->occurred_at)) {
|
||||||
|
$message .= 'Occurred at: ' . $incident->occurred_at . "\r\n";
|
||||||
|
}
|
||||||
|
if (isset($incident->scheduled_at)) {
|
||||||
|
$message .= 'Scheduled at: ' . $incident->scheduled_at . "\r\n";
|
||||||
|
}
|
||||||
|
if (isset($incident->created_at)) {
|
||||||
|
$message .= 'Created at: ' . $incident->created_at . "\r\n";
|
||||||
|
}
|
||||||
|
if (isset($incident->updated_at)) {
|
||||||
|
$message .= 'Updated at: ' . $incident->updated_at . "\r\n\r\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$message .= $incident->message;
|
||||||
|
$content = nl2br($message);
|
||||||
|
$componentName = $this->getComponentName($incident->component_id);
|
||||||
|
$uidOrig = $permalink . $incident->created_at;
|
||||||
|
$uid = hash('sha512', $uidOrig);
|
||||||
|
$timestamp = strtotime($incident->created_at);
|
||||||
|
$categories = [];
|
||||||
|
$categories[] = $incident->human_status;
|
||||||
|
if ($componentName !== '') {
|
||||||
|
$categories[] = $componentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = [];
|
||||||
|
$item['uri'] = $permalink;
|
||||||
|
$item['title'] = $title;
|
||||||
|
$item['timestamp'] = $timestamp;
|
||||||
|
$item['content'] = $content;
|
||||||
|
$item['uid'] = $uid;
|
||||||
|
$item['categories'] = $categories;
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
bridges/ComboiosDePortugalBridge.php
Normal file
22
bridges/ComboiosDePortugalBridge.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
class ComboiosDePortugalBridge extends BridgeAbstract {
|
||||||
|
const NAME = 'CP | Avisos';
|
||||||
|
const BASE_URI = 'https://www.cp.pt';
|
||||||
|
const URI = self::BASE_URI . '/passageiros/pt';
|
||||||
|
const DESCRIPTION = 'Comboios de Portugal | Avisos';
|
||||||
|
const MAINTAINER = 'somini';
|
||||||
|
|
||||||
|
public function collectData() {
|
||||||
|
$html = getSimpleHTMLDOM($this->getURI() . '/consultar-horarios/avisos')
|
||||||
|
or returnServerError('Could not load content');
|
||||||
|
|
||||||
|
foreach($html->find('.warnings-table a') as $element) {
|
||||||
|
$item = array();
|
||||||
|
|
||||||
|
$item['title'] = $element->innertext;
|
||||||
|
$item['uri'] = self::BASE_URI . implode('/', array_map('urlencode', explode('/', $element->href)));
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,6 @@ class ContainerLinuxReleasesBridge extends BridgeAbstract {
|
||||||
'channel' => [
|
'channel' => [
|
||||||
'name' => 'Release Channel',
|
'name' => 'Release Channel',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'defaultValue' => self::STABLE,
|
'defaultValue' => self::STABLE,
|
||||||
'values' => [
|
'values' => [
|
||||||
'Stable' => self::STABLE,
|
'Stable' => self::STABLE,
|
||||||
|
|
|
@ -15,13 +15,11 @@ class DealabsBridge extends PepperBridgeAbstract {
|
||||||
'hide_expired' => array(
|
'hide_expired' => array(
|
||||||
'name' => 'Masquer les éléments expirés',
|
'name' => 'Masquer les éléments expirés',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'required' => true
|
|
||||||
),
|
),
|
||||||
'hide_local' => array(
|
'hide_local' => array(
|
||||||
'name' => 'Masquer les deals locaux',
|
'name' => 'Masquer les deals locaux',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'title' => 'Masquer les deals en magasins physiques',
|
'title' => 'Masquer les deals en magasins physiques',
|
||||||
'required' => true
|
|
||||||
),
|
),
|
||||||
'priceFrom' => array(
|
'priceFrom' => array(
|
||||||
'name' => 'Prix minimum',
|
'name' => 'Prix minimum',
|
||||||
|
@ -41,7 +39,6 @@ class DealabsBridge extends PepperBridgeAbstract {
|
||||||
'group' => array(
|
'group' => array(
|
||||||
'name' => 'Groupe',
|
'name' => 'Groupe',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Groupe dont il faut afficher les deals',
|
'title' => 'Groupe dont il faut afficher les deals',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Abonnements internet' => 'abonnements-internet',
|
'Abonnements internet' => 'abonnements-internet',
|
||||||
|
@ -957,7 +954,6 @@ class DealabsBridge extends PepperBridgeAbstract {
|
||||||
'order' => array(
|
'order' => array(
|
||||||
'name' => 'Trier par',
|
'name' => 'Trier par',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Ordre de tri des deals',
|
'title' => 'Ordre de tri des deals',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Du deal le plus Hot au moins Hot' => '',
|
'Du deal le plus Hot au moins Hot' => '',
|
||||||
|
@ -1380,8 +1376,11 @@ class PepperBridgeAbstract extends BridgeAbstract {
|
||||||
|
|
||||||
// Add the Hour and minutes
|
// Add the Hour and minutes
|
||||||
$date_str .= ' 00:00';
|
$date_str .= ' 00:00';
|
||||||
|
|
||||||
$date = DateTime::createFromFormat('j F Y H:i', $date_str);
|
$date = DateTime::createFromFormat('j F Y H:i', $date_str);
|
||||||
|
// In some case, the date is not recognized : as a workaround the actual date is taken
|
||||||
|
if($date === false) {
|
||||||
|
$date = new DateTime();
|
||||||
|
}
|
||||||
return $date->getTimestamp();
|
return $date->getTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ class DesoutterBridge extends BridgeAbstract {
|
||||||
'news_lang' => array(
|
'news_lang' => array(
|
||||||
'name' => 'Language',
|
'name' => 'Language',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Select your language',
|
'title' => 'Select your language',
|
||||||
'defaultValue' => 'Corporate',
|
'defaultValue' => 'Corporate',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
|
@ -66,7 +65,6 @@ class DesoutterBridge extends BridgeAbstract {
|
||||||
'industry_lang' => array(
|
'industry_lang' => array(
|
||||||
'name' => 'Language',
|
'name' => 'Language',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Select your language',
|
'title' => 'Select your language',
|
||||||
'defaultValue' => 'Corporate',
|
'defaultValue' => 'Corporate',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
|
@ -117,7 +115,6 @@ class DesoutterBridge extends BridgeAbstract {
|
||||||
'full' => array(
|
'full' => array(
|
||||||
'name' => 'Load full articles',
|
'name' => 'Load full articles',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'required' => false,
|
|
||||||
'title' => 'Enable to load the full article for each item'
|
'title' => 'Enable to load the full article for each item'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
63
bridges/EconomistBridge.php
Normal file
63
bridges/EconomistBridge.php
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
class EconomistBridge extends BridgeAbstract {
|
||||||
|
const NAME = 'The Economist: Latest Updates';
|
||||||
|
const URI = 'https://www.economist.com';
|
||||||
|
const DESCRIPTION = 'Fetches the latest updates from the Economist.';
|
||||||
|
const MAINTAINER = 'thefranke';
|
||||||
|
const CACHE_TIMEOUT = 3600; // 1h
|
||||||
|
|
||||||
|
public function getIcon() {
|
||||||
|
return 'https://www.economist.com/sites/default/files/econfinal_favicon.ico';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collectData() {
|
||||||
|
$html = getSimpleHTMLDOM(self::URI . '/latest/')
|
||||||
|
or returnServerError('Could not fetch latest updates form The Economist.');
|
||||||
|
|
||||||
|
foreach($html->find('article') as $element) {
|
||||||
|
|
||||||
|
$a = $element->find('a', 0);
|
||||||
|
$href = self::URI . $a->href;
|
||||||
|
$full = getSimpleHTMLDOMCached($href);
|
||||||
|
$article = $full->find('article', 0);
|
||||||
|
|
||||||
|
$header = $article->find('h1', 0);
|
||||||
|
$author = $article->find('span[itemprop="author"]', 0);
|
||||||
|
$time = $article->find('time[itemprop="dateCreated"]', 0);
|
||||||
|
$content = $article->find('div[itemprop="description"]', 0);
|
||||||
|
|
||||||
|
// Remove newsletter subscription box
|
||||||
|
$newsletter = $content->find('div[class="newsletter-form__message"]', 0);
|
||||||
|
if ($newsletter)
|
||||||
|
$newsletter->outertext = '';
|
||||||
|
|
||||||
|
$newsletterForm = $content->find('form', 0);
|
||||||
|
if ($newsletterForm)
|
||||||
|
$newsletterForm->outertext = '';
|
||||||
|
|
||||||
|
// Remove next and previous article URLs at the bottom
|
||||||
|
$nextprev = $content->find('div[class="blog-post__next-previous-wrapper"]', 0);
|
||||||
|
if ($nextprev)
|
||||||
|
$nextprev->outertext = '';
|
||||||
|
|
||||||
|
$section = [ $article->find('h3[itemprop="articleSection"]', 0)->plaintext ];
|
||||||
|
|
||||||
|
$item = array();
|
||||||
|
$item['title'] = $header->find('span', 0)->innertext . ': '
|
||||||
|
. $header->find('span', 1)->innertext;
|
||||||
|
|
||||||
|
$item['uri'] = $href;
|
||||||
|
$item['timestamp'] = strtotime($time->datetime);
|
||||||
|
$item['author'] = $author->innertext;
|
||||||
|
$item['categories'] = $section;
|
||||||
|
|
||||||
|
$item['content'] = '<img style="max-width: 100%" src="'
|
||||||
|
. $a->find('img', 0)->src . '">' . $content->innertext;
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
|
||||||
|
if (count($this->items) >= 10)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -120,7 +120,7 @@ class ElloBridge extends BridgeAbstract {
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getAPIKey() {
|
private function getAPIKey() {
|
||||||
$cache = Cache::create('FileCache');
|
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
|
||||||
$cache->setPath(PATH_CACHE);
|
$cache->setPath(PATH_CACHE);
|
||||||
$cache->setParameters(['key']);
|
$cache->setParameters(['key']);
|
||||||
$key = $cache->loadData();
|
$key = $cache->loadData();
|
||||||
|
|
|
@ -15,7 +15,6 @@ class ExtremeDownloadBridge extends BridgeAbstract {
|
||||||
'filter' => array(
|
'filter' => array(
|
||||||
'name' => 'Type de contenu',
|
'name' => 'Type de contenu',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Type de contenu à suivre : Téléchargement, Streaming ou les deux',
|
'title' => 'Type de contenu à suivre : Téléchargement, Streaming ou les deux',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Streaming et Téléchargement' => 'both',
|
'Streaming et Téléchargement' => 'both',
|
||||||
|
|
|
@ -11,7 +11,6 @@ class FDroidBridge extends BridgeAbstract {
|
||||||
'u' => array(
|
'u' => array(
|
||||||
'name' => 'Widget selection',
|
'name' => 'Widget selection',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Latest added apps' => 'added',
|
'Latest added apps' => 'added',
|
||||||
'Latest updated apps' => 'updated'
|
'Latest updated apps' => 'updated'
|
||||||
|
@ -29,14 +28,14 @@ class FDroidBridge extends BridgeAbstract {
|
||||||
or returnServerError('Could not request F-Droid.');
|
or returnServerError('Could not request F-Droid.');
|
||||||
|
|
||||||
// targetting the corresponding widget based on user selection
|
// targetting the corresponding widget based on user selection
|
||||||
// "updated" is the 4th widget on the page, "added" is the 5th
|
// "updated" is the 5th widget on the page, "added" is the 6th
|
||||||
|
|
||||||
switch($this->getInput('u')) {
|
switch($this->getInput('u')) {
|
||||||
case 'updated':
|
case 'updated':
|
||||||
$html_widget = $html->find('div.sidebar-widget', 4);
|
$html_widget = $html->find('div.sidebar-widget', 5);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$html_widget = $html->find('div.sidebar-widget', 5);
|
$html_widget = $html->find('div.sidebar-widget', 6);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -219,8 +219,7 @@ class FacebookBridge extends BridgeAbstract {
|
||||||
$ogtitle = $html->find('meta[property="og:title"]', 0)
|
$ogtitle = $html->find('meta[property="og:title"]', 0)
|
||||||
or returnServerError('Unable to find group title!');
|
or returnServerError('Unable to find group title!');
|
||||||
|
|
||||||
return htmlspecialchars_decode($ogtitle->content, ENT_QUOTES);
|
return html_entity_decode($ogtitle->content, ENT_QUOTES);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function extractGroupURI($post) {
|
private function extractGroupURI($post) {
|
||||||
|
|
|
@ -11,7 +11,6 @@ class FeedExpanderExampleBridge extends FeedExpander {
|
||||||
'version' => array(
|
'version' => array(
|
||||||
'name' => 'Version',
|
'name' => 'Version',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Select your feed format/version',
|
'title' => 'Select your feed format/version',
|
||||||
'defaultValue' => 'RSS 2.0',
|
'defaultValue' => 'RSS 2.0',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
|
|
|
@ -62,10 +62,10 @@ class FindACrewBridge extends BridgeAbstract {
|
||||||
foreach ($annonces as $annonce) {
|
foreach ($annonces as $annonce) {
|
||||||
$item = array();
|
$item = array();
|
||||||
|
|
||||||
$img = parent::getURI() . $annonce->find('.css_LstPic img', 0)->getAttribute('src');
|
$img = parent::getURI() . $annonce->find('.lst-pic img', 0)->getAttribute('src');
|
||||||
$item['title'] = $annonce->find('.css_LstCtrls span', 0)->plaintext;
|
$item['title'] = $annonce->find('.lst-tags span', 0)->plaintext;
|
||||||
$item['uri'] = parent::getURI() . $annonce->find('.css_PnlCtrls a', 0)->href;
|
$item['uri'] = parent::getURI() . $annonce->find('.lst-ctrls a', 0)->href;
|
||||||
$content = $annonce->find('.css_LstDtl div', 2)->innertext;
|
$content = $annonce->find('.lst-dtl', 0)->innertext;
|
||||||
$item['content'] = "<img src='$img' /><br>$content";
|
$item['content'] = "<img src='$img' /><br>$content";
|
||||||
$item['enclosures'] = array($img);
|
$item['enclosures'] = array($img);
|
||||||
$item['categories'] = array($annonce->find('.css_AccLocCur', 0)->plaintext);
|
$item['categories'] = array($annonce->find('.css_AccLocCur', 0)->plaintext);
|
||||||
|
|
|
@ -10,7 +10,6 @@ class GBAtempBridge extends BridgeAbstract {
|
||||||
'type' => array(
|
'type' => array(
|
||||||
'name' => 'Type',
|
'name' => 'Type',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'News' => 'N',
|
'News' => 'N',
|
||||||
'Reviews' => 'R',
|
'Reviews' => 'R',
|
||||||
|
|
|
@ -24,7 +24,7 @@ class GithubSearchBridge extends BridgeAbstract {
|
||||||
$html = getSimpleHTMLDOM($url)
|
$html = getSimpleHTMLDOM($url)
|
||||||
or returnServerError('Error while downloading the website content');
|
or returnServerError('Error while downloading the website content');
|
||||||
|
|
||||||
foreach($html->find('div.repo-list-item') as $element) {
|
foreach($html->find('li.repo-list-item') as $element) {
|
||||||
$item = array();
|
$item = array();
|
||||||
|
|
||||||
$uri = $element->find('h3 a', 0)->href;
|
$uri = $element->find('h3 a', 0)->href;
|
||||||
|
|
88
bridges/GlowficBridge.php
Normal file
88
bridges/GlowficBridge.php
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<?php
|
||||||
|
class GlowficBridge extends BridgeAbstract {
|
||||||
|
const MAINTAINER = 'l1n';
|
||||||
|
const NAME = 'Glowfic Bridge';
|
||||||
|
const URI = 'https://www.glowfic.com';
|
||||||
|
const CACHE_TIMEOUT = 3600; // 1 hour
|
||||||
|
const DESCRIPTION = 'Returns the latest replies on a glowfic post.';
|
||||||
|
const PARAMETERS = array(
|
||||||
|
'global' => array(),
|
||||||
|
'Thread' => array(
|
||||||
|
'post_id' => array(
|
||||||
|
'name' => 'Post ID',
|
||||||
|
'title' => 'https://www.glowfic.com/posts/<POST ID>',
|
||||||
|
'type' => 'number'
|
||||||
|
),
|
||||||
|
'start_page' => array(
|
||||||
|
'name' => 'Start Page',
|
||||||
|
'title' => 'To start from an offset page',
|
||||||
|
'type' => 'number'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
public function collectData() {
|
||||||
|
$url = $this->getAPIURI();
|
||||||
|
$metadata = get_headers( $url . '/replies', true ) or returnClientError('Post did not return reply headers.');
|
||||||
|
$metadata['Last-Page'] = ceil( $metadata['Total'] / $metadata['Per-Page'] );
|
||||||
|
if(!is_null($this->getInput('start_page')) &&
|
||||||
|
$this->getInput('start_page') < 1 && $metadata['Last-Page'] - $this->getInput('start_page') > 0) {
|
||||||
|
$first_page = $metadata['Last-Page'] - $this->getInput('start_page');
|
||||||
|
} else if(!is_null($this->getInput('start_page')) && $this->getInput('start_page') <= $metadata['Last-Page']) {
|
||||||
|
$first_page = $this->getInput('start_page');
|
||||||
|
} else {
|
||||||
|
$first_page = 1;
|
||||||
|
}
|
||||||
|
for ($page_offset = $first_page; $page_offset <= $metadata['Last-Page']; $page_offset++) {
|
||||||
|
$jsonContents = getContents($url . '/replies?page=' . $page_offset ) or
|
||||||
|
returnClientError('Could not retrieve replies for page ' . $page_offset . '.');
|
||||||
|
$replies = json_decode($jsonContents);
|
||||||
|
foreach ($replies as $reply) {
|
||||||
|
$item = array();
|
||||||
|
|
||||||
|
$item['content'] = $reply->{'content'};
|
||||||
|
$item['uri'] = $this->getURI() . '?page=' . $page_offset . '#reply-' . $reply->{'id'};
|
||||||
|
if ($reply->{'icon'}) {
|
||||||
|
$item['enclosures'] = array($reply->{'icon'}->{'url'});
|
||||||
|
}
|
||||||
|
$item['author'] = $reply->{'character'}->{'screenname'} . ' (' . $reply->{'character'}->{'name'} . ')';
|
||||||
|
$item['timestamp'] = date('r', strtotime($reply->{'created_at'}));
|
||||||
|
$item['title'] = 'Tag by ' . $reply->{'user'}->{'username'} . ' updated at ' . $reply->{'updated_at'};
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAPIURI() {
|
||||||
|
$url = parent::getURI() . '/api/v1/posts/' . $this->getInput('post_id');
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getURI() {
|
||||||
|
$url = parent::getURI() . '/posts/' . $this->getInput('post_id');
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPost() {
|
||||||
|
$url = $this->getAPIURI();
|
||||||
|
$jsonPost = getContents( $url ) or returnClientError('Could not retrieve post metadata.');
|
||||||
|
$post = json_decode($jsonPost);
|
||||||
|
return $post;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(){
|
||||||
|
if(!is_null($this->getInput('post_id'))) {
|
||||||
|
$post = $this->getPost();
|
||||||
|
return $post->{'subject'} . ' - ' . parent::getName();
|
||||||
|
}
|
||||||
|
return parent::getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(){
|
||||||
|
if(!is_null($this->getInput('post_id'))) {
|
||||||
|
$post = $this->getPost();
|
||||||
|
return $post->{'content'};
|
||||||
|
}
|
||||||
|
return parent::getName();
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,13 +16,13 @@ class HDWallpapersBridge extends BridgeAbstract {
|
||||||
),
|
),
|
||||||
'r' => array(
|
'r' => array(
|
||||||
'name' => 'resolution',
|
'name' => 'resolution',
|
||||||
'defaultValue' => '1920x1200',
|
'defaultValue' => 'HD',
|
||||||
'exampleValue' => '1920x1200, 1680x1050,…'
|
'exampleValue' => 'HD, 1920x1200, 1680x1050,…'
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
|
|
||||||
public function collectData(){
|
public function collectData(){
|
||||||
$category = $this->category;
|
$category = $this->getInput('c');
|
||||||
if(strrpos($category, 'wallpapers') !== strlen($category) - strlen('wallpapers')) {
|
if(strrpos($category, 'wallpapers') !== strlen($category) - strlen('wallpapers')) {
|
||||||
$category .= '-desktop-wallpapers';
|
$category .= '-desktop-wallpapers';
|
||||||
}
|
}
|
||||||
|
@ -45,13 +45,12 @@ class HDWallpapersBridge extends BridgeAbstract {
|
||||||
$thumbnail = $element->find('img', 0);
|
$thumbnail = $element->find('img', 0);
|
||||||
|
|
||||||
$item = array();
|
$item = array();
|
||||||
// http://www.hdwallpapers.in/download/yosemite_reflections-1680x1050.jpg
|
|
||||||
$item['uri'] = self::URI
|
$item['uri'] = self::URI
|
||||||
. '/download'
|
. '/download'
|
||||||
. str_replace('wallpapers.html', $this->getInput('r') . '.jpg', $element->href);
|
. str_replace('wallpapers.html', $this->getInput('r') . '.jpg', $element->href);
|
||||||
|
|
||||||
$item['timestamp'] = time();
|
$item['timestamp'] = time();
|
||||||
$item['title'] = $element->find('p', 0)->text();
|
$item['title'] = $element->find('em1', 0)->text();
|
||||||
$item['content'] = $item['title']
|
$item['content'] = $item['title']
|
||||||
. '<br><a href="'
|
. '<br><a href="'
|
||||||
. $item['uri']
|
. $item['uri']
|
||||||
|
@ -60,6 +59,7 @@ class HDWallpapersBridge extends BridgeAbstract {
|
||||||
. $thumbnail->src
|
. $thumbnail->src
|
||||||
. '" /></a>';
|
. '" /></a>';
|
||||||
|
|
||||||
|
$item['enclosures'] = array($item['uri']);
|
||||||
$this->items[] = $item;
|
$this->items[] = $item;
|
||||||
|
|
||||||
$num++;
|
$num++;
|
||||||
|
|
75
bridges/HeiseBridge.php
Normal file
75
bridges/HeiseBridge.php
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class HeiseBridge extends FeedExpander {
|
||||||
|
const MAINTAINER = 'Dreckiger-Dan';
|
||||||
|
const NAME = 'Heise Online Bridge';
|
||||||
|
const URI = 'https://heise.de/';
|
||||||
|
const CACHE_TIMEOUT = 1800; // 30min
|
||||||
|
const DESCRIPTION = 'Returns the full articles instead of only the intro';
|
||||||
|
const PARAMETERS = array(array(
|
||||||
|
'category' => array(
|
||||||
|
'name' => 'Category',
|
||||||
|
'type' => 'list',
|
||||||
|
'values' => array(
|
||||||
|
'Alle News'
|
||||||
|
=> 'https://www.heise.de/newsticker/heise-atom.xml',
|
||||||
|
'Top-News'
|
||||||
|
=> 'https://www.heise.de/newsticker/heise-top-atom.xml',
|
||||||
|
'Internet-Störungen'
|
||||||
|
=> 'https://www.heise.de/netze/netzwerk-tools/imonitor-internet-stoerungen/feed/aktuelle-meldungen/',
|
||||||
|
'Alle News von heise Developer'
|
||||||
|
=> 'https://www.heise.de/developer/rss/news-atom.xml'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'limit' => array(
|
||||||
|
'name' => 'Limit',
|
||||||
|
'type' => 'number',
|
||||||
|
'required' => false,
|
||||||
|
'title' => 'Specify number of full articles to return',
|
||||||
|
'defaultValue' => 5
|
||||||
|
)
|
||||||
|
));
|
||||||
|
const LIMIT = 5;
|
||||||
|
|
||||||
|
public function collectData() {
|
||||||
|
$this->collectExpandableDatas(
|
||||||
|
$this->getInput('category'),
|
||||||
|
$this->getInput('limit') ?: static::LIMIT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseItem($feedItem) {
|
||||||
|
$item = parent::parseItem($feedItem);
|
||||||
|
$uri = $item['uri'];
|
||||||
|
|
||||||
|
do {
|
||||||
|
$article = getSimpleHTMLDOMCached($uri)
|
||||||
|
or returnServerError('Could not open article: ' . $uri);
|
||||||
|
|
||||||
|
$article = defaultLinkTo($article, $uri);
|
||||||
|
$item = $this->addArticleToItem($item, $article);
|
||||||
|
|
||||||
|
if($next = $article->find('.pagination a[rel="next"]', 0))
|
||||||
|
$uri = $next->href;
|
||||||
|
} while ($next);
|
||||||
|
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addArticleToItem($item, $article) {
|
||||||
|
if($author = $article->find('[itemprop="author"]', 0))
|
||||||
|
$item['author'] = $author->plaintext;
|
||||||
|
|
||||||
|
$content = $article->find('div[class*="article-content"]', 0);
|
||||||
|
|
||||||
|
foreach($content->find('p, h3, ul, table, pre, img') as $element) {
|
||||||
|
$item['content'] .= $element;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($content->find('img') as $img) {
|
||||||
|
$item['enclosures'][] = $img->src;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,13 +17,11 @@ class HotUKDealsBridge extends PepperBridgeAbstract {
|
||||||
'hide_expired' => array(
|
'hide_expired' => array(
|
||||||
'name' => 'Hide expired deals',
|
'name' => 'Hide expired deals',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'required' => true
|
|
||||||
),
|
),
|
||||||
'hide_local' => array(
|
'hide_local' => array(
|
||||||
'name' => 'Hide local deals',
|
'name' => 'Hide local deals',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'title' => 'Hide deals in physical store',
|
'title' => 'Hide deals in physical store',
|
||||||
'required' => true
|
|
||||||
),
|
),
|
||||||
'priceFrom' => array(
|
'priceFrom' => array(
|
||||||
'name' => 'Minimal Price',
|
'name' => 'Minimal Price',
|
||||||
|
@ -43,7 +41,6 @@ class HotUKDealsBridge extends PepperBridgeAbstract {
|
||||||
'group' => array(
|
'group' => array(
|
||||||
'name' => 'Group',
|
'name' => 'Group',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Group whose deals must be displayed',
|
'title' => 'Group whose deals must be displayed',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'2DS' => '2ds',
|
'2DS' => '2ds',
|
||||||
|
@ -1317,7 +1314,6 @@ class HotUKDealsBridge extends PepperBridgeAbstract {
|
||||||
'order' => array(
|
'order' => array(
|
||||||
'name' => 'Order by',
|
'name' => 'Order by',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Sort order of deals',
|
'title' => 'Sort order of deals',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'From the most to the least hot deal' => '-hot',
|
'From the most to the least hot deal' => '-hot',
|
||||||
|
|
|
@ -89,7 +89,7 @@ class InstagramBridge extends BridgeAbstract {
|
||||||
if (isset($media->edge_media_to_caption->edges[0]->node->text)) {
|
if (isset($media->edge_media_to_caption->edges[0]->node->text)) {
|
||||||
$textContent = $media->edge_media_to_caption->edges[0]->node->text;
|
$textContent = $media->edge_media_to_caption->edges[0]->node->text;
|
||||||
} else {
|
} else {
|
||||||
$textContent = basename($media->display_url);
|
$textContent = '(no text)';
|
||||||
}
|
}
|
||||||
|
|
||||||
$item['title'] = ($media->is_video ? '▶ ' : '') . trim($textContent);
|
$item['title'] = ($media->is_video ? '▶ ' : '') . trim($textContent);
|
||||||
|
@ -103,10 +103,11 @@ class InstagramBridge extends BridgeAbstract {
|
||||||
$item['content'] = $data[0];
|
$item['content'] = $data[0];
|
||||||
$item['enclosures'] = $data[1];
|
$item['enclosures'] = $data[1];
|
||||||
} else {
|
} else {
|
||||||
|
$mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l';
|
||||||
$item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
|
$item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
|
||||||
$item['content'] .= '<img src="' . htmlentities($media->display_url) . '" alt="' . $item['title'] . '" />';
|
$item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />';
|
||||||
$item['content'] .= '</a><br><br>' . nl2br(htmlentities($textContent));
|
$item['content'] .= '</a><br><br>' . nl2br(htmlentities($textContent));
|
||||||
$item['enclosures'] = array($media->display_url);
|
$item['enclosures'] = array($mediaURI);
|
||||||
}
|
}
|
||||||
|
|
||||||
$item['timestamp'] = $media->taken_at_timestamp;
|
$item['timestamp'] = $media->taken_at_timestamp;
|
||||||
|
|
|
@ -21,7 +21,6 @@ class InstructablesBridge extends BridgeAbstract {
|
||||||
'category' => array(
|
'category' => array(
|
||||||
'name' => 'Category',
|
'name' => 'Category',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Play' => array(
|
'Play' => array(
|
||||||
'All' => '/play/',
|
'All' => '/play/',
|
||||||
|
@ -240,7 +239,6 @@ class InstructablesBridge extends BridgeAbstract {
|
||||||
'filter' => array(
|
'filter' => array(
|
||||||
'name' => 'Filter',
|
'name' => 'Filter',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Featured' => ' ',
|
'Featured' => ' ',
|
||||||
'Recent' => 'recent/',
|
'Recent' => 'recent/',
|
||||||
|
|
128
bridges/IvooxBridge.php
Normal file
128
bridges/IvooxBridge.php
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* IvooxRssBridge
|
||||||
|
* Returns the latest search result
|
||||||
|
* TODO: support podcast episodes list
|
||||||
|
*/
|
||||||
|
class IvooxBridge extends BridgeAbstract {
|
||||||
|
const NAME = 'Ivoox Bridge';
|
||||||
|
const URI = 'https://www.ivoox.com/';
|
||||||
|
const CACHE_TIMEOUT = 10800; // 3h
|
||||||
|
const DESCRIPTION = 'Returns the 10 newest episodes by keyword search';
|
||||||
|
const MAINTAINER = 'xurxof'; // based on YoutubeBridge by mitsukarenai
|
||||||
|
const PARAMETERS = array(
|
||||||
|
'Search result' => array(
|
||||||
|
's' => array(
|
||||||
|
'name' => 'keyword',
|
||||||
|
'exampleValue' => 'test'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private function ivBridgeAddItem(
|
||||||
|
$episode_link,
|
||||||
|
$podcast_name,
|
||||||
|
$episode_title,
|
||||||
|
$author_name,
|
||||||
|
$episode_description,
|
||||||
|
$publication_date,
|
||||||
|
$episode_duration) {
|
||||||
|
$item = array();
|
||||||
|
$item['title'] = htmlspecialchars_decode($podcast_name . ': ' . $episode_title);
|
||||||
|
$item['author'] = $author_name;
|
||||||
|
$item['timestamp'] = $publication_date;
|
||||||
|
$item['uri'] = $episode_link;
|
||||||
|
$item['content'] = '<a href="' . $episode_link . '">' . $podcast_name . ': ' . $episode_title
|
||||||
|
. '</a><br />Duration: ' . $episode_duration
|
||||||
|
. '<br />Description:<br />' . $episode_description;
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ivBridgeParseHtmlListing($html) {
|
||||||
|
$limit = 4;
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach($html->find('div.flip-container') as $flipper) {
|
||||||
|
$linkcount = 0;
|
||||||
|
if(!empty($flipper->find( 'div.modulo-type-banner' ))) {
|
||||||
|
// ad
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($count < $limit) {
|
||||||
|
foreach($flipper->find('div.header-modulo') as $element) {
|
||||||
|
foreach($element->find('a') as $link) {
|
||||||
|
if ($linkcount == 0) {
|
||||||
|
$episode_link = $link->href;
|
||||||
|
$episode_title = $link->title;
|
||||||
|
} elseif ($linkcount == 1) {
|
||||||
|
$author_link = $link->href;
|
||||||
|
$author_name = $link->title;
|
||||||
|
} elseif ($linkcount == 2) {
|
||||||
|
$podcast_link = $link->href;
|
||||||
|
$podcast_name = $link->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
$linkcount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$episode_description = $flipper->find('button.btn-link', 0)->getAttribute('data-content');
|
||||||
|
$episode_duration = $flipper->find('p.time', 0)->innertext;
|
||||||
|
$publication_date = $flipper->find('li.date', 0)->getAttribute('title');
|
||||||
|
|
||||||
|
// alternative date_parse_from_format
|
||||||
|
// or DateTime::createFromFormat('G:i - d \d\e M \d\e Y', $publication);
|
||||||
|
// TODO: month name translations, due function doesn't support locale
|
||||||
|
|
||||||
|
$a = strptime($publication_date, '%H:%M - %d de %b. de %Y'); // obsolete function, uses c libraries
|
||||||
|
$publication_date = mktime(0, 0, 0, $a['tm_mon'] + 1, $a['tm_mday'], $a['tm_year'] + 1900);
|
||||||
|
|
||||||
|
$this->ivBridgeAddItem(
|
||||||
|
$episode_link,
|
||||||
|
$podcast_name,
|
||||||
|
$episode_title,
|
||||||
|
$author_name,
|
||||||
|
$episode_description,
|
||||||
|
$publication_date,
|
||||||
|
$episode_duration
|
||||||
|
);
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collectData() {
|
||||||
|
|
||||||
|
// store locale, change to spanish
|
||||||
|
$originalLocales = explode(';', setlocale(LC_ALL, 0));
|
||||||
|
setlocale(LC_ALL, 'es_ES.utf8');
|
||||||
|
|
||||||
|
$xml = '';
|
||||||
|
$html = '';
|
||||||
|
$url_feed = '';
|
||||||
|
if($this->getInput('s')) { /* Search modes */
|
||||||
|
$this->request = str_replace(' ', '-', $this->getInput('s'));
|
||||||
|
$url_feed = self::URI . urlencode($this->request) . '_sb_f_1.html?o=uploaddate';
|
||||||
|
} else {
|
||||||
|
returnClientError('Not valid mode at IvooxBridge');
|
||||||
|
}
|
||||||
|
|
||||||
|
$dom = getSimpleHTMLDOM($url_feed)
|
||||||
|
or returnServerError('Could not request ' . $url_feed);
|
||||||
|
$this->ivBridgeParseHtmlListing($dom);
|
||||||
|
|
||||||
|
// restore locale
|
||||||
|
|
||||||
|
foreach($originalLocales as $localeSetting) {
|
||||||
|
if(strpos($localeSetting, '=') !== false) {
|
||||||
|
list($category, $locale) = explode('=', $localeSetting);
|
||||||
|
} else {
|
||||||
|
$category = LC_ALL;
|
||||||
|
$locale = $localeSetting;
|
||||||
|
}
|
||||||
|
|
||||||
|
setlocale($category, $locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,6 @@ class JustETFBridge extends BridgeAbstract {
|
||||||
'global' => array(
|
'global' => array(
|
||||||
'lang' => array(
|
'lang' => array(
|
||||||
'name' => 'Language',
|
'name' => 'Language',
|
||||||
'required' => true,
|
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Englisch' => 'en',
|
'Englisch' => 'en',
|
||||||
|
|
|
@ -11,7 +11,6 @@ class KununuBridge extends BridgeAbstract {
|
||||||
'site' => array(
|
'site' => array(
|
||||||
'name' => 'Site',
|
'name' => 'Site',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Select your site',
|
'title' => 'Select your site',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Austria' => 'at',
|
'Austria' => 'at',
|
||||||
|
@ -23,7 +22,6 @@ class KununuBridge extends BridgeAbstract {
|
||||||
'full' => array(
|
'full' => array(
|
||||||
'name' => 'Load full article',
|
'name' => 'Load full article',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'required' => false,
|
|
||||||
'exampleValue' => 'checked',
|
'exampleValue' => 'checked',
|
||||||
'title' => 'Activate to load full article'
|
'title' => 'Activate to load full article'
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,12 +20,13 @@ class LeMondeInformatiqueBridge extends FeedExpander {
|
||||||
str_replace(
|
str_replace(
|
||||||
'/grande/',
|
'/grande/',
|
||||||
'/petite/',
|
'/petite/',
|
||||||
$article_html->find('.article-image', 0)->find('img', 0)->src
|
$article_html->find('.article-image > img, figure > img', 0)->src
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
//No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail
|
//No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail
|
||||||
$item['content'] = utf8_encode($this->cleanArticle($article_html->find('div.col-primary', 0)->innertext));
|
$content_node = $article_html->find('div.col-primary, div.col-sm-9', 0);
|
||||||
|
$item['content'] = utf8_encode($this->cleanArticle($content_node->innertext));
|
||||||
$item['author'] = utf8_encode($article_html->find('div.author-infos', 0)->find('b', 0)->plaintext);
|
$item['author'] = utf8_encode($article_html->find('div.author-infos', 0)->find('b', 0)->plaintext);
|
||||||
|
|
||||||
return $item;
|
return $item;
|
||||||
|
|
|
@ -13,7 +13,6 @@ class MangareaderBridge extends BridgeAbstract {
|
||||||
'category' => array(
|
'category' => array(
|
||||||
'name' => 'Category',
|
'name' => 'Category',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'All' => 'all',
|
'All' => 'all',
|
||||||
'Action' => 'action',
|
'Action' => 'action',
|
||||||
|
|
153
bridges/MozillaBugTrackerBridge.php
Normal file
153
bridges/MozillaBugTrackerBridge.php
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
<?php
|
||||||
|
class MozillaBugTrackerBridge extends BridgeAbstract {
|
||||||
|
|
||||||
|
const NAME = 'Mozilla Bug Tracker';
|
||||||
|
const URI = 'https://bugzilla.mozilla.org';
|
||||||
|
const DESCRIPTION = 'Returns feeds for bug comments';
|
||||||
|
const MAINTAINER = 'AntoineTurmel';
|
||||||
|
const PARAMETERS = array(
|
||||||
|
'Bug comments' => array(
|
||||||
|
'id' => array(
|
||||||
|
'name' => 'Bug tracking ID',
|
||||||
|
'type' => 'number',
|
||||||
|
'required' => true,
|
||||||
|
'title' => 'Insert bug tracking ID',
|
||||||
|
'exampleValue' => 121241
|
||||||
|
),
|
||||||
|
'limit' => array(
|
||||||
|
'name' => 'Number of comments to return',
|
||||||
|
'type' => 'number',
|
||||||
|
'required' => false,
|
||||||
|
'title' => 'Specify number of comments to return',
|
||||||
|
'defaultValue' => -1
|
||||||
|
),
|
||||||
|
'sorting' => array(
|
||||||
|
'name' => 'Sorting',
|
||||||
|
'type' => 'list',
|
||||||
|
'required' => false,
|
||||||
|
'title' => 'Defines the sorting order of the comments returned',
|
||||||
|
'defaultValue' => 'of',
|
||||||
|
'values' => array(
|
||||||
|
'Oldest first' => 'of',
|
||||||
|
'Latest first' => 'lf'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private $bugid = '';
|
||||||
|
private $bugdesc = '';
|
||||||
|
|
||||||
|
public function getIcon() {
|
||||||
|
return self::URI . '/extensions/BMO/web/images/favicon.ico';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collectData(){
|
||||||
|
$limit = $this->getInput('limit');
|
||||||
|
$sorting = $this->getInput('sorting');
|
||||||
|
|
||||||
|
// We use the print preview page for simplicity
|
||||||
|
$html = getSimpleHTMLDOMCached($this->getURI() . '&format=multiple',
|
||||||
|
86400,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
DEFAULT_TARGET_CHARSET,
|
||||||
|
false, // Do NOT remove line breaks
|
||||||
|
DEFAULT_BR_TEXT,
|
||||||
|
DEFAULT_SPAN_TEXT);
|
||||||
|
|
||||||
|
if($html === false)
|
||||||
|
returnServerError('Failed to load page!');
|
||||||
|
|
||||||
|
// Store header information into private members
|
||||||
|
$this->bugid = $html->find('#bugzilla-body', 0)->find('a', 0)->innertext;
|
||||||
|
$this->bugdesc = $html->find('table.bugfields', 0)->find('tr', 0)->find('td', 0)->innertext;
|
||||||
|
|
||||||
|
// Get and limit comments
|
||||||
|
$comments = $html->find('.bz_comment_table div.bz_comment');
|
||||||
|
|
||||||
|
if($limit > 0 && count($comments) > $limit) {
|
||||||
|
$comments = array_slice($comments, count($comments) - $limit, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order comments
|
||||||
|
switch($sorting) {
|
||||||
|
case 'lf': $comments = array_reverse($comments, true);
|
||||||
|
case 'of':
|
||||||
|
default: // Nothing to do, keep original order
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($comments as $comment) {
|
||||||
|
$comment = $this->inlineStyles($comment);
|
||||||
|
|
||||||
|
$item = array();
|
||||||
|
$item['uri'] = $this->getURI() . '#' . $comment->id;
|
||||||
|
$item['author'] = $comment->find('span.bz_comment_user', 0)->innertext;
|
||||||
|
$item['title'] = $comment->find('span.bz_comment_number', 0)->find('a', 0)->innertext;
|
||||||
|
$item['timestamp'] = strtotime($comment->find('span.bz_comment_time', 0)->innertext);
|
||||||
|
$item['content'] = $comment->find('pre.bz_comment_text', 0)->innertext;
|
||||||
|
|
||||||
|
// Fix line breaks (they use LF)
|
||||||
|
$item['content'] = str_replace("\n", '<br>', $item['content']);
|
||||||
|
|
||||||
|
// Fix relative URIs
|
||||||
|
$item['content'] = $this->replaceRelativeURI($item['content']);
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getURI(){
|
||||||
|
switch($this->queriedContext) {
|
||||||
|
case 'Bug comments':
|
||||||
|
return parent::getURI()
|
||||||
|
. '/show_bug.cgi?id='
|
||||||
|
. $this->getInput('id');
|
||||||
|
break;
|
||||||
|
default: return parent::getURI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(){
|
||||||
|
switch($this->queriedContext) {
|
||||||
|
case 'Bug comments':
|
||||||
|
return 'Bug '
|
||||||
|
. $this->bugid
|
||||||
|
. ' tracker for '
|
||||||
|
. $this->bugdesc
|
||||||
|
. ' - '
|
||||||
|
. parent::getName();
|
||||||
|
break;
|
||||||
|
default: return parent::getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces all relative URIs with absolute ones
|
||||||
|
*
|
||||||
|
* @param string $content The source string
|
||||||
|
* @return string Returns the source string with all relative URIs replaced
|
||||||
|
* by absolute ones.
|
||||||
|
*/
|
||||||
|
private function replaceRelativeURI($content){
|
||||||
|
return preg_replace('/href="(?!http)/', 'href="' . self::URI . '/', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds styles as attributes to tags with known classes
|
||||||
|
*
|
||||||
|
* @param object $html A simplehtmldom object
|
||||||
|
* @return object Returns the original object with styles added as
|
||||||
|
* attributes.
|
||||||
|
*/
|
||||||
|
private function inlineStyles($html){
|
||||||
|
foreach($html->find('.bz_obsolete') as $element) {
|
||||||
|
$element->style = 'text-decoration:line-through;';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,8 @@ class MozillaSecurityBridge extends BridgeAbstract {
|
||||||
$item['title'] = $element->innertext;
|
$item['title'] = $element->innertext;
|
||||||
$item['timestamp'] = strtotime($element->innertext);
|
$item['timestamp'] = strtotime($element->innertext);
|
||||||
$item['content'] = $element->next_sibling()->innertext;
|
$item['content'] = $element->next_sibling()->innertext;
|
||||||
$item['uri'] = self::URI;
|
$item['uri'] = self::URI . '?' . $item['timestamp'];
|
||||||
|
$item['uid'] = self::URI . '?' . $item['timestamp'];
|
||||||
$this->items[] = $item;
|
$this->items[] = $item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,11 @@ class MydealsBridge extends PepperBridgeAbstract {
|
||||||
'hide_expired' => array(
|
'hide_expired' => array(
|
||||||
'name' => 'Abgelaufenes ausblenden',
|
'name' => 'Abgelaufenes ausblenden',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'required' => true
|
|
||||||
),
|
),
|
||||||
'hide_local' => array(
|
'hide_local' => array(
|
||||||
'name' => 'Lokales ausblenden',
|
'name' => 'Lokales ausblenden',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'title' => 'Deals im physischen Geschäft ausblenden',
|
'title' => 'Deals im physischen Geschäft ausblenden',
|
||||||
'required' => true
|
|
||||||
),
|
),
|
||||||
'priceFrom' => array(
|
'priceFrom' => array(
|
||||||
'name' => 'Minimaler Preis',
|
'name' => 'Minimaler Preis',
|
||||||
|
@ -43,7 +41,6 @@ class MydealsBridge extends PepperBridgeAbstract {
|
||||||
'group' => array(
|
'group' => array(
|
||||||
'name' => 'Gruppen',
|
'name' => 'Gruppen',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Gruppe, deren Deals angezeigt werden müssen',
|
'title' => 'Gruppe, deren Deals angezeigt werden müssen',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Elektronik' => 'elektronik',
|
'Elektronik' => 'elektronik',
|
||||||
|
@ -66,7 +63,6 @@ class MydealsBridge extends PepperBridgeAbstract {
|
||||||
'order' => array(
|
'order' => array(
|
||||||
'name' => 'sortieren nach',
|
'name' => 'sortieren nach',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Sortierung der deals',
|
'title' => 'Sortierung der deals',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Vom heißesten zum kältesten Deal' => '',
|
'Vom heißesten zum kältesten Deal' => '',
|
||||||
|
|
|
@ -11,7 +11,6 @@ class NineGagBridge extends BridgeAbstract {
|
||||||
'd' => array(
|
'd' => array(
|
||||||
'name' => 'Section',
|
'name' => 'Section',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Hot' => 'hot',
|
'Hot' => 'hot',
|
||||||
'Trending' => 'trending',
|
'Trending' => 'trending',
|
||||||
|
@ -28,7 +27,6 @@ class NineGagBridge extends BridgeAbstract {
|
||||||
'g' => array(
|
'g' => array(
|
||||||
'name' => 'Section',
|
'name' => 'Section',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Animals' => 'cute',
|
'Animals' => 'cute',
|
||||||
'Anime & Manga' => 'anime-manga',
|
'Anime & Manga' => 'anime-manga',
|
||||||
|
@ -88,7 +86,6 @@ class NineGagBridge extends BridgeAbstract {
|
||||||
't' => array(
|
't' => array(
|
||||||
'name' => 'Type',
|
'name' => 'Type',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Hot' => 'hot',
|
'Hot' => 'hot',
|
||||||
'Fresh' => 'fresh',
|
'Fresh' => 'fresh',
|
||||||
|
|
|
@ -21,8 +21,7 @@ class NotAlwaysBridge extends BridgeAbstract {
|
||||||
'Friendly' => 'friendly',
|
'Friendly' => 'friendly',
|
||||||
'Hopeless' => 'hopeless',
|
'Hopeless' => 'hopeless',
|
||||||
'Unfiltered' => 'unfiltered'
|
'Unfiltered' => 'unfiltered'
|
||||||
),
|
)
|
||||||
'required' => true
|
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ class OnVaSortirBridge extends FeedExpander {
|
||||||
'city' => array(
|
'city' => array(
|
||||||
'name' => 'City',
|
'name' => 'City',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Agen' => 'Agen',
|
'Agen' => 'Agen',
|
||||||
'Ajaccio' => 'Ajaccio',
|
'Ajaccio' => 'Ajaccio',
|
||||||
|
|
|
@ -35,25 +35,33 @@ class OneFortuneADayBridge extends BridgeAbstract {
|
||||||
'23:00' => 23,
|
'23:00' => 23,
|
||||||
),
|
),
|
||||||
'defaultValue' => 5
|
'defaultValue' => 5
|
||||||
|
),
|
||||||
|
'lucky' => array(
|
||||||
|
'name' => 'Lucky number (optional)',
|
||||||
|
'type' => 'text'
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
|
|
||||||
const LIMIT_ITEMS = 7;
|
const LIMIT_ITEMS = 7;
|
||||||
const DAY_SECS = 86400;
|
const DAY_SECS = 86400;
|
||||||
|
|
||||||
|
public function getDescription(){
|
||||||
|
return self::DESCRIPTION . '<br/>Set a lucky number to get your personal quotes, like ' . mt_rand();
|
||||||
|
}
|
||||||
|
|
||||||
public function collectData() {
|
public function collectData() {
|
||||||
$time = gmmktime((int)$this->getInput('time'), 0, 0);
|
$time = gmmktime((int)$this->getInput('time'), 0, 0);
|
||||||
if ($time > time())
|
if ($time > time())
|
||||||
$time -= self::DAY_SECS;
|
$time -= self::DAY_SECS;
|
||||||
|
|
||||||
for ($i = self::LIMIT_ITEMS; $i > 0; --$i) {
|
for ($i = self::LIMIT_ITEMS; $i > 0; --$i) {
|
||||||
$seed = date('Ymd', $time);
|
$seed = gmdate('Ymd', $time) . $this->getInput('lucky');
|
||||||
$quote = $this->getQuote($seed);
|
$quote = $this->getQuote($seed);
|
||||||
|
|
||||||
$item['title'] = strftime('%A, %x', $time);
|
$item['title'] = strftime('%A, %x', $time);
|
||||||
$item['content'] = htmlentities($quote, ENT_QUOTES, 'UTF-8');
|
$item['content'] = htmlentities($quote, ENT_QUOTES, 'UTF-8');
|
||||||
$item['timestamp'] = $time;
|
$item['timestamp'] = $time;
|
||||||
$item['uri'] = 'urn:sha1:' . hash('sha1', $seed);
|
$item['uid'] = hash('sha1', $seed);
|
||||||
|
|
||||||
$this->items[] = $item;
|
$this->items[] = $item;
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ class OpenClassroomsBridge extends BridgeAbstract {
|
||||||
'u' => array(
|
'u' => array(
|
||||||
'name' => 'Catégorie',
|
'name' => 'Catégorie',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Arts & Culture' => 'arts',
|
'Arts & Culture' => 'arts',
|
||||||
'Code' => 'code',
|
'Code' => 'code',
|
||||||
|
|
|
@ -1,34 +1,88 @@
|
||||||
<?php
|
<?php
|
||||||
class RadioMelodieBridge extends BridgeAbstract {
|
class RadioMelodieBridge extends BridgeAbstract {
|
||||||
const NAME = 'Radio Melodie Actu';
|
const NAME = 'Radio Melodie Actu';
|
||||||
const URI = 'https://www.radiomelodie.com/';
|
const URI = 'https://www.radiomelodie.com';
|
||||||
const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie';
|
const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie';
|
||||||
const MAINTAINER = 'sysadminstory';
|
const MAINTAINER = 'sysadminstory';
|
||||||
|
|
||||||
public function getIcon() {
|
public function getIcon() {
|
||||||
return self::URI . 'img/favicon.png';
|
return self::URI . '/img/favicon.png';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function collectData(){
|
public function collectData(){
|
||||||
$html = getSimpleHTMLDOM(self::URI . 'actu')
|
$html = getSimpleHTMLDOM(self::URI . '/actu/')
|
||||||
or returnServerError('Could not request Radio Melodie.');
|
or returnServerError('Could not request Radio Melodie.');
|
||||||
$list = $html->find('div[class=actuitem]');
|
$list = $html->find('div[class=actu_col1]', 0)->children();;
|
||||||
foreach($list as $element) {
|
foreach($list as $element) {
|
||||||
$item = array();
|
if($element->tag == 'a') {
|
||||||
|
$articleURL = self::URI . $element->href;
|
||||||
|
$article = getSimpleHTMLDOM($articleURL);
|
||||||
|
|
||||||
// Get picture URL
|
// Initialise arrays
|
||||||
$pictureHTML = $element->find('div[class=picture]');
|
$item = array();
|
||||||
preg_match(
|
$audio = array();
|
||||||
'/background-image:url\((.*)\);/',
|
$picture = array();
|
||||||
$pictureHTML[0]->getAttribute('style'),
|
|
||||||
$pictures);
|
|
||||||
$pictureURL = $pictures[1];
|
|
||||||
|
|
||||||
$item['enclosures'] = array($pictureURL);
|
// Get the Main picture URL
|
||||||
$item['uri'] = self::URI . $element->parent()->href;
|
$picture[] = $this->rewriteImage($article->find('img[id=picturearticle]', 0)->src);
|
||||||
$item['title'] = $element->find('h3', 0)->plaintext;
|
$audioHTML = $article->find('div[class=sm2-playlist-wrapper]');
|
||||||
$item['content'] = $element->find('p', 0)->plaintext . '<br/><img src="' . $pictureURL . '"/>';
|
|
||||||
$this->items[] = $item;
|
// Remove the audio placeholder under the Audio player with an <audio>
|
||||||
|
// element and add the audio element to the enclosure
|
||||||
|
foreach($audioHTML as $audioElement) {
|
||||||
|
$audioURL = $audioElement->find('a', 0)->href;
|
||||||
|
$audio[] = $audioURL;
|
||||||
|
$audioElement->outertext = '<audio controls src="' . $audioURL . '"></audio>';
|
||||||
|
$article->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite pictures URL
|
||||||
|
$imgs = $article->find('img[src^="https://www.radiomelodie.com/image.php]');
|
||||||
|
foreach($imgs as $img) {
|
||||||
|
$img->src = $this->rewriteImage($img->src);
|
||||||
|
$article->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove inline audio player HTML
|
||||||
|
$inlinePlayers = $article->find('div[class*=sm2-main-controls]');
|
||||||
|
foreach($inlinePlayers as $inlinePlayer) {
|
||||||
|
$inlinePlayer->outertext = '';
|
||||||
|
$article->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove Google Ads
|
||||||
|
$ads = $article->find('div[style^=margin:25px 0; position:relative; height:auto;]');
|
||||||
|
foreach($ads as $ad) {
|
||||||
|
$ad->outertext = '';
|
||||||
|
$article->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$author = $article->find('div[id=author]', 0)->find('span', 0)->plaintext;
|
||||||
|
|
||||||
|
$item['enclosures'] = array_merge($picture, $audio);
|
||||||
|
$item['author'] = $author;
|
||||||
|
$item['uri'] = $articleURL;
|
||||||
|
$item['title'] = $article->find('meta[property=og:title]', 0)->content;
|
||||||
|
$date_category = $article->find('div[class*=date]', 0)->plaintext;
|
||||||
|
$header = $article->find('a[class=fancybox]', 0)->innertext;
|
||||||
|
$textDOM = $article->find('div[class=text_content]', 0);
|
||||||
|
$textDOM->find('div[id=author]', 0)->outertext = '';
|
||||||
|
$article->save();
|
||||||
|
$text = $textDOM->innertext;
|
||||||
|
$item['content'] = '<h1>' . $item['title'] . '</h1>' . $date_category . $header . $text;
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Function to rewrite image URL to use the real Image URL and not the resized one (which is very slow)
|
||||||
|
*/
|
||||||
|
private function rewriteImage($url)
|
||||||
|
{
|
||||||
|
$parts = explode('?', $url);
|
||||||
|
parse_str($parts[1], $params);
|
||||||
|
return self::URI . '/' . $params['image'];
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
99
bridges/RoadAndTrackBridge.php
Normal file
99
bridges/RoadAndTrackBridge.php
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
class RoadAndTrackBridge extends BridgeAbstract {
|
||||||
|
const MAINTAINER = 'teromene';
|
||||||
|
const NAME = 'Road And Track Bridge';
|
||||||
|
const URI = 'https://www.roadandtrack.com/';
|
||||||
|
const CACHE_TIMEOUT = 86400; // 24h
|
||||||
|
const DESCRIPTION = 'Returns the latest news from Road & Track.';
|
||||||
|
|
||||||
|
const PARAMETERS = array(
|
||||||
|
array(
|
||||||
|
'new-cars' => array(
|
||||||
|
'name' => 'New Cars',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'exampleValue' => 'checked',
|
||||||
|
'title' => 'Activate to load New Cars articles'
|
||||||
|
),
|
||||||
|
'motorsports' => array(
|
||||||
|
'name' => 'Motorsports',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'exampleValue' => 'checked',
|
||||||
|
'title' => 'Activate to load Motorsports articles'
|
||||||
|
),
|
||||||
|
'car-culture' => array(
|
||||||
|
'name' => 'Car Culture',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'exampleValue' => 'checked',
|
||||||
|
'title' => 'Activate to load Car Culture articles'
|
||||||
|
),
|
||||||
|
'car-shows' => array(
|
||||||
|
'name' => 'Car shows',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'exampleValue' => 'checked',
|
||||||
|
'title' => 'Activate to load Car shows articles'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const API_TOKEN = '2e18e904-d9cd-4911-b30c-1817b1e0b04b';
|
||||||
|
const SIG_URL = 'https://cloud.mazdigital.com/feeds/production/comboapp/204/api/v3/';
|
||||||
|
const GSIG_URL = 'https://dashboard.mazsystems.com/services/cf_access?app_id=204&app_type=comboapp&api_token=';
|
||||||
|
|
||||||
|
public function collectData() {
|
||||||
|
|
||||||
|
$signVal = json_decode(getContents(self::GSIG_URL . self::API_TOKEN));
|
||||||
|
$signVal = $signVal->signature;
|
||||||
|
|
||||||
|
$newsElements = array();
|
||||||
|
if($this->getInput('new-cars')) {
|
||||||
|
$newsElements = array_merge($newsElements,
|
||||||
|
json_decode(getContents(self::SIG_URL . '7591/item_feed' . $signVal))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if($this->getInput('motorsports')) {
|
||||||
|
$newsElements = array_merge($newsElements,
|
||||||
|
json_decode(getContents(self::SIG_URL . '7590/item_feed' . $signVal))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if($this->getInput('car-culture')) {
|
||||||
|
$newsElements = array_merge($newsElements,
|
||||||
|
json_decode(getContents(self::SIG_URL . '7588/item_feed' . $signVal))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if($this->getInput('car-shows')) {
|
||||||
|
$newsElements = array_merge($newsElements,
|
||||||
|
json_decode(getContents(self::SIG_URL . '7589/item_feed' . $signVal))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($newsElements, function($a, $b) {
|
||||||
|
return $b->published - $a->published;
|
||||||
|
});
|
||||||
|
|
||||||
|
$limit = 19;
|
||||||
|
foreach($newsElements as $element) {
|
||||||
|
|
||||||
|
$item = array();
|
||||||
|
$item['uri'] = $element->sourceUrl;
|
||||||
|
$item['timestamp'] = $element->published;
|
||||||
|
$item['enclosures'] = array($element->cover->url);
|
||||||
|
$item['title'] = $element->title;
|
||||||
|
$item['content'] = $this->getArticleContent($element);
|
||||||
|
$this->items[] = $item;
|
||||||
|
|
||||||
|
if($limit > 0) {
|
||||||
|
$limit--;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getArticleContent($article) {
|
||||||
|
|
||||||
|
return getContents($article->contentUrl);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
63
bridges/SIMARBridge.php
Normal file
63
bridges/SIMARBridge.php
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
class SIMARBridge extends BridgeAbstract {
|
||||||
|
const NAME = 'SIMAR';
|
||||||
|
const URI = 'http://www.simar-louresodivelas.pt/';
|
||||||
|
const DESCRIPTION = 'Verificar estado da rede SIMAR';
|
||||||
|
const MAINTAINER = 'somini';
|
||||||
|
const PARAMETERS = array(
|
||||||
|
'Público' => array(
|
||||||
|
'interventions' => array(
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'name' => 'Incluir Intervenções?',
|
||||||
|
'defaultValue' => 'checked',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
public function collectData() {
|
||||||
|
$html = getSimpleHTMLDOM(self::getURI())
|
||||||
|
or returnServerError('Could not load content');
|
||||||
|
$e_home = $html->find('#home', 0)
|
||||||
|
or returnServerError('Invalid site structure');
|
||||||
|
|
||||||
|
foreach($e_home->find('span') as $element) {
|
||||||
|
$item = array();
|
||||||
|
|
||||||
|
$item['title'] = 'Rotura: ' . $element->plaintext;
|
||||||
|
$item['content'] = $element->innertext;
|
||||||
|
$item['uid'] = 'urn:sha1:' . hash('sha1', $item['content']);
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->getInput('interventions')) {
|
||||||
|
$e_main1 = $html->find('#menu1', 0)
|
||||||
|
or returnServerError('Invalid site structure');
|
||||||
|
|
||||||
|
foreach ($e_main1->find('a') as $element) {
|
||||||
|
$item = array();
|
||||||
|
|
||||||
|
$item['title'] = 'Intervenção: ' . $element->plaintext;
|
||||||
|
$item['uri'] = self::getURI() . $element->href;
|
||||||
|
$item['content'] = $element->innertext;
|
||||||
|
|
||||||
|
/* Try to get the actual contents for this kind of item */
|
||||||
|
$item_html = getSimpleHTMLDOMCached($item['uri']);
|
||||||
|
if ($item_html) {
|
||||||
|
$e_item = $item_html->find('.auto-style59', 0);
|
||||||
|
foreach($e_item->find('p') as $paragraph) {
|
||||||
|
/* Remove empty paragraphs */
|
||||||
|
if (preg_match('/^(\W| )+$/', $paragraph->innertext) == 1) {
|
||||||
|
$paragraph->outertext = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($e_item) {
|
||||||
|
$item['content'] = $e_item->innertext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ class SkimfeedBridge extends BridgeAbstract {
|
||||||
'box_channel' => array(
|
'box_channel' => array(
|
||||||
'name' => 'Channel',
|
'name' => 'Channel',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Select your channel',
|
'title' => 'Select your channel',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Hacker News' => '/news/hacker-news.html',
|
'Hacker News' => '/news/hacker-news.html',
|
||||||
|
@ -68,7 +67,6 @@ class SkimfeedBridge extends BridgeAbstract {
|
||||||
'tech_channel' => array(
|
'tech_channel' => array(
|
||||||
'name' => 'Tech channel',
|
'name' => 'Tech channel',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Select your tech channel',
|
'title' => 'Select your tech channel',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'Agg' => array(
|
'Agg' => array(
|
||||||
|
|
|
@ -14,7 +14,7 @@ class SoundCloudBridge extends BridgeAbstract {
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
|
|
||||||
const CLIENT_ID = '4jkoEFmZEDaqjwJ9Eih4ATNhcH3vMVfp';
|
const CLIENT_ID = 'W0KEWWILAjDiRH89X0jpwzuq6rbSK08R';
|
||||||
|
|
||||||
public function collectData(){
|
public function collectData(){
|
||||||
|
|
||||||
|
|
80
bridges/StockFilingsBridge.php
Normal file
80
bridges/StockFilingsBridge.php
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class StockFilingsBridge extends FeedExpander {
|
||||||
|
const MAINTAINER = 'captn3m0';
|
||||||
|
const NAME = 'SEC Stock filings';
|
||||||
|
const URI = 'https://www.sec.gov/edgar/searchedgar/companysearch.html';
|
||||||
|
const CACHE_TIMEOUT = 3600; // 1h
|
||||||
|
const DESCRIPTION = 'Tracks SEC Filings for a single company';
|
||||||
|
const SEARCH_URL = 'https://www.sec.gov/cgi-bin/browse-edgar?owner=exclude&action=getcompany&CIK=';
|
||||||
|
const WEBSITE_ROOT = 'https://www.sec.gov';
|
||||||
|
|
||||||
|
const PARAMETERS = array(
|
||||||
|
array(
|
||||||
|
'ticker' => array(
|
||||||
|
'name' => 'cik',
|
||||||
|
'required' => true,
|
||||||
|
'exampleValue' => 'AMD',
|
||||||
|
// https://stackoverflow.com/a/12827734
|
||||||
|
'pattern' => '[A-Za-z0-9]+',
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
public function getIcon() {
|
||||||
|
return 'https://www.sec.gov/favicon.ico';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates search URL
|
||||||
|
*/
|
||||||
|
private function getSearchUrl() {
|
||||||
|
return self::SEARCH_URL . $this->getInput('ticker');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Company Name
|
||||||
|
*/
|
||||||
|
private function getRssFeed($html) {
|
||||||
|
$links = $html->find('#contentDiv a');
|
||||||
|
|
||||||
|
foreach ($links as $link) {
|
||||||
|
$href = $link->href;
|
||||||
|
|
||||||
|
if (substr($href, 0, 4) !== 'http') {
|
||||||
|
$href = self::WEBSITE_ROOT . $href;
|
||||||
|
}
|
||||||
|
parse_str(html_entity_decode(parse_url($href, PHP_URL_QUERY)), $query);
|
||||||
|
|
||||||
|
if (isset($query['output']) and ($query['output'] == 'atom')) {
|
||||||
|
return $href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return \simple_html_dom object
|
||||||
|
* for the entire html of the product page
|
||||||
|
*/
|
||||||
|
private function getHtml() {
|
||||||
|
$uri = $this->getSearchUrl();
|
||||||
|
|
||||||
|
return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request SEC.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrape the SEC Stock Filings RSS Feed URL
|
||||||
|
* and redirect there
|
||||||
|
*/
|
||||||
|
public function collectData() {
|
||||||
|
$html = $this->getHtml();
|
||||||
|
$rssFeedUrl = $this->getRssFeed($html);
|
||||||
|
|
||||||
|
if ($rssFeedUrl) {
|
||||||
|
parent::collectExpandableDatas($rssFeedUrl);
|
||||||
|
} else {
|
||||||
|
returnClientError('Could not find RSS Feed URL. Are you sure you used a valid CIK?');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -165,7 +165,7 @@ class TwitterBridge extends BridgeAbstract {
|
||||||
|
|
||||||
// Skip retweets?
|
// Skip retweets?
|
||||||
if($this->getInput('noretweet')
|
if($this->getInput('noretweet')
|
||||||
&& $tweet->getAttribute('data-screen-name') !== $this->getInput('u')) {
|
&& strcasecmp($tweet->getAttribute('data-screen-name'), $this->getInput('u'))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,6 +189,9 @@ class TwitterBridge extends BridgeAbstract {
|
||||||
$item['fullname'] = htmlspecialchars_decode($tweet->getAttribute('data-name'), ENT_QUOTES);
|
$item['fullname'] = htmlspecialchars_decode($tweet->getAttribute('data-name'), ENT_QUOTES);
|
||||||
// get author
|
// get author
|
||||||
$item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
|
$item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
|
||||||
|
if(strcasecmp($tweet->getAttribute('data-screen-name'), $this->getInput('u'))) {
|
||||||
|
$item['author'] .= ' RT: @' . $this->getInput('u');
|
||||||
|
}
|
||||||
// get avatar link
|
// get avatar link
|
||||||
$item['avatar'] = $tweet->find('img', 0)->src;
|
$item['avatar'] = $tweet->find('img', 0)->src;
|
||||||
// get TweetID
|
// get TweetID
|
||||||
|
|
31
bridges/VMwareSecurityBridge.php
Normal file
31
bridges/VMwareSecurityBridge.php
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
class VMwareSecurityBridge extends BridgeAbstract {
|
||||||
|
|
||||||
|
const MAINTAINER = 'm0le.net';
|
||||||
|
const NAME = 'VMware Security Advisories';
|
||||||
|
const URI = 'https://www.vmware.com/security/advisories.html';
|
||||||
|
const CACHE_TIMEOUT = 7200; // 2h
|
||||||
|
const DESCRIPTION = 'VMware Security Advisories';
|
||||||
|
const WEBROOT = 'https://www.vmware.com';
|
||||||
|
|
||||||
|
public function collectData(){
|
||||||
|
$html = getSimpleHTMLDOM(self::URI)
|
||||||
|
or returnServerError('Could not request VSA.');
|
||||||
|
|
||||||
|
$html = defaultLinkTo($html, self::WEBROOT);
|
||||||
|
|
||||||
|
$item = array();
|
||||||
|
$articles = $html->find('div[class="news_block"]');
|
||||||
|
|
||||||
|
foreach ($articles as $element) {
|
||||||
|
$item['uri'] = $element->find('a', 0)->getAttribute('href');
|
||||||
|
$title = $element->find('a', 0)->innertext;
|
||||||
|
$item['title'] = $title;
|
||||||
|
$item['timestamp'] = strtotime($element->find('p', 0)->innertext);
|
||||||
|
$item['content'] = $element->find('p', 1)->innertext;
|
||||||
|
$item['uid'] = $title;
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,10 @@ class VkBridge extends BridgeAbstract
|
||||||
'u' => array(
|
'u' => array(
|
||||||
'name' => 'Group or user name',
|
'name' => 'Group or user name',
|
||||||
'required' => true
|
'required' => true
|
||||||
|
),
|
||||||
|
'hide_reposts' => array(
|
||||||
|
'name' => 'Hide reposts',
|
||||||
|
'type' => 'checkbox',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -234,6 +238,9 @@ class VkBridge extends BridgeAbstract
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_object($post->find('div.copy_quote', 0))) {
|
if (is_object($post->find('div.copy_quote', 0))) {
|
||||||
|
if ($this->getInput('hide_reposts') === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$copy_quote = $post->find('div.copy_quote', 0);
|
$copy_quote = $post->find('div.copy_quote', 0);
|
||||||
if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) {
|
if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) {
|
||||||
$copy_post_header->outertext = '';
|
$copy_post_header->outertext = '';
|
||||||
|
|
|
@ -9,7 +9,6 @@ class WikiLeaksBridge extends BridgeAbstract {
|
||||||
'category' => array(
|
'category' => array(
|
||||||
'name' => 'Category',
|
'name' => 'Category',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Select your category',
|
'title' => 'Select your category',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
'News' => '-News-',
|
'News' => '-News-',
|
||||||
|
@ -28,7 +27,6 @@ class WikiLeaksBridge extends BridgeAbstract {
|
||||||
'teaser' => array(
|
'teaser' => array(
|
||||||
'name' => 'Show teaser',
|
'name' => 'Show teaser',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'required' => false,
|
|
||||||
'title' => 'If checked feeds will display the teaser',
|
'title' => 'If checked feeds will display the teaser',
|
||||||
'defaultValue' => true
|
'defaultValue' => true
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,6 @@ class WikipediaBridge extends BridgeAbstract {
|
||||||
'language' => array(
|
'language' => array(
|
||||||
'name' => 'Language',
|
'name' => 'Language',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'Select your language',
|
'title' => 'Select your language',
|
||||||
'exampleValue' => 'English',
|
'exampleValue' => 'English',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
|
@ -27,7 +26,6 @@ class WikipediaBridge extends BridgeAbstract {
|
||||||
'subject' => array(
|
'subject' => array(
|
||||||
'name' => 'Subject',
|
'name' => 'Subject',
|
||||||
'type' => 'list',
|
'type' => 'list',
|
||||||
'required' => true,
|
|
||||||
'title' => 'What subject are you interested in?',
|
'title' => 'What subject are you interested in?',
|
||||||
'exampleValue' => 'Today\'s featured article',
|
'exampleValue' => 'Today\'s featured article',
|
||||||
'values' => array(
|
'values' => array(
|
||||||
|
|
|
@ -75,7 +75,7 @@ class WordPressPluginUpdateBridge extends BridgeAbstract {
|
||||||
private function getCachedDate($url){
|
private function getCachedDate($url){
|
||||||
Debug::log('getting pubdate from url ' . $url . '');
|
Debug::log('getting pubdate from url ' . $url . '');
|
||||||
// Initialize cache
|
// Initialize cache
|
||||||
$cache = Cache::create('FileCache');
|
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
|
||||||
$cache->setPath(PATH_CACHE . 'pages/');
|
$cache->setPath(PATH_CACHE . 'pages/');
|
||||||
$params = [$url];
|
$params = [$url];
|
||||||
$cache->setParameters($params);
|
$cache->setParameters($params);
|
||||||
|
|
99
caches/SQLiteCache.php
Normal file
99
caches/SQLiteCache.php
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Cache based on SQLite 3 <https://www.sqlite.org>
|
||||||
|
*/
|
||||||
|
class SQLiteCache implements CacheInterface {
|
||||||
|
protected $path;
|
||||||
|
protected $param;
|
||||||
|
|
||||||
|
private $db = null;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
if (!extension_loaded('sqlite3'))
|
||||||
|
die('"sqlite3" extension not loaded. Please check "php.ini"');
|
||||||
|
|
||||||
|
$file = PATH_CACHE . 'cache.sqlite';
|
||||||
|
|
||||||
|
if (!is_file($file)) {
|
||||||
|
$this->db = new SQLite3($file);
|
||||||
|
$this->db->enableExceptions(true);
|
||||||
|
$this->db->exec("CREATE TABLE storage ('key' BLOB PRIMARY KEY, 'value' BLOB, 'updated' INTEGER)");
|
||||||
|
} else {
|
||||||
|
$this->db = new SQLite3($file);
|
||||||
|
$this->db->enableExceptions(true);
|
||||||
|
}
|
||||||
|
$this->db->busyTimeout(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadData(){
|
||||||
|
$Qselect = $this->db->prepare('SELECT value FROM storage WHERE key = :key');
|
||||||
|
$Qselect->bindValue(':key', $this->getCacheKey());
|
||||||
|
$result = $Qselect->execute();
|
||||||
|
if ($result instanceof SQLite3Result) {
|
||||||
|
$data = $result->fetchArray(SQLITE3_ASSOC);
|
||||||
|
if (isset($data['value'])) {
|
||||||
|
return unserialize($data['value']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveData($datas){
|
||||||
|
$Qupdate = $this->db->prepare('INSERT OR REPLACE INTO storage (key, value, updated) VALUES (:key, :value, :updated)');
|
||||||
|
$Qupdate->bindValue(':key', $this->getCacheKey());
|
||||||
|
$Qupdate->bindValue(':value', serialize($datas));
|
||||||
|
$Qupdate->bindValue(':updated', time());
|
||||||
|
$Qupdate->execute();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTime(){
|
||||||
|
$Qselect = $this->db->prepare('SELECT updated FROM storage WHERE key = :key');
|
||||||
|
$Qselect->bindValue(':key', $this->getCacheKey());
|
||||||
|
$result = $Qselect->execute();
|
||||||
|
if ($result instanceof SQLite3Result) {
|
||||||
|
$data = $result->fetchArray(SQLITE3_ASSOC);
|
||||||
|
if (isset($data['updated'])) {
|
||||||
|
return $data['updated'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function purgeCache($duration){
|
||||||
|
$Qdelete = $this->db->prepare('DELETE FROM storage WHERE updated < :expired');
|
||||||
|
$Qdelete->bindValue(':expired', time() - $duration);
|
||||||
|
$Qdelete->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cache path
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function setPath($path){
|
||||||
|
$this->path = $path;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set HTTP GET parameters
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function setParameters(array $param){
|
||||||
|
$this->param = array_map('strtolower', $param);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
protected function getCacheKey(){
|
||||||
|
if(is_null($this->param)) {
|
||||||
|
throw new \Exception('Call "setParameters" first!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash('sha1', $this->path . http_build_query($this->param), true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,10 @@
|
||||||
|
|
||||||
[cache]
|
[cache]
|
||||||
|
|
||||||
|
; Defines the cache type used by RSS-Bridge
|
||||||
|
; "file" = FileCache (default)
|
||||||
|
type = "file"
|
||||||
|
|
||||||
; Allow users to specify custom timeout for specific requests.
|
; Allow users to specify custom timeout for specific requests.
|
||||||
; true = enabled
|
; true = enabled
|
||||||
; false = disabled (default)
|
; false = disabled (default)
|
||||||
|
|
|
@ -1,21 +1,30 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Atom
|
* AtomFormat - RFC 4287: The Atom Syndication Format
|
||||||
* Documentation Source http://en.wikipedia.org/wiki/Atom_%28standard%29 and
|
* https://tools.ietf.org/html/rfc4287
|
||||||
* http://tools.ietf.org/html/rfc4287
|
*
|
||||||
*/
|
* Validator:
|
||||||
|
* https://validator.w3.org/feed/
|
||||||
|
*/
|
||||||
class AtomFormat extends FormatAbstract{
|
class AtomFormat extends FormatAbstract{
|
||||||
public function stringify(){
|
const LIMIT_TITLE = 140;
|
||||||
$https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 's' : '';
|
|
||||||
$httpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
|
|
||||||
$httpInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
|
|
||||||
|
|
||||||
$serverRequestUri = isset($_SERVER['REQUEST_URI']) ? $this->xml_encode($_SERVER['REQUEST_URI']) : '';
|
public function stringify(){
|
||||||
|
$urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
|
||||||
|
$urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
|
||||||
|
$urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
|
||||||
|
$urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
|
||||||
|
|
||||||
|
$feedUrl = $this->xml_encode($urlPrefix . $urlHost . $urlRequest);
|
||||||
|
|
||||||
$extraInfos = $this->getExtraInfos();
|
$extraInfos = $this->getExtraInfos();
|
||||||
$title = $this->xml_encode($extraInfos['name']);
|
$title = $this->xml_encode($extraInfos['name']);
|
||||||
$uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
|
$uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
|
||||||
|
|
||||||
|
// since we can't guarantee that all items have an author,
|
||||||
|
// a global feed author is mandatory
|
||||||
|
$feedAuthor = 'RSS-Bridge';
|
||||||
|
|
||||||
$uriparts = parse_url($uri);
|
$uriparts = parse_url($uri);
|
||||||
if(!empty($extraInfos['icon'])) {
|
if(!empty($extraInfos['icon'])) {
|
||||||
$icon = $extraInfos['icon'];
|
$icon = $extraInfos['icon'];
|
||||||
|
@ -27,11 +36,40 @@ class AtomFormat extends FormatAbstract{
|
||||||
|
|
||||||
$entries = '';
|
$entries = '';
|
||||||
foreach($this->getItems() as $item) {
|
foreach($this->getItems() as $item) {
|
||||||
$entryAuthor = $this->xml_encode($item->getAuthor());
|
$entryTimestamp = $item->getTimestamp();
|
||||||
$entryTitle = $this->xml_encode($item->getTitle());
|
$entryTitle = $this->xml_encode($item->getTitle());
|
||||||
$entryUri = $this->xml_encode($item->getURI());
|
$entryContent = $item->getContent();
|
||||||
$entryTimestamp = $this->xml_encode(date(DATE_ATOM, $item->getTimestamp()));
|
$entryUri = $item->getURI();
|
||||||
$entryContent = $this->xml_encode($this->sanitizeHtml($item->getContent()));
|
$entryID = '';
|
||||||
|
|
||||||
|
if (!empty($item->getUid()))
|
||||||
|
$entryID = 'urn:sha1:' . $item->getUid();
|
||||||
|
|
||||||
|
if (empty($entryID)) // Fallback to provided URI
|
||||||
|
$entryID = $this->xml_encode($entryUri);
|
||||||
|
|
||||||
|
if (empty($entryID)) // Fallback to title and content
|
||||||
|
$entryID = 'urn:sha1:' . hash('sha1', $entryTitle . $entryContent);
|
||||||
|
|
||||||
|
if (empty($entryTimestamp))
|
||||||
|
$entryTimestamp = $this->lastModified;
|
||||||
|
|
||||||
|
if (empty($entryTitle)) {
|
||||||
|
$entryTitle = str_replace("\n", ' ', strip_tags($entryContent));
|
||||||
|
if (strlen($entryTitle) > self::LIMIT_TITLE) {
|
||||||
|
$wrapPos = strpos(wordwrap($entryTitle, self::LIMIT_TITLE), "\n");
|
||||||
|
$entryTitle = substr($entryTitle, 0, $wrapPos) . '...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($entryContent))
|
||||||
|
$entryContent = $entryTitle;
|
||||||
|
|
||||||
|
$entryAuthor = $this->xml_encode($item->getAuthor());
|
||||||
|
$entryTitle = $this->xml_encode($entryTitle);
|
||||||
|
$entryUri = $this->xml_encode($entryUri);
|
||||||
|
$entryTimestamp = $this->xml_encode(gmdate(DATE_ATOM, $entryTimestamp));
|
||||||
|
$entryContent = $this->xml_encode($this->sanitizeHtml($entryContent));
|
||||||
|
|
||||||
$entryEnclosures = '';
|
$entryEnclosures = '';
|
||||||
foreach($item->getEnclosures() as $enclosure) {
|
foreach($item->getEnclosures() as $enclosure) {
|
||||||
|
@ -49,16 +87,28 @@ class AtomFormat extends FormatAbstract{
|
||||||
. PHP_EOL;
|
. PHP_EOL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$entryLinkAlternate = '';
|
||||||
|
if (!empty($entryUri)) {
|
||||||
|
$entryLinkAlternate = '<link rel="alternate" type="text/html" href="'
|
||||||
|
. $entryUri
|
||||||
|
. '"/>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($entryAuthor)) {
|
||||||
|
$entryAuthor = '<author><name>'
|
||||||
|
. $entryAuthor
|
||||||
|
. '</name></author>';
|
||||||
|
}
|
||||||
|
|
||||||
$entries .= <<<EOD
|
$entries .= <<<EOD
|
||||||
|
|
||||||
<entry>
|
<entry>
|
||||||
<author>
|
|
||||||
<name>{$entryAuthor}</name>
|
|
||||||
</author>
|
|
||||||
<title type="html">{$entryTitle}</title>
|
<title type="html">{$entryTitle}</title>
|
||||||
<link rel="alternate" type="text/html" href="{$entryUri}" />
|
<published>{$entryTimestamp}</published>
|
||||||
<id>{$entryUri}</id>
|
|
||||||
<updated>{$entryTimestamp}</updated>
|
<updated>{$entryTimestamp}</updated>
|
||||||
|
<id>{$entryID}</id>
|
||||||
|
{$entryLinkAlternate}
|
||||||
|
{$entryAuthor}
|
||||||
<content type="html">{$entryContent}</content>
|
<content type="html">{$entryContent}</content>
|
||||||
{$entryEnclosures}
|
{$entryEnclosures}
|
||||||
{$entryCategories}
|
{$entryCategories}
|
||||||
|
@ -67,21 +117,24 @@ class AtomFormat extends FormatAbstract{
|
||||||
EOD;
|
EOD;
|
||||||
}
|
}
|
||||||
|
|
||||||
$feedTimestamp = date(DATE_ATOM, time());
|
$feedTimestamp = gmdate(DATE_ATOM, $this->lastModified);
|
||||||
$charset = $this->getCharset();
|
$charset = $this->getCharset();
|
||||||
|
|
||||||
/* Data are prepared, now let's begin the "MAGIE !!!" */
|
/* Data are prepared, now let's begin the "MAGIE !!!" */
|
||||||
$toReturn = <<<EOD
|
$toReturn = <<<EOD
|
||||||
<?xml version="1.0" encoding="{$charset}"?>
|
<?xml version="1.0" encoding="{$charset}"?>
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0">
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
|
||||||
<title type="text">{$title}</title>
|
<title type="text">{$title}</title>
|
||||||
<id>http{$https}://{$httpHost}{$httpInfo}/</id>
|
<id>{$feedUrl}</id>
|
||||||
<icon>{$icon}</icon>
|
<icon>{$icon}</icon>
|
||||||
<logo>{$icon}</logo>
|
<logo>{$icon}</logo>
|
||||||
<updated>{$feedTimestamp}</updated>
|
<updated>{$feedTimestamp}</updated>
|
||||||
|
<author>
|
||||||
|
<name>{$feedAuthor}</name>
|
||||||
|
</author>
|
||||||
<link rel="alternate" type="text/html" href="{$uri}" />
|
<link rel="alternate" type="text/html" href="{$uri}" />
|
||||||
<link rel="self" href="http{$https}://{$httpHost}{$serverRequestUri}" />
|
<link rel="self" type="application/atom+xml" href="{$feedUrl}" />
|
||||||
{$entries}
|
{$entries}
|
||||||
</feed>
|
</feed>
|
||||||
EOD;
|
EOD;
|
||||||
|
|
|
@ -16,21 +16,22 @@ class JsonFormat extends FormatAbstract {
|
||||||
'content',
|
'content',
|
||||||
'enclosures',
|
'enclosures',
|
||||||
'categories',
|
'categories',
|
||||||
|
'uid',
|
||||||
);
|
);
|
||||||
|
|
||||||
public function stringify(){
|
public function stringify(){
|
||||||
$urlScheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
|
$urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
|
||||||
$urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
|
$urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
|
||||||
$urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
|
$urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
|
||||||
$urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
|
$urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
|
||||||
|
|
||||||
$extraInfos = $this->getExtraInfos();
|
$extraInfos = $this->getExtraInfos();
|
||||||
|
|
||||||
$data = array(
|
$data = array(
|
||||||
'version' => 'https://jsonfeed.org/version/1',
|
'version' => 'https://jsonfeed.org/version/1',
|
||||||
'title' => (!empty($extraInfos['name'])) ? $extraInfos['name'] : $urlHost,
|
'title' => (!empty($extraInfos['name'])) ? $extraInfos['name'] : $urlHost,
|
||||||
'home_page_url' => (!empty($extraInfos['uri'])) ? $extraInfos['uri'] : REPOSITORY,
|
'home_page_url' => (!empty($extraInfos['uri'])) ? $extraInfos['uri'] : REPOSITORY,
|
||||||
'feed_url' => $urlScheme . $urlHost . $urlRequest
|
'feed_url' => $urlPrefix . $urlHost . $urlRequest
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!empty($extraInfos['icon'])) {
|
if (!empty($extraInfos['icon'])) {
|
||||||
|
@ -42,20 +43,24 @@ class JsonFormat extends FormatAbstract {
|
||||||
foreach ($this->getItems() as $item) {
|
foreach ($this->getItems() as $item) {
|
||||||
$entry = array();
|
$entry = array();
|
||||||
|
|
||||||
$entryAuthor = $item->getAuthor();
|
$entryAuthor = $item->getAuthor();
|
||||||
$entryTitle = $item->getTitle();
|
$entryTitle = $item->getTitle();
|
||||||
$entryUri = $item->getURI();
|
$entryUri = $item->getURI();
|
||||||
$entryTimestamp = $item->getTimestamp();
|
$entryTimestamp = $item->getTimestamp();
|
||||||
$entryContent = $this->sanitizeHtml($item->getContent());
|
$entryContent = $this->sanitizeHtml($item->getContent());
|
||||||
$entryEnclosures = $item->getEnclosures();
|
$entryEnclosures = $item->getEnclosures();
|
||||||
$entryCategories = $item->getCategories();
|
$entryCategories = $item->getCategories();
|
||||||
|
|
||||||
$vendorFields = $item->toArray();
|
$vendorFields = $item->toArray();
|
||||||
foreach (self::VENDOR_EXCLUDES as $key) {
|
foreach (self::VENDOR_EXCLUDES as $key) {
|
||||||
unset($vendorFields[$key]);
|
unset($vendorFields[$key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$entry['id'] = $entryUri;
|
$entry['id'] = $item->getUid();
|
||||||
|
|
||||||
|
if (empty($entry['id'])) {
|
||||||
|
$entry['id'] = $entryUri;
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($entryTitle)) {
|
if (!empty($entryTitle)) {
|
||||||
$entry['title'] = $entryTitle;
|
$entry['title'] = $entryTitle;
|
||||||
|
@ -82,8 +87,8 @@ class JsonFormat extends FormatAbstract {
|
||||||
$entry['attachments'] = array();
|
$entry['attachments'] = array();
|
||||||
foreach ($entryEnclosures as $enclosure) {
|
foreach ($entryEnclosures as $enclosure) {
|
||||||
$entry['attachments'][] = array(
|
$entry['attachments'][] = array(
|
||||||
'url' => $enclosure,
|
'url' => $enclosure,
|
||||||
'mime_type' => getMimeType($enclosure)
|
'mime_type' => getMimeType($enclosure)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,45 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Mrss
|
* MrssFormat - RSS 2.0 + Media RSS
|
||||||
* Documentation Source http://www.rssboard.org/media-rss
|
* http://www.rssboard.org/rss-specification
|
||||||
*/
|
* http://www.rssboard.org/media-rss
|
||||||
|
*
|
||||||
|
* Validators:
|
||||||
|
* https://validator.w3.org/feed/
|
||||||
|
* http://www.rssboard.org/rss-validator/
|
||||||
|
*
|
||||||
|
* Notes about the implementation:
|
||||||
|
*
|
||||||
|
* - The item author is not supported as it needs to be an e-mail address to be
|
||||||
|
* valid.
|
||||||
|
* - The RSS specification does not explicitly allow to have more than one
|
||||||
|
* enclosure as every item is meant to provide one "story", thus having
|
||||||
|
* multiple enclosures per item may lead to unexpected behavior.
|
||||||
|
* On top of that, it requires to have a length specified, which RSS-Bridge
|
||||||
|
* can't provide.
|
||||||
|
* - The Media RSS extension comes in handy, since it allows to have multiple
|
||||||
|
* enclosures, even though they recommend to have only one enclosure because
|
||||||
|
* of the one-story-per-item reason. It only requires to specify the URL,
|
||||||
|
* everything else is optional.
|
||||||
|
* - Since the Media RSS extension has its own namespace, the output is a valid
|
||||||
|
* RSS 2.0 feed that works with feed readers that don't support the extension.
|
||||||
|
*/
|
||||||
class MrssFormat extends FormatAbstract {
|
class MrssFormat extends FormatAbstract {
|
||||||
public function stringify(){
|
const ALLOWED_IMAGE_EXT = array(
|
||||||
$https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 's' : '';
|
'.gif', '.jpg', '.png'
|
||||||
$httpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
|
);
|
||||||
$httpInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
|
|
||||||
|
|
||||||
$serverRequestUri = isset($_SERVER['REQUEST_URI']) ? $this->xml_encode($_SERVER['REQUEST_URI']) : '';
|
public function stringify(){
|
||||||
|
$urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
|
||||||
|
$urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
|
||||||
|
$urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
|
||||||
|
$urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
|
||||||
|
|
||||||
|
$feedUrl = $this->xml_encode($urlPrefix . $urlHost . $urlRequest);
|
||||||
|
|
||||||
$extraInfos = $this->getExtraInfos();
|
$extraInfos = $this->getExtraInfos();
|
||||||
$title = $this->xml_encode($extraInfos['name']);
|
$title = $this->xml_encode($extraInfos['name']);
|
||||||
|
$icon = $extraInfos['icon'];
|
||||||
|
|
||||||
if(!empty($extraInfos['uri'])) {
|
if(!empty($extraInfos['uri'])) {
|
||||||
$uri = $this->xml_encode($extraInfos['uri']);
|
$uri = $this->xml_encode($extraInfos['uri']);
|
||||||
|
@ -20,34 +47,48 @@ class MrssFormat extends FormatAbstract {
|
||||||
$uri = REPOSITORY;
|
$uri = REPOSITORY;
|
||||||
}
|
}
|
||||||
|
|
||||||
$uriparts = parse_url($uri);
|
|
||||||
$icon = $this->xml_encode($uriparts['scheme'] . '://' . $uriparts['host'] . '/favicon.ico');
|
|
||||||
|
|
||||||
$items = '';
|
$items = '';
|
||||||
foreach($this->getItems() as $item) {
|
foreach($this->getItems() as $item) {
|
||||||
$itemAuthor = $this->xml_encode($item->getAuthor());
|
$itemTimestamp = $item->getTimestamp();
|
||||||
$itemTitle = $this->xml_encode($item->getTitle());
|
$itemTitle = $this->xml_encode($item->getTitle());
|
||||||
$itemUri = $this->xml_encode($item->getURI());
|
$itemUri = $this->xml_encode($item->getURI());
|
||||||
$itemTimestamp = $this->xml_encode(date(DATE_RFC2822, $item->getTimestamp()));
|
|
||||||
$itemContent = $this->xml_encode($this->sanitizeHtml($item->getContent()));
|
$itemContent = $this->xml_encode($this->sanitizeHtml($item->getContent()));
|
||||||
|
$entryID = $item->getUid();
|
||||||
|
$isPermaLink = 'false';
|
||||||
|
|
||||||
|
if (empty($entryID) && !empty($itemUri)) { // Fallback to provided URI
|
||||||
|
$entryID = $itemUri;
|
||||||
|
$isPermaLink = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($entryID)) // Fallback to title and content
|
||||||
|
$entryID = hash('sha1', $itemTitle . $itemContent);
|
||||||
|
|
||||||
|
$entryTitle = '';
|
||||||
|
if (!empty($itemTitle))
|
||||||
|
$entryTitle = '<title>' . $itemTitle . '</title>';
|
||||||
|
|
||||||
|
$entryLink = '';
|
||||||
|
if (!empty($itemUri))
|
||||||
|
$entryLink = '<link>' . $itemUri . '</link>';
|
||||||
|
|
||||||
|
$entryPublished = '';
|
||||||
|
if (!empty($itemTimestamp)) {
|
||||||
|
$entryPublished = '<pubDate>'
|
||||||
|
. $this->xml_encode(gmdate(DATE_RFC2822, $itemTimestamp))
|
||||||
|
. '</pubDate>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$entryDescription = '';
|
||||||
|
if (!empty($itemContent))
|
||||||
|
$entryDescription = '<description>' . $itemContent . '</description>';
|
||||||
|
|
||||||
$entryEnclosuresWarning = '';
|
|
||||||
$entryEnclosures = '';
|
$entryEnclosures = '';
|
||||||
if(!empty($item->getEnclosures())) {
|
foreach($item->getEnclosures() as $enclosure) {
|
||||||
$entryEnclosures .= '<enclosure url="'
|
$entryEnclosures .= '<media:content url="'
|
||||||
. $this->xml_encode($item->getEnclosures()[0])
|
. $this->xml_encode($enclosure)
|
||||||
. '" type="' . getMimeType($item->getEnclosures()[0]) . '" />';
|
. '" type="' . getMimeType($enclosure) . '"/>'
|
||||||
|
. PHP_EOL;
|
||||||
if(count($item->getEnclosures()) > 1) {
|
|
||||||
$entryEnclosures .= PHP_EOL;
|
|
||||||
$entryEnclosuresWarning = '<br>Warning:
|
|
||||||
Some media files might not be shown to you. Consider using the ATOM format instead!';
|
|
||||||
foreach($item->getEnclosures() as $enclosure) {
|
|
||||||
$entryEnclosures .= '<atom:link rel="enclosure" href="'
|
|
||||||
. $enclosure . '" type="' . getMimeType($enclosure) . '" />'
|
|
||||||
. PHP_EOL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$entryCategories = '';
|
$entryCategories = '';
|
||||||
|
@ -60,12 +101,11 @@ Some media files might not be shown to you. Consider using the ATOM format inste
|
||||||
$items .= <<<EOD
|
$items .= <<<EOD
|
||||||
|
|
||||||
<item>
|
<item>
|
||||||
<title>{$itemTitle}</title>
|
{$entryTitle}
|
||||||
<link>{$itemUri}</link>
|
{$entryLink}
|
||||||
<guid isPermaLink="true">{$itemUri}</guid>
|
<guid isPermaLink="{$isPermaLink}">{$entryID}</guid>
|
||||||
<pubDate>{$itemTimestamp}</pubDate>
|
{$entryPublished}
|
||||||
<description>{$itemContent}{$entryEnclosuresWarning}</description>
|
{$entryDescription}
|
||||||
<author>{$itemAuthor}</author>
|
|
||||||
{$entryEnclosures}
|
{$entryEnclosures}
|
||||||
{$entryCategories}
|
{$entryCategories}
|
||||||
</item>
|
</item>
|
||||||
|
@ -75,22 +115,28 @@ EOD;
|
||||||
|
|
||||||
$charset = $this->getCharset();
|
$charset = $this->getCharset();
|
||||||
|
|
||||||
/* xml attributes need to have certain characters escaped to be w3c compliant */
|
$feedImage = '';
|
||||||
$imageTitle = htmlspecialchars($title, ENT_COMPAT);
|
if (!empty($icon) && in_array(substr($icon, -4), self::ALLOWED_IMAGE_EXT)) {
|
||||||
|
$feedImage .= <<<EOD
|
||||||
|
<image>
|
||||||
|
<url>{$icon}</url>
|
||||||
|
<title>{$title}</title>
|
||||||
|
<link>{$uri}</link>
|
||||||
|
</image>
|
||||||
|
EOD;
|
||||||
|
}
|
||||||
|
|
||||||
/* Data are prepared, now let's begin the "MAGIE !!!" */
|
/* Data are prepared, now let's begin the "MAGIE !!!" */
|
||||||
$toReturn = <<<EOD
|
$toReturn = <<<EOD
|
||||||
<?xml version="1.0" encoding="{$charset}"?>
|
<?xml version="1.0" encoding="{$charset}"?>
|
||||||
<rss version="2.0"
|
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:media="http://search.yahoo.com/mrss/"
|
|
||||||
xmlns:atom="http://www.w3.org/2005/Atom">
|
|
||||||
<channel>
|
<channel>
|
||||||
<title>{$title}</title>
|
<title>{$title}</title>
|
||||||
<link>http{$https}://{$httpHost}{$httpInfo}/</link>
|
<link>{$uri}</link>
|
||||||
<description>{$title}</description>
|
<description>{$title}</description>
|
||||||
<image url="{$icon}" title="{$imageTitle}" link="{$uri}"/>
|
{$feedImage}
|
||||||
<atom:link rel="alternate" type="text/html" href="{$uri}" />
|
<atom:link rel="alternate" type="text/html" href="{$uri}"/>
|
||||||
<atom:link rel="self" href="http{$https}://{$httpHost}{$serverRequestUri}" />
|
<atom:link rel="self" href="{$feedUrl}" type="application/atom+xml"/>
|
||||||
{$items}
|
{$items}
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
|
|
286
index.php
286
index.php
|
@ -51,287 +51,15 @@ $whitelist_default = array(
|
||||||
try {
|
try {
|
||||||
|
|
||||||
Bridge::setWhitelist($whitelist_default);
|
Bridge::setWhitelist($whitelist_default);
|
||||||
|
$actionFac = new \ActionFactory();
|
||||||
|
$actionFac->setWorkingDir(PATH_LIB_ACTIONS);
|
||||||
|
|
||||||
$showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
|
if(array_key_exists('action', $params)) {
|
||||||
$action = array_key_exists('action', $params) ? $params['action'] : null;
|
$action = $actionFac->create($params['action']);
|
||||||
$bridge = array_key_exists('bridge', $params) ? $params['bridge'] : null;
|
$action->setUserData($params);
|
||||||
|
$action->execute();
|
||||||
// Return list of bridges as JSON formatted text
|
|
||||||
if($action === 'list') {
|
|
||||||
|
|
||||||
$list = new StdClass();
|
|
||||||
$list->bridges = array();
|
|
||||||
$list->total = 0;
|
|
||||||
|
|
||||||
foreach(Bridge::getBridgeNames() as $bridgeName) {
|
|
||||||
|
|
||||||
$bridge = Bridge::create($bridgeName);
|
|
||||||
|
|
||||||
if($bridge === false) { // Broken bridge, show as inactive
|
|
||||||
|
|
||||||
$list->bridges[$bridgeName] = array(
|
|
||||||
'status' => 'inactive'
|
|
||||||
);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = Bridge::isWhitelisted($bridgeName) ? 'active' : 'inactive';
|
|
||||||
|
|
||||||
$list->bridges[$bridgeName] = array(
|
|
||||||
'status' => $status,
|
|
||||||
'uri' => $bridge->getURI(),
|
|
||||||
'name' => $bridge->getName(),
|
|
||||||
'icon' => $bridge->getIcon(),
|
|
||||||
'parameters' => $bridge->getParameters(),
|
|
||||||
'maintainer' => $bridge->getMaintainer(),
|
|
||||||
'description' => $bridge->getDescription()
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
$list->total = count($list->bridges);
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
echo json_encode($list, JSON_PRETTY_PRINT);
|
|
||||||
|
|
||||||
} 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!');
|
|
||||||
|
|
||||||
// DEPRECATED: 'nameFormat' scheme is replaced by 'name' in format parameter values
|
|
||||||
// this is to keep compatibility until futher complete removal
|
|
||||||
if(($pos = strpos($format, 'Format')) === (strlen($format) - strlen('Format'))) {
|
|
||||||
$format = substr($format, 0, $pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// whitelist control
|
|
||||||
if(!Bridge::isWhitelisted($bridge)) {
|
|
||||||
throw new \Exception('This bridge is not whitelisted', 401);
|
|
||||||
die;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data retrieval
|
|
||||||
$bridge = Bridge::create($bridge);
|
|
||||||
|
|
||||||
$noproxy = array_key_exists('_noproxy', $params) && filter_var($params['_noproxy'], FILTER_VALIDATE_BOOLEAN);
|
|
||||||
if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
|
|
||||||
define('NOPROXY', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache timeout
|
|
||||||
$cache_timeout = -1;
|
|
||||||
if(array_key_exists('_cache_timeout', $params)) {
|
|
||||||
|
|
||||||
if(!CUSTOM_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(PATH_CACHE);
|
|
||||||
$cache->purgeCache(86400); // 24 hours
|
|
||||||
$cache->setParameters($cache_params);
|
|
||||||
|
|
||||||
$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'])) {
|
|
||||||
foreach($cached['items'] as $item) {
|
|
||||||
$items[] = new \FeedItem($item);
|
|
||||||
}
|
|
||||||
|
|
||||||
$infos = $cached['extraInfos'];
|
|
||||||
}
|
|
||||||
|
|
||||||
} else { // Collect new data
|
|
||||||
|
|
||||||
try {
|
|
||||||
$bridge->setDatas($bridge_params);
|
|
||||||
$bridge->collectData();
|
|
||||||
|
|
||||||
$items = $bridge->getItems();
|
|
||||||
|
|
||||||
// Transform "legacy" items to FeedItems if necessary.
|
|
||||||
// Remove this code when support for "legacy" items ends!
|
|
||||||
if(isset($items[0]) && is_array($items[0])) {
|
|
||||||
$feedItems = array();
|
|
||||||
|
|
||||||
foreach($items as $item) {
|
|
||||||
$feedItems[] = new \FeedItem($item);
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = $feedItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
$infos = array(
|
|
||||||
'name' => $bridge->getName(),
|
|
||||||
'uri' => $bridge->getURI(),
|
|
||||||
'icon' => $bridge->getIcon()
|
|
||||||
);
|
|
||||||
} catch(Error $e) {
|
|
||||||
error_log($e);
|
|
||||||
|
|
||||||
$item = new \FeedItem();
|
|
||||||
|
|
||||||
// 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->setTitle('Bridge encountered an unexpected situation! (' . $params['_error_time'] . ')');
|
|
||||||
} else {
|
|
||||||
$item->setTitle('Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')');
|
|
||||||
}
|
|
||||||
|
|
||||||
$item->setURI(
|
|
||||||
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
|
|
||||||
. '?'
|
|
||||||
. http_build_query($params)
|
|
||||||
);
|
|
||||||
|
|
||||||
$item->setTimestamp(time());
|
|
||||||
$item->setContent(buildBridgeException($e, $bridge));
|
|
||||||
|
|
||||||
$items[] = $item;
|
|
||||||
} catch(Exception $e) {
|
|
||||||
error_log($e);
|
|
||||||
|
|
||||||
$item = new \FeedItem();
|
|
||||||
|
|
||||||
// Create "new" error message every 24 hours
|
|
||||||
$params['_error_time'] = urlencode((int)(time() / 86400));
|
|
||||||
|
|
||||||
$item->setURI(
|
|
||||||
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
|
|
||||||
. '?'
|
|
||||||
. http_build_query($params)
|
|
||||||
);
|
|
||||||
|
|
||||||
$item->setTitle('Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')');
|
|
||||||
$item->setTimestamp(time());
|
|
||||||
$item->setContent(buildBridgeException($e, $bridge));
|
|
||||||
|
|
||||||
$items[] = $item;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store data in cache
|
|
||||||
$cache->saveData(array(
|
|
||||||
'items' => array_map(function($i){ return $i->toArray(); }, $items),
|
|
||||||
'extraInfos' => $infos
|
|
||||||
));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data transformation
|
|
||||||
try {
|
|
||||||
$format = Format::create($format);
|
|
||||||
$format->setItems($items);
|
|
||||||
$format->setExtraInfos($infos);
|
|
||||||
$format->setLastModified($cache->getTime());
|
|
||||||
$format->display();
|
|
||||||
} catch(Error $e) {
|
|
||||||
error_log($e);
|
|
||||||
header('Content-Type: text/html', true, $e->getCode());
|
|
||||||
die(buildTransformException($e, $bridge));
|
|
||||||
} catch(Exception $e) {
|
|
||||||
error_log($e);
|
|
||||||
header('Content-Type: text/html', true, $e->getCode());
|
|
||||||
die(buildTransformException($e, $bridge));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
$showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
|
||||||
echo BridgeList::create($showInactive);
|
echo BridgeList::create($showInactive);
|
||||||
}
|
}
|
||||||
} catch(\Exception $e) {
|
} catch(\Exception $e) {
|
||||||
|
|
33
lib/ActionAbstract.php
Normal file
33
lib/ActionAbstract.php
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?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 action objects
|
||||||
|
*/
|
||||||
|
abstract class ActionAbstract implements ActionInterface {
|
||||||
|
/**
|
||||||
|
* Holds the user data.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $userData = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @param array $userData {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function setUserData($userData) {
|
||||||
|
$this->userData = $userData;
|
||||||
|
}
|
||||||
|
}
|
65
lib/ActionFactory.php
Normal file
65
lib/ActionFactory.php
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<?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 for action objects.
|
||||||
|
*/
|
||||||
|
class ActionFactory extends FactoryAbstract {
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @param string $name {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function create($name) {
|
||||||
|
$filePath = $this->buildFilePath($name);
|
||||||
|
|
||||||
|
if(!file_exists($filePath)) {
|
||||||
|
throw new \Exception('File ' . $filePath . ' does not exist!');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once $filePath;
|
||||||
|
|
||||||
|
$class = $this->buildClassName($name);
|
||||||
|
|
||||||
|
if((new \ReflectionClass($class))->isInstantiable()) {
|
||||||
|
return new $class();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build class name from action name
|
||||||
|
*
|
||||||
|
* The class name consists of the action name with prefix "Action". The first
|
||||||
|
* character of the class name must be uppercase.
|
||||||
|
*
|
||||||
|
* Example: 'display' => 'DisplayAction'
|
||||||
|
*
|
||||||
|
* @param string $name The action name.
|
||||||
|
* @return string The class name.
|
||||||
|
*/
|
||||||
|
protected function buildClassName($name) {
|
||||||
|
return ucfirst(strtolower($name)) . 'Action';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build file path to the action class.
|
||||||
|
*
|
||||||
|
* @param string $name The action name.
|
||||||
|
* @return string Path to the action class.
|
||||||
|
*/
|
||||||
|
protected function buildFilePath($name) {
|
||||||
|
return $this->getWorkingDir() . $this->buildClassName($name) . '.php';
|
||||||
|
}
|
||||||
|
}
|
34
lib/ActionInterface.php
Normal file
34
lib/ActionInterface.php
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for action objects.
|
||||||
|
*/
|
||||||
|
interface ActionInterface {
|
||||||
|
/**
|
||||||
|
* Set user data for the action to consume.
|
||||||
|
*
|
||||||
|
* @param array $userData An associative array of user data.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function setUserData($userData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the action.
|
||||||
|
*
|
||||||
|
* Note: This function directly outputs data to the user.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function execute();
|
||||||
|
}
|
|
@ -213,7 +213,7 @@ class Bridge {
|
||||||
// Create initial whitelist or load from disk
|
// Create initial whitelist or load from disk
|
||||||
if (!file_exists(WHITELIST) && !empty(self::$whitelist)) {
|
if (!file_exists(WHITELIST) && !empty(self::$whitelist)) {
|
||||||
file_put_contents(WHITELIST, implode("\n", self::$whitelist));
|
file_put_contents(WHITELIST, implode("\n", self::$whitelist));
|
||||||
} else {
|
} elseif(file_exists(WHITELIST)) {
|
||||||
|
|
||||||
$contents = trim(file_get_contents(WHITELIST));
|
$contents = trim(file_get_contents(WHITELIST));
|
||||||
|
|
||||||
|
|
|
@ -207,6 +207,11 @@ This bridge is not fetching its content through a secure connection</div>';
|
||||||
* @return string The list input field
|
* @return string The list input field
|
||||||
*/
|
*/
|
||||||
private static function getListInput($entry, $id, $name) {
|
private static function getListInput($entry, $id, $name) {
|
||||||
|
if(isset($entry['required']) && $entry['required'] === true) {
|
||||||
|
Debug::log('The "required" attribute is not supported for lists.');
|
||||||
|
unset($entry['required']);
|
||||||
|
}
|
||||||
|
|
||||||
$list = '<select '
|
$list = '<select '
|
||||||
. self::getInputAttributes($entry)
|
. self::getInputAttributes($entry)
|
||||||
. ' id="'
|
. ' id="'
|
||||||
|
@ -267,6 +272,11 @@ This bridge is not fetching its content through a secure connection</div>';
|
||||||
* @return string The checkbox input field
|
* @return string The checkbox input field
|
||||||
*/
|
*/
|
||||||
private static function getCheckboxInput($entry, $id, $name) {
|
private static function getCheckboxInput($entry, $id, $name) {
|
||||||
|
if(isset($entry['required']) && $entry['required'] === true) {
|
||||||
|
Debug::log('The "required" attribute is not supported for checkboxes.');
|
||||||
|
unset($entry['required']);
|
||||||
|
}
|
||||||
|
|
||||||
return '<input '
|
return '<input '
|
||||||
. self::getInputAttributes($entry)
|
. self::getInputAttributes($entry)
|
||||||
. ' id="'
|
. ' id="'
|
||||||
|
|
|
@ -64,6 +64,8 @@ class Cache {
|
||||||
* @return object|bool The cache object or false if the class is not instantiable.
|
* @return object|bool The cache object or false if the class is not instantiable.
|
||||||
*/
|
*/
|
||||||
public static function create($name){
|
public static function create($name){
|
||||||
|
$name = self::sanitizeCacheName($name) . 'Cache';
|
||||||
|
|
||||||
if(!self::isCacheName($name)) {
|
if(!self::isCacheName($name)) {
|
||||||
throw new \InvalidArgumentException('Cache name invalid!');
|
throw new \InvalidArgumentException('Cache name invalid!');
|
||||||
}
|
}
|
||||||
|
@ -137,4 +139,75 @@ class Cache {
|
||||||
public static function isCacheName($name){
|
public static function isCacheName($name){
|
||||||
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
|
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of cache names from the working directory.
|
||||||
|
*
|
||||||
|
* The list is cached internally to allow for successive calls.
|
||||||
|
*
|
||||||
|
* @return array List of cache names
|
||||||
|
*/
|
||||||
|
public static function getCacheNames(){
|
||||||
|
|
||||||
|
static $cacheNames = array(); // Initialized on first call
|
||||||
|
|
||||||
|
if(empty($cacheNames)) {
|
||||||
|
$files = scandir(self::getWorkingDir());
|
||||||
|
|
||||||
|
if($files !== false) {
|
||||||
|
foreach($files as $file) {
|
||||||
|
if(preg_match('/^([^.]+)Cache\.php$/U', $file, $out)) {
|
||||||
|
$cacheNames[] = $out[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cacheNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sanitized cache name.
|
||||||
|
*
|
||||||
|
* The cache name can be specified in various ways:
|
||||||
|
* * The PHP file name (i.e. `FileCache.php`)
|
||||||
|
* * The PHP file name without file extension (i.e. `FileCache`)
|
||||||
|
* * The cache name (i.e. `file`)
|
||||||
|
*
|
||||||
|
* Casing is ignored (i.e. `FILE` and `fIlE` are the same).
|
||||||
|
*
|
||||||
|
* A cache file matching the given cache name must exist in the working
|
||||||
|
* directory!
|
||||||
|
*
|
||||||
|
* @param string $name The cache name
|
||||||
|
* @return string|null The sanitized cache name if the provided name is
|
||||||
|
* valid, null otherwise.
|
||||||
|
*/
|
||||||
|
protected static function sanitizeCacheName($name) {
|
||||||
|
|
||||||
|
if(is_string($name)) {
|
||||||
|
|
||||||
|
// Trim trailing '.php' if exists
|
||||||
|
if(preg_match('/(.+)(?:\.php)/', $name, $matches)) {
|
||||||
|
$name = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim trailing 'Cache' if exists
|
||||||
|
if(preg_match('/(.+)(?:Cache)$/i', $name, $matches)) {
|
||||||
|
$name = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The name is valid if a corresponding file is found on disk
|
||||||
|
if(in_array(strtolower($name), array_map('strtolower', self::getCacheNames()))) {
|
||||||
|
$index = array_search(strtolower($name), array_map('strtolower', self::getCacheNames()));
|
||||||
|
return self::getCacheNames()[$index];
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug::log('Invalid cache name specified: "' . $name . '"!');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Bad parameter
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ final class Configuration {
|
||||||
*
|
*
|
||||||
* @todo Replace this property by a constant.
|
* @todo Replace this property by a constant.
|
||||||
*/
|
*/
|
||||||
public static $VERSION = 'dev.2019-01-13';
|
public static $VERSION = 'dev.2019-03-17';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the configuration data.
|
* Holds the configuration data.
|
||||||
|
@ -179,6 +179,9 @@ final class Configuration {
|
||||||
/** Name of the proxy server */
|
/** Name of the proxy server */
|
||||||
define('PROXY_NAME', self::getConfig('proxy', 'name'));
|
define('PROXY_NAME', self::getConfig('proxy', 'name'));
|
||||||
|
|
||||||
|
if(!is_string(self::getConfig('cache', 'type')))
|
||||||
|
die('Parameter [cache] => "type" is not a valid string! Please check "config.ini.php"!');
|
||||||
|
|
||||||
if(!is_bool(self::getConfig('cache', 'custom_timeout')))
|
if(!is_bool(self::getConfig('cache', 'custom_timeout')))
|
||||||
die('Parameter [cache] => "custom_timeout" is not a valid Boolean! Please check "config.ini.php"!');
|
die('Parameter [cache] => "custom_timeout" is not a valid Boolean! Please check "config.ini.php"!');
|
||||||
|
|
||||||
|
|
70
lib/FactoryAbstract.php
Normal file
70
lib/FactoryAbstract.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for factories.
|
||||||
|
*/
|
||||||
|
abstract class FactoryAbstract {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the working directory
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $workingDir = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the working directory.
|
||||||
|
*
|
||||||
|
* @param string $dir The working directory.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setWorkingDir($dir) {
|
||||||
|
$this->workingDir = null;
|
||||||
|
|
||||||
|
if(!is_string($dir)) {
|
||||||
|
throw new \InvalidArgumentException('Working directory must be a string!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!file_exists($dir)) {
|
||||||
|
throw new \Exception('Working directory does not exist!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!is_dir($dir)) {
|
||||||
|
throw new \InvalidArgumentException($dir . ' is not a directory!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->workingDir = realpath($dir) . '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the working directory
|
||||||
|
*
|
||||||
|
* @return string The working directory.
|
||||||
|
*/
|
||||||
|
public function getWorkingDir() {
|
||||||
|
if(is_null($this->workingDir)) {
|
||||||
|
throw new \LogicException('Working directory is not set!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->workingDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance for the object specified by name.
|
||||||
|
*
|
||||||
|
* @param string $name The name of the object to create.
|
||||||
|
* @return object The object instance
|
||||||
|
*/
|
||||||
|
abstract public function create($name);
|
||||||
|
}
|
|
@ -265,7 +265,7 @@ abstract class FeedExpander extends BridgeAbstract {
|
||||||
|
|
||||||
//When "link" field is present, URL is more reliable than "id" field
|
//When "link" field is present, URL is more reliable than "id" field
|
||||||
if (count($feedItem->link) === 1) {
|
if (count($feedItem->link) === 1) {
|
||||||
$this->uri = (string)$feedItem->link[0]['href'];
|
$item['uri'] = (string)$feedItem->link[0]['href'];
|
||||||
} else {
|
} else {
|
||||||
foreach($feedItem->link as $link) {
|
foreach($feedItem->link as $link) {
|
||||||
if(strtolower($link['rel']) === 'alternate') {
|
if(strtolower($link['rel']) === 'alternate') {
|
||||||
|
|
|
@ -55,6 +55,9 @@ class FeedItem {
|
||||||
/** @var array List of category names or tags */
|
/** @var array List of category names or tags */
|
||||||
protected $categories = array();
|
protected $categories = array();
|
||||||
|
|
||||||
|
/** @var string Unique ID for the current item */
|
||||||
|
protected $uid = null;
|
||||||
|
|
||||||
/** @var array Associative list of additional parameters */
|
/** @var array Associative list of additional parameters */
|
||||||
protected $misc = array(); // Custom parameters
|
protected $misc = array(); // Custom parameters
|
||||||
|
|
||||||
|
@ -73,7 +76,7 @@ class FeedItem {
|
||||||
* $item['uri'] = 'https://www.github.com/rss-bridge/rss-bridge/';
|
* $item['uri'] = 'https://www.github.com/rss-bridge/rss-bridge/';
|
||||||
* $item['title'] = 'Title';
|
* $item['title'] = 'Title';
|
||||||
* $item['timestamp'] = strtotime('now');
|
* $item['timestamp'] = strtotime('now');
|
||||||
* $item['autor'] = 'Unknown author';
|
* $item['author'] = 'Unknown author';
|
||||||
* $item['content'] = 'Hello World!';
|
* $item['content'] = 'Hello World!';
|
||||||
* $item['enclosures'] = array('https://github.com/favicon.ico');
|
* $item['enclosures'] = array('https://github.com/favicon.ico');
|
||||||
* $item['categories'] = array('php', 'rss-bridge', 'awesome');
|
* $item['categories'] = array('php', 'rss-bridge', 'awesome');
|
||||||
|
@ -344,7 +347,7 @@ class FeedItem {
|
||||||
$enclosure,
|
$enclosure,
|
||||||
FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) {
|
FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) {
|
||||||
Debug::log('Each enclosure must contain a scheme, host and path!');
|
Debug::log('Each enclosure must contain a scheme, host and path!');
|
||||||
} else {
|
} elseif(!in_array($enclosure, $this->enclosures)) {
|
||||||
$this->enclosures[] = $enclosure;
|
$this->enclosures[] = $enclosure;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -391,6 +394,37 @@ class FeedItem {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get unique id
|
||||||
|
*
|
||||||
|
* Use {@see FeedItem::setUid()} to set the unique id.
|
||||||
|
*
|
||||||
|
* @param string The unique id.
|
||||||
|
*/
|
||||||
|
public function getUid() {
|
||||||
|
return $this->uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set unique id.
|
||||||
|
*
|
||||||
|
* Use {@see FeedItem::getUid()} to get the unique id.
|
||||||
|
*
|
||||||
|
* @param string $uid A string that uniquely identifies the current item
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function setUid($uid) {
|
||||||
|
$this->uid = null; // Clear previous data
|
||||||
|
|
||||||
|
if(!is_string($uid)) {
|
||||||
|
Debug::log('Unique id must be a string!');
|
||||||
|
} else {
|
||||||
|
$this->uid = sha1($uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add miscellaneous elements to the item.
|
* Add miscellaneous elements to the item.
|
||||||
*
|
*
|
||||||
|
@ -426,6 +460,7 @@ class FeedItem {
|
||||||
'content' => $this->content,
|
'content' => $this->content,
|
||||||
'enclosures' => $this->enclosures,
|
'enclosures' => $this->enclosures,
|
||||||
'categories' => $this->categories,
|
'categories' => $this->categories,
|
||||||
|
'uid' => $this->uid,
|
||||||
), $this->misc
|
), $this->misc
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -454,6 +489,7 @@ class FeedItem {
|
||||||
case 'content': $this->setContent($value); break;
|
case 'content': $this->setContent($value); break;
|
||||||
case 'enclosures': $this->setEnclosures($value); break;
|
case 'enclosures': $this->setEnclosures($value); break;
|
||||||
case 'categories': $this->setCategories($value); break;
|
case 'categories': $this->setCategories($value); break;
|
||||||
|
case 'uid': $this->setUid($value); break;
|
||||||
default: $this->addMisc($name, $value);
|
default: $this->addMisc($name, $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -476,6 +512,7 @@ class FeedItem {
|
||||||
case 'content': return $this->getContent();
|
case 'content': return $this->getContent();
|
||||||
case 'enclosures': return $this->getEnclosures();
|
case 'enclosures': return $this->getEnclosures();
|
||||||
case 'categories': return $this->getCategories();
|
case 'categories': return $this->getCategories();
|
||||||
|
case 'uid': return $this->getUid();
|
||||||
default:
|
default:
|
||||||
if(array_key_exists($name, $this->misc))
|
if(array_key_exists($name, $this->misc))
|
||||||
return $this->misc[$name];
|
return $this->misc[$name];
|
||||||
|
|
|
@ -195,13 +195,14 @@ class ParameterValidator {
|
||||||
foreach($set as $id => $properties) {
|
foreach($set as $id => $properties) {
|
||||||
if(isset($data[$id]) && !empty($data[$id])) {
|
if(isset($data[$id]) && !empty($data[$id])) {
|
||||||
$queriedContexts[$context] = true;
|
$queriedContexts[$context] = true;
|
||||||
} elseif(isset($properties['required'])
|
} elseif (isset($properties['type'])
|
||||||
&& $properties['required'] === true) {
|
&& ($properties['type'] === 'checkbox' || $properties['type'] === 'list')) {
|
||||||
|
continue;
|
||||||
|
} elseif(isset($properties['required']) && $properties['required'] === true) {
|
||||||
$queriedContexts[$context] = false;
|
$queriedContexts[$context] = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abort if one of the globally required parameters is not satisfied
|
// Abort if one of the globally required parameters is not satisfied
|
||||||
|
|
|
@ -45,7 +45,7 @@ function getContents($url, $header = array(), $opts = array()){
|
||||||
Debug::log('Reading contents from "' . $url . '"');
|
Debug::log('Reading contents from "' . $url . '"');
|
||||||
|
|
||||||
// Initialize cache
|
// Initialize cache
|
||||||
$cache = Cache::create('FileCache');
|
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
|
||||||
$cache->setPath(PATH_CACHE . 'server/');
|
$cache->setPath(PATH_CACHE . 'server/');
|
||||||
$cache->purgeCache(86400); // 24 hours (forced)
|
$cache->purgeCache(86400); // 24 hours (forced)
|
||||||
|
|
||||||
|
@ -207,14 +207,15 @@ EOD
|
||||||
* @return string Contents as simplehtmldom object.
|
* @return string Contents as simplehtmldom object.
|
||||||
*/
|
*/
|
||||||
function getSimpleHTMLDOM($url,
|
function getSimpleHTMLDOM($url,
|
||||||
$header = array(),
|
$header = array(),
|
||||||
$opts = array(),
|
$opts = array(),
|
||||||
$lowercase = true,
|
$lowercase = true,
|
||||||
$forceTagsClosed = true,
|
$forceTagsClosed = true,
|
||||||
$target_charset = DEFAULT_TARGET_CHARSET,
|
$target_charset = DEFAULT_TARGET_CHARSET,
|
||||||
$stripRN = true,
|
$stripRN = true,
|
||||||
$defaultBRText = DEFAULT_BR_TEXT,
|
$defaultBRText = DEFAULT_BR_TEXT,
|
||||||
$defaultSpanText = DEFAULT_SPAN_TEXT){
|
$defaultSpanText = DEFAULT_SPAN_TEXT){
|
||||||
|
|
||||||
$content = getContents($url, $header, $opts);
|
$content = getContents($url, $header, $opts);
|
||||||
return str_get_html($content,
|
return str_get_html($content,
|
||||||
$lowercase,
|
$lowercase,
|
||||||
|
@ -256,19 +257,20 @@ $defaultSpanText = DEFAULT_SPAN_TEXT){
|
||||||
* @return string Contents as simplehtmldom object.
|
* @return string Contents as simplehtmldom object.
|
||||||
*/
|
*/
|
||||||
function getSimpleHTMLDOMCached($url,
|
function getSimpleHTMLDOMCached($url,
|
||||||
$duration = 86400,
|
$duration = 86400,
|
||||||
$header = array(),
|
$header = array(),
|
||||||
$opts = array(),
|
$opts = array(),
|
||||||
$lowercase = true,
|
$lowercase = true,
|
||||||
$forceTagsClosed = true,
|
$forceTagsClosed = true,
|
||||||
$target_charset = DEFAULT_TARGET_CHARSET,
|
$target_charset = DEFAULT_TARGET_CHARSET,
|
||||||
$stripRN = true,
|
$stripRN = true,
|
||||||
$defaultBRText = DEFAULT_BR_TEXT,
|
$defaultBRText = DEFAULT_BR_TEXT,
|
||||||
$defaultSpanText = DEFAULT_SPAN_TEXT){
|
$defaultSpanText = DEFAULT_SPAN_TEXT){
|
||||||
|
|
||||||
Debug::log('Caching url ' . $url . ', duration ' . $duration);
|
Debug::log('Caching url ' . $url . ', duration ' . $duration);
|
||||||
|
|
||||||
// Initialize cache
|
// Initialize cache
|
||||||
$cache = Cache::create('FileCache');
|
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
|
||||||
$cache->setPath(PATH_CACHE . 'pages/');
|
$cache->setPath(PATH_CACHE . 'pages/');
|
||||||
$cache->purgeCache(86400); // 24 hours (forced)
|
$cache->purgeCache(86400); // 24 hours (forced)
|
||||||
|
|
||||||
|
|
|
@ -26,9 +26,10 @@
|
||||||
* already removes some of the tags (search for `remove_noise` in simple_html_dom.php).
|
* already removes some of the tags (search for `remove_noise` in simple_html_dom.php).
|
||||||
*/
|
*/
|
||||||
function sanitize($html,
|
function sanitize($html,
|
||||||
$tags_to_remove = array('script', 'iframe', 'input', 'form'),
|
$tags_to_remove = array('script', 'iframe', 'input', 'form'),
|
||||||
$attributes_to_keep = array('title', 'href', 'src'),
|
$attributes_to_keep = array('title', 'href', 'src'),
|
||||||
$text_to_keep = array()){
|
$text_to_keep = array()){
|
||||||
|
|
||||||
$htmlContent = str_get_html($html);
|
$htmlContent = str_get_html($html);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -29,6 +29,9 @@ define('PATH_LIB_FORMATS', __DIR__ . '/../formats/');
|
||||||
/** Path to the caches library */
|
/** Path to the caches library */
|
||||||
define('PATH_LIB_CACHES', __DIR__ . '/../caches/');
|
define('PATH_LIB_CACHES', __DIR__ . '/../caches/');
|
||||||
|
|
||||||
|
/** Path to the actions library */
|
||||||
|
define('PATH_LIB_ACTIONS', __DIR__ . '/../actions/');
|
||||||
|
|
||||||
/** Path to the cache folder */
|
/** Path to the cache folder */
|
||||||
define('PATH_CACHE', __DIR__ . '/../cache/');
|
define('PATH_CACHE', __DIR__ . '/../cache/');
|
||||||
|
|
||||||
|
@ -39,11 +42,13 @@ define('WHITELIST', __DIR__ . '/../whitelist.txt');
|
||||||
define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/');
|
define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/');
|
||||||
|
|
||||||
// Interfaces
|
// Interfaces
|
||||||
|
require_once PATH_LIB . 'ActionInterface.php';
|
||||||
require_once PATH_LIB . 'BridgeInterface.php';
|
require_once PATH_LIB . 'BridgeInterface.php';
|
||||||
require_once PATH_LIB . 'CacheInterface.php';
|
require_once PATH_LIB . 'CacheInterface.php';
|
||||||
require_once PATH_LIB . 'FormatInterface.php';
|
require_once PATH_LIB . 'FormatInterface.php';
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
|
require_once PATH_LIB . 'FactoryAbstract.php';
|
||||||
require_once PATH_LIB . 'FeedItem.php';
|
require_once PATH_LIB . 'FeedItem.php';
|
||||||
require_once PATH_LIB . 'Debug.php';
|
require_once PATH_LIB . 'Debug.php';
|
||||||
require_once PATH_LIB . 'Exceptions.php';
|
require_once PATH_LIB . 'Exceptions.php';
|
||||||
|
@ -58,6 +63,8 @@ require_once PATH_LIB . 'Configuration.php';
|
||||||
require_once PATH_LIB . 'BridgeCard.php';
|
require_once PATH_LIB . 'BridgeCard.php';
|
||||||
require_once PATH_LIB . 'BridgeList.php';
|
require_once PATH_LIB . 'BridgeList.php';
|
||||||
require_once PATH_LIB . 'ParameterValidator.php';
|
require_once PATH_LIB . 'ParameterValidator.php';
|
||||||
|
require_once PATH_LIB . 'ActionFactory.php';
|
||||||
|
require_once PATH_LIB . 'ActionAbstract.php';
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
require_once PATH_LIB . 'html.php';
|
require_once PATH_LIB . 'html.php';
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
<testsuite name="formats">
|
<testsuite name="formats">
|
||||||
<directory suffix="FormatTest.php">tests</directory>
|
<directory suffix="FormatTest.php">tests</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
|
<testsuite name="actions">
|
||||||
|
<directory suffix="ActionTest.php">tests</directory>
|
||||||
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|
|
@ -32,7 +32,6 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
|
||||||
width: 60%;
|
width: 60%;
|
||||||
margin: 30px auto;
|
margin: 30px auto;
|
||||||
padding: 15px 15px;
|
padding: 15px 15px;
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.09);
|
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.09);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
@ -59,6 +58,18 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
|
||||||
}
|
}
|
||||||
section > div.content, section > div.attachments {
|
section > div.content, section > div.attachments {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
}
|
||||||
|
section h1, section h2, section h3, section b, section strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
section i, section em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
section p:not(:last-child) {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
section li {
|
||||||
|
margin-left: 1em;
|
||||||
}
|
}
|
||||||
section > div.attachments > li.enclosure {
|
section > div.attachments > li.enclosure {
|
||||||
list-style-type: circle;
|
list-style-type: circle;
|
||||||
|
@ -84,4 +95,4 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
background: #49afff;
|
background: #49afff;
|
||||||
}
|
}
|
||||||
|
|
59
tests/ActionImplementationTest.php
Normal file
59
tests/ActionImplementationTest.php
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../lib/rssbridge.php';
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ActionImplementationTest extends TestCase {
|
||||||
|
private $class;
|
||||||
|
private $obj;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataActionsProvider
|
||||||
|
*/
|
||||||
|
public function testClassName($path) {
|
||||||
|
$this->setAction($path);
|
||||||
|
$this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
|
||||||
|
$this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
|
||||||
|
$this->assertStringEndsWith('Action', $this->class, 'class name must end with "Action"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataActionsProvider
|
||||||
|
*/
|
||||||
|
public function testClassType($path) {
|
||||||
|
$this->setAction($path);
|
||||||
|
$this->assertInstanceOf(ActionInterface::class, $this->obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataActionsProvider
|
||||||
|
*/
|
||||||
|
public function testVisibleMethods($path) {
|
||||||
|
$allowedActionAbstract = get_class_methods(ActionAbstract::class);
|
||||||
|
sort($allowedActionAbstract);
|
||||||
|
|
||||||
|
$this->setAction($path);
|
||||||
|
|
||||||
|
$methods = get_class_methods($this->obj);
|
||||||
|
sort($methods);
|
||||||
|
|
||||||
|
$this->assertEquals($allowedActionAbstract, $methods);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public function dataActionsProvider() {
|
||||||
|
$actions = array();
|
||||||
|
foreach (glob(PATH_LIB_ACTIONS . '*.php') as $path) {
|
||||||
|
$actions[basename($path, '.php')] = array($path);
|
||||||
|
}
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setAction($path) {
|
||||||
|
require_once $path;
|
||||||
|
$this->class = basename($path, '.php');
|
||||||
|
$this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
|
||||||
|
$this->obj = new $this->class();
|
||||||
|
}
|
||||||
|
}
|
89
tests/AtomFormatTest.php
Normal file
89
tests/AtomFormatTest.php
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AtomFormat - RFC 4287: The Atom Syndication Format
|
||||||
|
* https://tools.ietf.org/html/rfc4287
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../lib/rssbridge.php';
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class AtomFormatTest extends TestCase {
|
||||||
|
const PATH_SAMPLES = __DIR__ . '/samples/';
|
||||||
|
const PATH_EXPECTED = __DIR__ . '/samples/expectedAtomFormat/';
|
||||||
|
|
||||||
|
private $sample;
|
||||||
|
private $format;
|
||||||
|
private $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider sampleProvider
|
||||||
|
* @runInSeparateProcess
|
||||||
|
* @requires function xdebug_get_headers
|
||||||
|
*/
|
||||||
|
public function testHeaders($path) {
|
||||||
|
$this->setSample($path);
|
||||||
|
$this->initFormat();
|
||||||
|
|
||||||
|
$this->assertContains(
|
||||||
|
'Content-Type: application/atom+xml; charset=' . $this->format->getCharset(),
|
||||||
|
xdebug_get_headers()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider sampleProvider
|
||||||
|
* @runInSeparateProcess
|
||||||
|
*/
|
||||||
|
public function testOutput($path) {
|
||||||
|
$this->setSample($path);
|
||||||
|
$this->initFormat();
|
||||||
|
|
||||||
|
$this->assertXmlStringEqualsXmlFile($this->sample->expected, $this->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public function sampleProvider() {
|
||||||
|
$samples = array();
|
||||||
|
foreach (glob(self::PATH_SAMPLES . '*.json') as $path) {
|
||||||
|
$samples[basename($path, '.json')] = array($path);
|
||||||
|
}
|
||||||
|
return $samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setSample($path) {
|
||||||
|
$data = json_decode(file_get_contents($path), true);
|
||||||
|
if (isset($data['meta']) && isset($data['items'])) {
|
||||||
|
if (!empty($data['server']))
|
||||||
|
$this->setServerVars($data['server']);
|
||||||
|
|
||||||
|
$items = array();
|
||||||
|
foreach($data['items'] as $item) {
|
||||||
|
$items[] = new \FeedItem($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sample = (object)array(
|
||||||
|
'meta' => $data['meta'],
|
||||||
|
'items' => $items,
|
||||||
|
'expected' => self::PATH_EXPECTED . basename($path, '.json') . '.xml'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fail('invalid test sample: ' . basename($path, '.json'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setServerVars($list) {
|
||||||
|
$_SERVER = array_merge($_SERVER, $list);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initFormat() {
|
||||||
|
$this->format = \Format::create('Atom');
|
||||||
|
$this->format->setItems($this->sample->items);
|
||||||
|
$this->format->setExtraInfos($this->sample->meta);
|
||||||
|
$this->format->setLastModified(strtotime('2000-01-01 12:00:00 UTC'));
|
||||||
|
|
||||||
|
$this->data = $this->getActualOutput($this->format->display());
|
||||||
|
$this->assertNotFalse(simplexml_load_string($this->data));
|
||||||
|
ob_clean();
|
||||||
|
}
|
||||||
|
}
|
|
@ -98,6 +98,19 @@ class BridgeImplementationTest extends TestCase {
|
||||||
|
|
||||||
if (isset($options['required'])) {
|
if (isset($options['required'])) {
|
||||||
$this->assertInternalType('bool', $options['required'], $field . ': invalid required');
|
$this->assertInternalType('bool', $options['required'], $field . ': invalid required');
|
||||||
|
|
||||||
|
if($options['required'] === true && isset($options['type'])) {
|
||||||
|
switch($options['type']) {
|
||||||
|
case 'list':
|
||||||
|
case 'checkbox':
|
||||||
|
$this->assertArrayNotHasKey(
|
||||||
|
'required',
|
||||||
|
$options,
|
||||||
|
$field . ': "required" attribute not supported for ' . $options['type']
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($options['title'])) {
|
if (isset($options['title'])) {
|
||||||
|
|
42
tests/CacheImplementationTest.php
Normal file
42
tests/CacheImplementationTest.php
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../lib/rssbridge.php';
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class CacheImplementationTest extends TestCase {
|
||||||
|
private $class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataCachesProvider
|
||||||
|
*/
|
||||||
|
public function testClassName($path) {
|
||||||
|
$this->setCache($path);
|
||||||
|
$this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
|
||||||
|
$this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
|
||||||
|
$this->assertStringEndsWith('Cache', $this->class, 'class name must end with "Cache"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataCachesProvider
|
||||||
|
*/
|
||||||
|
public function testClassType($path) {
|
||||||
|
$this->setCache($path);
|
||||||
|
$this->assertTrue(is_subclass_of($this->class, CacheInterface::class), 'class must be subclass of CacheInterface');
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public function dataCachesProvider() {
|
||||||
|
$caches = array();
|
||||||
|
foreach (glob(PATH_LIB_CACHES . '*.php') as $path) {
|
||||||
|
$caches[basename($path, '.php')] = array($path);
|
||||||
|
}
|
||||||
|
return $caches;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setCache($path) {
|
||||||
|
require_once $path;
|
||||||
|
$this->class = basename($path, '.php');
|
||||||
|
$this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
|
||||||
|
}
|
||||||
|
}
|
44
tests/FormatImplementationTest.php
Normal file
44
tests/FormatImplementationTest.php
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../lib/rssbridge.php';
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class FormatImplementationTest extends TestCase {
|
||||||
|
private $class;
|
||||||
|
private $obj;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataFormatsProvider
|
||||||
|
*/
|
||||||
|
public function testClassName($path) {
|
||||||
|
$this->setFormat($path);
|
||||||
|
$this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
|
||||||
|
$this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
|
||||||
|
$this->assertStringEndsWith('Format', $this->class, 'class name must end with "Format"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataFormatsProvider
|
||||||
|
*/
|
||||||
|
public function testClassType($path) {
|
||||||
|
$this->setFormat($path);
|
||||||
|
$this->assertInstanceOf(FormatInterface::class, $this->obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public function dataFormatsProvider() {
|
||||||
|
$formats = array();
|
||||||
|
foreach (glob(PATH_LIB_FORMATS . '*.php') as $path) {
|
||||||
|
$formats[basename($path, '.php')] = array($path);
|
||||||
|
}
|
||||||
|
return $formats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setFormat($path) {
|
||||||
|
require_once $path;
|
||||||
|
$this->class = basename($path, '.php');
|
||||||
|
$this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
|
||||||
|
$this->obj = new $this->class();
|
||||||
|
}
|
||||||
|
}
|
90
tests/ListActionTest.php
Normal file
90
tests/ListActionTest.php
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../lib/rssbridge.php';
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ListActionTest extends TestCase {
|
||||||
|
|
||||||
|
private $action;
|
||||||
|
private $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @runInSeparateProcess
|
||||||
|
* @requires function xdebug_get_headers
|
||||||
|
*/
|
||||||
|
public function testHeaders() {
|
||||||
|
$this->initAction();
|
||||||
|
|
||||||
|
$this->assertContains(
|
||||||
|
'Content-Type: application/json',
|
||||||
|
xdebug_get_headers()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @runInSeparateProcess
|
||||||
|
*/
|
||||||
|
public function testOutput() {
|
||||||
|
$this->initAction();
|
||||||
|
|
||||||
|
$items = json_decode($this->data, true);
|
||||||
|
|
||||||
|
$this->assertNotNull($items, 'invalid JSON output: ' . json_last_error_msg());
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('total', $items, 'Missing "total" parameter');
|
||||||
|
$this->assertInternalType('int', $items['total'], 'Invalid type');
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('bridges', $items, 'Missing "bridges" array');
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
$items['total'],
|
||||||
|
count($items['bridges']),
|
||||||
|
'Item count doesn\'t match'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
count(Bridge::getBridgeNames()),
|
||||||
|
count($items['bridges']),
|
||||||
|
'Number of bridges doesn\'t match'
|
||||||
|
);
|
||||||
|
|
||||||
|
$expectedKeys = array(
|
||||||
|
'status',
|
||||||
|
'uri',
|
||||||
|
'name',
|
||||||
|
'icon',
|
||||||
|
'parameters',
|
||||||
|
'maintainer',
|
||||||
|
'description'
|
||||||
|
);
|
||||||
|
|
||||||
|
$allowedStatus = array(
|
||||||
|
'active',
|
||||||
|
'inactive'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach($items['bridges'] as $bridge) {
|
||||||
|
foreach($expectedKeys as $key) {
|
||||||
|
$this->assertArrayHasKey($key, $bridge, 'Missing key "' . $key . '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertContains($bridge['status'], $allowedStatus, 'Invalid status value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private function initAction() {
|
||||||
|
$actionFac = new ActionFactory();
|
||||||
|
$actionFac->setWorkingDir(PATH_LIB_ACTIONS);
|
||||||
|
|
||||||
|
$this->action = $actionFac->create('list');
|
||||||
|
$this->action->setUserData(array()); /* no user data required */
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$this->action->execute();
|
||||||
|
$this->data = ob_get_contents();
|
||||||
|
ob_clean();
|
||||||
|
ob_end_flush();
|
||||||
|
}
|
||||||
|
}
|
90
tests/MrssFormatTest.php
Normal file
90
tests/MrssFormatTest.php
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* MrssFormat - RSS 2.0 + Media RSS
|
||||||
|
* http://www.rssboard.org/rss-specification
|
||||||
|
* http://www.rssboard.org/media-rss
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../lib/rssbridge.php';
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class MrssFormatTest extends TestCase {
|
||||||
|
const PATH_SAMPLES = __DIR__ . '/samples/';
|
||||||
|
const PATH_EXPECTED = __DIR__ . '/samples/expectedMrssFormat/';
|
||||||
|
|
||||||
|
private $sample;
|
||||||
|
private $format;
|
||||||
|
private $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider sampleProvider
|
||||||
|
* @runInSeparateProcess
|
||||||
|
* @requires function xdebug_get_headers
|
||||||
|
*/
|
||||||
|
public function testHeaders($path) {
|
||||||
|
$this->setSample($path);
|
||||||
|
$this->initFormat();
|
||||||
|
|
||||||
|
$this->assertContains(
|
||||||
|
'Content-Type: application/rss+xml; charset=' . $this->format->getCharset(),
|
||||||
|
xdebug_get_headers()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider sampleProvider
|
||||||
|
* @runInSeparateProcess
|
||||||
|
*/
|
||||||
|
public function testOutput($path) {
|
||||||
|
$this->setSample($path);
|
||||||
|
$this->initFormat();
|
||||||
|
|
||||||
|
$this->assertXmlStringEqualsXmlFile($this->sample->expected, $this->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public function sampleProvider() {
|
||||||
|
$samples = array();
|
||||||
|
foreach (glob(self::PATH_SAMPLES . '*.json') as $path) {
|
||||||
|
$samples[basename($path, '.json')] = array($path);
|
||||||
|
}
|
||||||
|
return $samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setSample($path) {
|
||||||
|
$data = json_decode(file_get_contents($path), true);
|
||||||
|
if (isset($data['meta']) && isset($data['items'])) {
|
||||||
|
if (!empty($data['server']))
|
||||||
|
$this->setServerVars($data['server']);
|
||||||
|
|
||||||
|
$items = array();
|
||||||
|
foreach($data['items'] as $item) {
|
||||||
|
$items[] = new \FeedItem($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sample = (object)array(
|
||||||
|
'meta' => $data['meta'],
|
||||||
|
'items' => $items,
|
||||||
|
'expected' => self::PATH_EXPECTED . basename($path, '.json') . '.xml'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fail('invalid test sample: ' . basename($path, '.json'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setServerVars($list) {
|
||||||
|
$_SERVER = array_merge($_SERVER, $list);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initFormat() {
|
||||||
|
$this->format = \Format::create('Mrss');
|
||||||
|
$this->format->setItems($this->sample->items);
|
||||||
|
$this->format->setExtraInfos($this->sample->meta);
|
||||||
|
$this->format->setLastModified(strtotime('2000-01-01 12:00:00 UTC'));
|
||||||
|
|
||||||
|
$this->data = $this->getActualOutput($this->format->display());
|
||||||
|
$this->assertNotFalse(simplexml_load_string($this->data));
|
||||||
|
ob_clean();
|
||||||
|
}
|
||||||
|
}
|
77
tests/samples/expectedAtomFormat/feed.common.xml
Normal file
77
tests/samples/expectedAtomFormat/feed.common.xml
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
|
||||||
|
<title type="text">Sample feed with common data</title>
|
||||||
|
<id>https://example.com/feed?type=common&items=4</id>
|
||||||
|
<icon>https://example.com/logo.png</icon>
|
||||||
|
<logo>https://example.com/logo.png</logo>
|
||||||
|
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||||
|
<author>
|
||||||
|
<name>RSS-Bridge</name>
|
||||||
|
</author>
|
||||||
|
<link href="https://example.com/blog/" rel="alternate" type="text/html"/>
|
||||||
|
<link href="https://example.com/feed?type=common&items=4" rel="self" type="application/atom+xml"/>
|
||||||
|
|
||||||
|
<entry>
|
||||||
|
<title type="html">Test Entry</title>
|
||||||
|
<published>2018-12-01T12:00:00+00:00</published>
|
||||||
|
<updated>2018-12-01T12:00:00+00:00</updated>
|
||||||
|
<id>http://example.com/blog/test-entry</id>
|
||||||
|
<link href="http://example.com/blog/test-entry" rel="alternate" type="text/html"/>
|
||||||
|
<author>
|
||||||
|
<name>fulmeek</name>
|
||||||
|
</author>
|
||||||
|
<content type="html">Hello world, this is a test entry.</content>
|
||||||
|
<category term="test"/>
|
||||||
|
<category term="Hello World"/>
|
||||||
|
<category term="example"/>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title type="html">Announcing JSON Feed</title>
|
||||||
|
<published>2017-05-17T13:02:12+00:00</published>
|
||||||
|
<updated>2017-05-17T13:02:12+00:00</updated>
|
||||||
|
<id>https://jsonfeed.org/2017/05/17/announcing_json_feed</id>
|
||||||
|
<link href="https://jsonfeed.org/2017/05/17/announcing_json_feed" rel="alternate" type="text/html"/>
|
||||||
|
<author>
|
||||||
|
<name>Brent Simmons and Manton Reece</name>
|
||||||
|
</author>
|
||||||
|
<content type="html"><p>We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers’ choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and it’s less prone to bugs.</p>
|
||||||
|
|
||||||
|
<p>So we developed JSON Feed, a format similar to <a href="http://cyber.harvard.edu/rss/rss.html">RSS</a> and <a href="https://tools.ietf.org/html/rfc4287">Atom</a> but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.</p>
|
||||||
|
|
||||||
|
<p><a href="https://jsonfeed.org/version/1">See the spec</a>. It’s at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.</p>
|
||||||
|
|
||||||
|
<h4>Notes</h4>
|
||||||
|
|
||||||
|
<p>We have a <a href="https://github.com/manton/jsonfeed-wp">WordPress plugin</a> and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, we’ll update the <a href="https://jsonfeed.org/code">code</a> page.</p>
|
||||||
|
|
||||||
|
<p>See <a href="https://jsonfeed.org/mappingrssandatom">Mapping RSS and Atom to JSON Feed</a> for more on the similarities between the formats.</p>
|
||||||
|
|
||||||
|
<p>This website — the Markdown files and supporting resources — <a href="https://github.com/brentsimmons/JSONFeed">is up on GitHub</a>, and you’re welcome to comment there.</p>
|
||||||
|
|
||||||
|
<p>This website is also a blog, and you can subscribe to the <a href="https://jsonfeed.org/xml/rss.xml">RSS feed</a> or the <a href="https://jsonfeed.org/feed.json">JSON feed</a> (if your reader supports it).</p>
|
||||||
|
|
||||||
|
<p>We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the <a href="https://jsonfeed.org/version/1">spec</a>. But — most importantly — <a href="http://furbo.org/">Craig Hockenberry</a> spent a little time making it look pretty. :)</p></content>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title type="html">Atom draft-07 snapshot</title>
|
||||||
|
<published>2005-07-31T12:29:29+00:00</published>
|
||||||
|
<updated>2005-07-31T12:29:29+00:00</updated>
|
||||||
|
<id>urn:sha1:dd6b6c920d3b340ab9e07faf6682f2a7c4f70134</id>
|
||||||
|
<link href="http://example.org/2005/04/02/atom" rel="alternate" type="text/html"/>
|
||||||
|
<author>
|
||||||
|
<name>Mark Pilgrim</name>
|
||||||
|
</author>
|
||||||
|
<content type="html"><p><i>[Update: The Atom draft is finished.]</i></p></content>
|
||||||
|
<link rel="enclosure" type="audio/mpeg" href="http://example.org/audio/ph34r_my_podcast.mp3"/>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title type="html">Star City</title>
|
||||||
|
<published>2003-06-03T09:39:21+00:00</published>
|
||||||
|
<updated>2003-06-03T09:39:21+00:00</updated>
|
||||||
|
<id>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</id>
|
||||||
|
<link href="http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp" rel="alternate" type="text/html"/>
|
||||||
|
<content type="html">How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.</content>
|
||||||
|
</entry>
|
||||||
|
|
||||||
|
</feed>
|
15
tests/samples/expectedAtomFormat/feed.empty.xml
Normal file
15
tests/samples/expectedAtomFormat/feed.empty.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
|
||||||
|
<title type="text">Sample feed with minimum data</title>
|
||||||
|
<id>https://example.com/feed</id>
|
||||||
|
<icon>https://github.com/favicon.ico</icon>
|
||||||
|
<logo>https://github.com/favicon.ico</logo>
|
||||||
|
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||||
|
<author>
|
||||||
|
<name>RSS-Bridge</name>
|
||||||
|
</author>
|
||||||
|
<link href="https://github.com/RSS-Bridge/rss-bridge/" rel="alternate" type="text/html"/>
|
||||||
|
<link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
|
||||||
|
|
||||||
|
</feed>
|
30
tests/samples/expectedAtomFormat/feed.emptyItems.xml
Normal file
30
tests/samples/expectedAtomFormat/feed.emptyItems.xml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
|
||||||
|
<title type="text">Sample feed with minimum data</title>
|
||||||
|
<id>https://example.com/feed</id>
|
||||||
|
<icon>https://github.com/favicon.ico</icon>
|
||||||
|
<logo>https://github.com/favicon.ico</logo>
|
||||||
|
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||||
|
<author>
|
||||||
|
<name>RSS-Bridge</name>
|
||||||
|
</author>
|
||||||
|
<link href="https://github.com/RSS-Bridge/rss-bridge/" rel="alternate" type="text/html"/>
|
||||||
|
<link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
|
||||||
|
|
||||||
|
<entry>
|
||||||
|
<title type="html">Sample Item #1</title>
|
||||||
|
<published>2000-01-01T12:00:00+00:00</published>
|
||||||
|
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||||
|
<id>urn:sha1:29f59918d266c56a935da13e4122b524298e5a39</id>
|
||||||
|
<content type="html">Sample Item #1</content>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title type="html">Sample Item #2</title>
|
||||||
|
<published>2000-01-01T12:00:00+00:00</published>
|
||||||
|
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||||
|
<id>urn:sha1:edf358cad1a7ae255d6bc97640dd9d27738f1b7b</id>
|
||||||
|
<content type="html">Sample Item #2</content>
|
||||||
|
</entry>
|
||||||
|
|
||||||
|
</feed>
|
30
tests/samples/expectedAtomFormat/feed.microblog.xml
Normal file
30
tests/samples/expectedAtomFormat/feed.microblog.xml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
|
||||||
|
<title type="text">Sample microblog feed</title>
|
||||||
|
<id>https://example.com/feed</id>
|
||||||
|
<icon>https://example.com/logo.png</icon>
|
||||||
|
<logo>https://example.com/logo.png</logo>
|
||||||
|
<updated>2000-01-01T12:00:00+00:00</updated>
|
||||||
|
<author>
|
||||||
|
<name>RSS-Bridge</name>
|
||||||
|
</author>
|
||||||
|
<link href="https://example.com/blog/" rel="alternate" type="text/html"/>
|
||||||
|
<link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
|
||||||
|
|
||||||
|
<entry>
|
||||||
|
<title type="html">Oh 😲 I found three monkeys 🙈🙉🙊</title>
|
||||||
|
<published>2018-10-07T16:53:03+00:00</published>
|
||||||
|
<updated>2018-10-07T16:53:03+00:00</updated>
|
||||||
|
<id>urn:sha1:1918f084648b82057c1dd3faa3d091da82a6fac2</id>
|
||||||
|
<content type="html">Oh 😲 I found three monkeys 🙈🙉🙊</content>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title type="html">Something happened</title>
|
||||||
|
<published>2018-10-07T16:38:17+00:00</published>
|
||||||
|
<updated>2018-10-07T16:38:17+00:00</updated>
|
||||||
|
<id>urn:sha1:e62189168a06dfa74f61c621c79c33c4c8517e1f</id>
|
||||||
|
<content type="html">Something happened</content>
|
||||||
|
</entry>
|
||||||
|
|
||||||
|
</feed>
|
|
@ -26,7 +26,7 @@
|
||||||
},
|
},
|
||||||
"content_html": "<p>We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers’ choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and it’s less prone to bugs.</p>\n\n<p>So we developed JSON Feed, a format similar to <a href=\"http://cyber.harvard.edu/rss/rss.html\">RSS</a> and <a href=\"https://tools.ietf.org/html/rfc4287\">Atom</a> but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.</p>\n\n<p><a href=\"https://jsonfeed.org/version/1\">See the spec</a>. It’s at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.</p>\n\n<h4>Notes</h4>\n\n<p>We have a <a href=\"https://github.com/manton/jsonfeed-wp\">WordPress plugin</a> and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, we’ll update the <a href=\"https://jsonfeed.org/code\">code</a> page.</p>\n\n<p>See <a href=\"https://jsonfeed.org/mappingrssandatom\">Mapping RSS and Atom to JSON Feed</a> for more on the similarities between the formats.</p>\n\n<p>This website — the Markdown files and supporting resources — <a href=\"https://github.com/brentsimmons/JSONFeed\">is up on GitHub</a>, and you’re welcome to comment there.</p>\n\n<p>This website is also a blog, and you can subscribe to the <a href=\"https://jsonfeed.org/xml/rss.xml\">RSS feed</a> or the <a href=\"https://jsonfeed.org/feed.json\">JSON feed</a> (if your reader supports it).</p>\n\n<p>We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the <a href=\"https://jsonfeed.org/version/1\">spec</a>. But — most importantly — <a href=\"http://furbo.org/\">Craig Hockenberry</a> spent a little time making it look pretty. :)</p>"
|
"content_html": "<p>We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers’ choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and it’s less prone to bugs.</p>\n\n<p>So we developed JSON Feed, a format similar to <a href=\"http://cyber.harvard.edu/rss/rss.html\">RSS</a> and <a href=\"https://tools.ietf.org/html/rfc4287\">Atom</a> but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.</p>\n\n<p><a href=\"https://jsonfeed.org/version/1\">See the spec</a>. It’s at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.</p>\n\n<h4>Notes</h4>\n\n<p>We have a <a href=\"https://github.com/manton/jsonfeed-wp\">WordPress plugin</a> and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, we’ll update the <a href=\"https://jsonfeed.org/code\">code</a> page.</p>\n\n<p>See <a href=\"https://jsonfeed.org/mappingrssandatom\">Mapping RSS and Atom to JSON Feed</a> for more on the similarities between the formats.</p>\n\n<p>This website — the Markdown files and supporting resources — <a href=\"https://github.com/brentsimmons/JSONFeed\">is up on GitHub</a>, and you’re welcome to comment there.</p>\n\n<p>This website is also a blog, and you can subscribe to the <a href=\"https://jsonfeed.org/xml/rss.xml\">RSS feed</a> or the <a href=\"https://jsonfeed.org/feed.json\">JSON feed</a> (if your reader supports it).</p>\n\n<p>We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the <a href=\"https://jsonfeed.org/version/1\">spec</a>. But — most importantly — <a href=\"http://furbo.org/\">Craig Hockenberry</a> spent a little time making it look pretty. :)</p>"
|
||||||
},{
|
},{
|
||||||
"id": "http://example.org/2005/04/02/atom",
|
"id": "dd6b6c920d3b340ab9e07faf6682f2a7c4f70134",
|
||||||
"url": "http://example.org/2005/04/02/atom",
|
"url": "http://example.org/2005/04/02/atom",
|
||||||
"title": "Atom draft-07 snapshot",
|
"title": "Atom draft-07 snapshot",
|
||||||
"date_modified": "2005-07-31T12:29:29+00:00",
|
"date_modified": "2005-07-31T12:29:29+00:00",
|
||||||
|
|
64
tests/samples/expectedMrssFormat/feed.common.xml
Normal file
64
tests/samples/expectedMrssFormat/feed.common.xml
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
|
||||||
|
<channel>
|
||||||
|
<title>Sample feed with common data</title>
|
||||||
|
<link>https://example.com/blog/</link>
|
||||||
|
<description>Sample feed with common data</description>
|
||||||
|
<image>
|
||||||
|
<url>https://example.com/logo.png</url>
|
||||||
|
<title>Sample feed with common data</title>
|
||||||
|
<link>https://example.com/blog/</link>
|
||||||
|
</image>
|
||||||
|
<atom:link href="https://example.com/blog/" rel="alternate" type="text/html"/>
|
||||||
|
<atom:link href="https://example.com/feed?type=common&items=4" rel="self" type="application/atom+xml"/>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>Test Entry</title>
|
||||||
|
<link>http://example.com/blog/test-entry</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/blog/test-entry</guid>
|
||||||
|
<pubDate>Sat, 01 Dec 2018 12:00:00 +0000</pubDate>
|
||||||
|
<description>Hello world, this is a test entry.</description>
|
||||||
|
<category>test</category>
|
||||||
|
<category>Hello World</category>
|
||||||
|
<category>example</category>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Announcing JSON Feed</title>
|
||||||
|
<link>https://jsonfeed.org/2017/05/17/announcing_json_feed</link>
|
||||||
|
<guid isPermaLink="true">https://jsonfeed.org/2017/05/17/announcing_json_feed</guid>
|
||||||
|
<pubDate>Wed, 17 May 2017 13:02:12 +0000</pubDate>
|
||||||
|
<description><p>We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers’ choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and it’s less prone to bugs.</p>
|
||||||
|
|
||||||
|
<p>So we developed JSON Feed, a format similar to <a href="http://cyber.harvard.edu/rss/rss.html">RSS</a> and <a href="https://tools.ietf.org/html/rfc4287">Atom</a> but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.</p>
|
||||||
|
|
||||||
|
<p><a href="https://jsonfeed.org/version/1">See the spec</a>. It’s at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.</p>
|
||||||
|
|
||||||
|
<h4>Notes</h4>
|
||||||
|
|
||||||
|
<p>We have a <a href="https://github.com/manton/jsonfeed-wp">WordPress plugin</a> and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, we’ll update the <a href="https://jsonfeed.org/code">code</a> page.</p>
|
||||||
|
|
||||||
|
<p>See <a href="https://jsonfeed.org/mappingrssandatom">Mapping RSS and Atom to JSON Feed</a> for more on the similarities between the formats.</p>
|
||||||
|
|
||||||
|
<p>This website — the Markdown files and supporting resources — <a href="https://github.com/brentsimmons/JSONFeed">is up on GitHub</a>, and you’re welcome to comment there.</p>
|
||||||
|
|
||||||
|
<p>This website is also a blog, and you can subscribe to the <a href="https://jsonfeed.org/xml/rss.xml">RSS feed</a> or the <a href="https://jsonfeed.org/feed.json">JSON feed</a> (if your reader supports it).</p>
|
||||||
|
|
||||||
|
<p>We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the <a href="https://jsonfeed.org/version/1">spec</a>. But — most importantly — <a href="http://furbo.org/">Craig Hockenberry</a> spent a little time making it look pretty. :)</p></description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Atom draft-07 snapshot</title>
|
||||||
|
<link>http://example.org/2005/04/02/atom</link>
|
||||||
|
<guid isPermaLink="false">dd6b6c920d3b340ab9e07faf6682f2a7c4f70134</guid>
|
||||||
|
<pubDate>Sun, 31 Jul 2005 12:29:29 +0000</pubDate>
|
||||||
|
<description><p><i>[Update: The Atom draft is finished.]</i></p></description>
|
||||||
|
<media:content url="http://example.org/audio/ph34r_my_podcast.mp3" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Star City</title>
|
||||||
|
<link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>
|
||||||
|
<guid isPermaLink="true">http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</guid>
|
||||||
|
<pubDate>Tue, 03 Jun 2003 09:39:21 +0000</pubDate>
|
||||||
|
<description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.</description>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
10
tests/samples/expectedMrssFormat/feed.empty.xml
Normal file
10
tests/samples/expectedMrssFormat/feed.empty.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
|
||||||
|
<channel>
|
||||||
|
<title>Sample feed with minimum data</title>
|
||||||
|
<link>https://github.com/RSS-Bridge/rss-bridge/</link>
|
||||||
|
<description>Sample feed with minimum data</description>
|
||||||
|
<atom:link href="https://github.com/RSS-Bridge/rss-bridge/" rel="alternate" type="text/html"/>
|
||||||
|
<atom:link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
19
tests/samples/expectedMrssFormat/feed.emptyItems.xml
Normal file
19
tests/samples/expectedMrssFormat/feed.emptyItems.xml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
|
||||||
|
<channel>
|
||||||
|
<title>Sample feed with minimum data</title>
|
||||||
|
<link>https://github.com/RSS-Bridge/rss-bridge/</link>
|
||||||
|
<description>Sample feed with minimum data</description>
|
||||||
|
<atom:link href="https://github.com/RSS-Bridge/rss-bridge/" rel="alternate" type="text/html"/>
|
||||||
|
<atom:link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>Sample Item #1</title>
|
||||||
|
<guid isPermaLink="false">29f59918d266c56a935da13e4122b524298e5a39</guid>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Sample Item #2</title>
|
||||||
|
<guid isPermaLink="false">edf358cad1a7ae255d6bc97640dd9d27738f1b7b</guid>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
26
tests/samples/expectedMrssFormat/feed.microblog.xml
Normal file
26
tests/samples/expectedMrssFormat/feed.microblog.xml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
|
||||||
|
<channel>
|
||||||
|
<title>Sample microblog feed</title>
|
||||||
|
<link>https://example.com/blog/</link>
|
||||||
|
<description>Sample microblog feed</description>
|
||||||
|
<image>
|
||||||
|
<url>https://example.com/logo.png</url>
|
||||||
|
<title>Sample microblog feed</title>
|
||||||
|
<link>https://example.com/blog/</link>
|
||||||
|
</image>
|
||||||
|
<atom:link href="https://example.com/blog/" rel="alternate" type="text/html"/>
|
||||||
|
<atom:link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">1918f084648b82057c1dd3faa3d091da82a6fac2</guid>
|
||||||
|
<pubDate>Sun, 07 Oct 2018 16:53:03 +0000</pubDate>
|
||||||
|
<description>Oh 😲 I found three monkeys 🙈🙉🙊</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">e62189168a06dfa74f61c621c79c33c4c8517e1f</guid>
|
||||||
|
<pubDate>Sun, 07 Oct 2018 16:38:17 +0000</pubDate>
|
||||||
|
<description>Something happened</description>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
|
@ -25,6 +25,7 @@
|
||||||
"content": "<p>We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers’ choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and it’s less prone to bugs.</p>\n\n<p>So we developed JSON Feed, a format similar to <a href=\"http://cyber.harvard.edu/rss/rss.html\">RSS</a> and <a href=\"https://tools.ietf.org/html/rfc4287\">Atom</a> but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.</p>\n\n<p><a href=\"https://jsonfeed.org/version/1\">See the spec</a>. It’s at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.</p>\n\n<h4>Notes</h4>\n\n<p>We have a <a href=\"https://github.com/manton/jsonfeed-wp\">WordPress plugin</a> and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, we’ll update the <a href=\"https://jsonfeed.org/code\">code</a> page.</p>\n\n<p>See <a href=\"https://jsonfeed.org/mappingrssandatom\">Mapping RSS and Atom to JSON Feed</a> for more on the similarities between the formats.</p>\n\n<p>This website — the Markdown files and supporting resources — <a href=\"https://github.com/brentsimmons/JSONFeed\">is up on GitHub</a>, and you’re welcome to comment there.</p>\n\n<p>This website is also a blog, and you can subscribe to the <a href=\"https://jsonfeed.org/xml/rss.xml\">RSS feed</a> or the <a href=\"https://jsonfeed.org/feed.json\">JSON feed</a> (if your reader supports it).</p>\n\n<p>We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the <a href=\"https://jsonfeed.org/version/1\">spec</a>. But — most importantly — <a href=\"http://furbo.org/\">Craig Hockenberry</a> spent a little time making it look pretty. :)</p>"
|
"content": "<p>We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers’ choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and it’s less prone to bugs.</p>\n\n<p>So we developed JSON Feed, a format similar to <a href=\"http://cyber.harvard.edu/rss/rss.html\">RSS</a> and <a href=\"https://tools.ietf.org/html/rfc4287\">Atom</a> but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.</p>\n\n<p><a href=\"https://jsonfeed.org/version/1\">See the spec</a>. It’s at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.</p>\n\n<h4>Notes</h4>\n\n<p>We have a <a href=\"https://github.com/manton/jsonfeed-wp\">WordPress plugin</a> and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, we’ll update the <a href=\"https://jsonfeed.org/code\">code</a> page.</p>\n\n<p>See <a href=\"https://jsonfeed.org/mappingrssandatom\">Mapping RSS and Atom to JSON Feed</a> for more on the similarities between the formats.</p>\n\n<p>This website — the Markdown files and supporting resources — <a href=\"https://github.com/brentsimmons/JSONFeed\">is up on GitHub</a>, and you’re welcome to comment there.</p>\n\n<p>This website is also a blog, and you can subscribe to the <a href=\"https://jsonfeed.org/xml/rss.xml\">RSS feed</a> or the <a href=\"https://jsonfeed.org/feed.json\">JSON feed</a> (if your reader supports it).</p>\n\n<p>We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the <a href=\"https://jsonfeed.org/version/1\">spec</a>. But — most importantly — <a href=\"http://furbo.org/\">Craig Hockenberry</a> spent a little time making it look pretty. :)</p>"
|
||||||
},{
|
},{
|
||||||
"uri": "http://example.org/2005/04/02/atom",
|
"uri": "http://example.org/2005/04/02/atom",
|
||||||
|
"uid": "tag:example.org,2003:3.2397",
|
||||||
"title": "Atom draft-07 snapshot",
|
"title": "Atom draft-07 snapshot",
|
||||||
"timestamp": 1122812969,
|
"timestamp": 1122812969,
|
||||||
"author": "Mark Pilgrim",
|
"author": "Mark Pilgrim",
|
||||||
|
|
Loading…
Reference in a new issue