two factor authentication essental elements

This commit is contained in:
Andy Miller
2017-08-25 16:20:57 -06:00
parent c2f81fd26d
commit 8658466e8d
322 changed files with 5904 additions and 892 deletions

View File

@@ -5,6 +5,7 @@ theme: grav
logo_text: ''
body_classes: ''
content_padding: true
twofa_enabled: true
sidebar:
activate: tab
hover_delay: 100

View File

@@ -48,6 +48,18 @@ form:
validate:
type: bool
twofa_enabled:
type: toggle
label: PLUGIN_ADMIN.2FA_TITLE
help: PLUGIN_ADMIN.2FA_ENABLED_HELP
default: 0
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
route:
type: text
label: Administrator path

View File

@@ -27,6 +27,7 @@ use RocketTheme\Toolbox\Session\Session;
use Symfony\Component\Yaml\Yaml;
use Composer\Semver\Semver;
use PicoFeed\Reader\Reader;
use RobThree\Auth\TwoFactorAuth;
define('LOGIN_REDIRECT_COOKIE', 'grav-login-redirect');
@@ -377,6 +378,17 @@ class Admin
$action = [];
if ($user->authorize('admin.login')) {
$twofa_admin_enabled = $this->grav['config']->get('plugins.admin.twofa_enabled', false);
if ($twofa_admin_enabled && isset($user->twofa_enabled) && $user->twofa_enabled == true) {
$twofa = $this->get2FA();
$secret = isset($user->twofa_secret) ? $user->twofa_secret : null;
if (!(isset($data['2fa_code']) && $data['2fa_code'] == $twofa->getCode($secret))) {
return false;
}
}
$this->user = $this->session->user = $user;
/** @var Grav $grav */
@@ -1709,4 +1721,9 @@ class Admin
return $pagesWithFiles;
}
public function get2FA()
{
return new TwoFactorAuth($this->grav['config']->get('site.title'));
}
}

View File

@@ -1,7 +1,8 @@
{
"require": {
"composer/semver": "^1.4",
"fguillot/picofeed": "@stable"
"fguillot/picofeed": "@stable",
"robthree/twofactorauth": "^1.6"
},
"require-dev": {
"codeception/codeception": "^2.1",

933
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -682,3 +682,9 @@ PLUGIN_ADMIN:
CONTENT_PADDING_HELP: "Enable/Disable content padding around content area to provide more space"
ENABLE_AUTO_METADATA: "Auto metadata from Exif"
ENABLE_AUTO_METADATA_HELP: "Automatically generate metadata files for images with exif information"
2FA_TITLE: "2-Factor Authentication"
2FA_LABEL: "Admin Access"
2FA_ENABLED: "2FA Enabled"
2FA_CODE_INPUT: "2FA Code (if enabled)"
2FA_SECRET: "2FA Secret"
2FA_SECRET_HELP: "Scan this code into your Authenticator App or enter the code manually. Check the Grav docs for more information"

View File

@@ -7,16 +7,26 @@ form:
method: post
fields:
- name: username
username:
type: text
placeholder: PLUGIN_ADMIN.USERNAME_EMAIL
autofocus: true
validate:
required: true
- name: password
password:
type: password
placeholder: PLUGIN_ADMIN.PASSWORD
validate:
required: true
twofa_check:
type: conditional
condition: config.plugins.admin.twofa_enabled
fields:
2fa_code:
type: text
placeholder: PLUGIN_ADMIN.2FA_CODE_INPUT
---

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -967,8 +967,8 @@ form {
}
&.switch-toggle input:checked + label {
background: #777;
color: #fff;
color: $content-bg;
background: $content-text;
}
}

View File

@@ -635,8 +635,10 @@ textarea.frontmatter {
height: 39px;
}
.switch-toggle label {
white-space: nowrap;
.switch-toggle {
line-height: 37px;
margin: 0 5px 5px 0;
}
}

View File

@@ -131,3 +131,17 @@
padding: 1rem 3rem;
}
}
.twofa-secret {
position: absolute;
opacity: 0;
visibility: hidden;
transition: opacity 600ms, visibility 600ms;
&.show {
position: static;
visibility: visible;
opacity: 1;
}
}

View File

@@ -16,6 +16,7 @@
display: inline-block;
cursor: pointer;
padding: 0 15px;
white-space: nowrap;
}
}

View File

@@ -0,0 +1,32 @@
{% extends "forms/field.html.twig" %}
{% block input %}
<div class="form-input-wrapper 2fa-wrapper">
{% set two_fa = admin.get2FA() %}
{% set value = (value is null ? two_fa.createSecret(160) : value) %}
<img style="border: 1px solid #ddd" src="{{ two_fa.getQRCodeImageAsDataUri(grav.user.email, value) }}" />
<div class="2fa-secret">{{ value|chunkSplit(4, ' ') }}</div>
<input type="text" style="display:none;" name="{{ (scope ~ field.name)|fieldName }}" value="{{ value|join(', ') }}" />
</div>
<script>
function twofaVisiblity() {
console.log('clicked');
if ($('#toggle_twofa_enabled1').is(':checked')) {
console.log('checked');
$('.twofa-secret').addClass("show");
} else {
console.log('unchecked');
$('.twofa-secret').removeClass("show");
}
}
$( document ).ready(function() {
twofaVisiblity();
$('.twofa-toggle input').click(twofaVisiblity);
});
</script>
{% endblock %}

View File

@@ -22,7 +22,7 @@
{% block input %}
<div class="switch-toggle switch-grav {{ field.size }} switch-{{ field.options|length }}">
<div class="switch-toggle switch-grav {{ field.size }} switch-{{ field.options|length }} {{ field.classes }}">
{% set maxLen = 0 %}
{% for text in field.options %}
{% set translation = grav.twig.twig.filters['tu'] is defined ? text|tu : text|t %}

View File

@@ -41,7 +41,7 @@ class AdminTwigExtension extends \Twig_Extension
new \Twig_SimpleFilter('toYaml', [$this, 'toYamlFilter']),
new \Twig_SimpleFilter('fromYaml', [$this, 'fromYamlFilter']),
new \Twig_SimpleFilter('adminNicetime', [$this, 'adminNicetimeFilter']),
new \Twig_SimpleFilter('chunkSplit', [$this, 'chunkSplitFilter']),
];
}
@@ -181,4 +181,9 @@ class AdminTwigExtension extends \Twig_Extension
return "$difference $periods[$j] {$tense}";
}
public function chunkSplitFilter($value, $chars, $split = '-')
{
return chunk_split($value, $chars, $split);
}
}

4
vendor/autoload.php vendored
View File

@@ -2,6 +2,6 @@
// autoload.php @generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php';
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitef6e9937a63bd796f32542b02941ee0e::getLoader();
return ComposerAutoloaderInitf3438a4bfc092aad40a104edf0a3eb02::getLoader();

View File

@@ -53,8 +53,9 @@ class ClassLoader
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
public function getPrefixes()
{
@@ -271,6 +272,26 @@ class ClassLoader
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
@@ -313,29 +334,34 @@ class ClassLoader
*/
public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative) {
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if ($file === null && defined('HHVM_VERSION')) {
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if ($file === null) {
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
return $this->classMap[$class] = false;
$this->missingClasses[$class] = true;
}
return $file;
@@ -348,9 +374,13 @@ class ClassLoader
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
if (0 === strpos($class, $prefix)) {
foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath.'\\';
if (isset($this->prefixDirsPsr4[$search])) {
foreach ($this->prefixDirsPsr4[$search] as $dir) {
$length = $this->prefixLengthsPsr4[$first][$search];
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
return $file;
}
@@ -399,6 +429,8 @@ class ClassLoader
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}

View File

@@ -1,5 +1,5 @@
Copyright (c) 2016 Nils Adermann, Jordi Boggiano
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -6,5 +6,6 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'RobThree\\Auth\\' => array($vendorDir . '/robthree/twofactorauth/lib'),
'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
);

View File

@@ -2,7 +2,7 @@
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitef6e9937a63bd796f32542b02941ee0e
class ComposerAutoloaderInitf3438a4bfc092aad40a104edf0a3eb02
{
private static $loader;
@@ -19,15 +19,15 @@ class ComposerAutoloaderInitef6e9937a63bd796f32542b02941ee0e
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInitef6e9937a63bd796f32542b02941ee0e', 'loadClassLoader'), true, true);
spl_autoload_register(array('ComposerAutoloaderInitf3438a4bfc092aad40a104edf0a3eb02', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInitef6e9937a63bd796f32542b02941ee0e', 'loadClassLoader'));
spl_autoload_unregister(array('ComposerAutoloaderInitf3438a4bfc092aad40a104edf0a3eb02', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitef6e9937a63bd796f32542b02941ee0e::getInitializer($loader));
call_user_func(\Composer\Autoload\ComposerStaticInitf3438a4bfc092aad40a104edf0a3eb02::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {

View File

@@ -4,9 +4,13 @@
namespace Composer\Autoload;
class ComposerStaticInitef6e9937a63bd796f32542b02941ee0e
class ComposerStaticInitf3438a4bfc092aad40a104edf0a3eb02
{
public static $prefixLengthsPsr4 = array (
'R' =>
array (
'RobThree\\Auth\\' => 14,
),
'C' =>
array (
'Composer\\Semver\\' => 16,
@@ -14,6 +18,10 @@ class ComposerStaticInitef6e9937a63bd796f32542b02941ee0e
);
public static $prefixDirsPsr4 = array (
'RobThree\\Auth\\' =>
array (
0 => __DIR__ . '/..' . '/robthree/twofactorauth/lib',
),
'Composer\\Semver\\' =>
array (
0 => __DIR__ . '/..' . '/composer/semver/src',
@@ -40,9 +48,9 @@ class ComposerStaticInitef6e9937a63bd796f32542b02941ee0e
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitef6e9937a63bd796f32542b02941ee0e::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitef6e9937a63bd796f32542b02941ee0e::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInitef6e9937a63bd796f32542b02941ee0e::$prefixesPsr0;
$loader->prefixLengthsPsr4 = ComposerStaticInitf3438a4bfc092aad40a104edf0a3eb02::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitf3438a4bfc092aad40a104edf0a3eb02::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInitf3438a4bfc092aad40a104edf0a3eb02::$prefixesPsr0;
}, null, ClassLoader::class);
}

View File

@@ -1,51 +1,4 @@
[
{
"name": "zendframework/zendxml",
"version": "1.0.2",
"version_normalized": "1.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/zendframework/ZendXml.git",
"reference": "7b64507bc35d841c9c5802d67f6f87ef8e1a58c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zendframework/ZendXml/zipball/7b64507bc35d841c9c5802d67f6f87ef8e1a58c9",
"reference": "7b64507bc35d841c9c5802d67f6f87ef8e1a58c9",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^3.7 || ^4.0",
"squizlabs/php_codesniffer": "^1.5"
},
"time": "2016-02-04 21:02:08",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-0": {
"ZendXml\\": "library/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Utility library for XML usage, best practices, and security in PHP",
"homepage": "http://packages.zendframework.com/",
"keywords": [
"security",
"xml",
"zf2"
]
},
{
"name": "composer/semver",
"version": "1.4.2",
@@ -68,7 +21,7 @@
"phpunit/phpunit": "^4.5 || ^5.0.5",
"phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
},
"time": "2016-08-30 16:08:34",
"time": "2016-08-30T16:08:34+00:00",
"type": "library",
"extra": {
"branch-alias": {
@@ -111,18 +64,65 @@
]
},
{
"name": "fguillot/picofeed",
"version": "v0.1.25",
"version_normalized": "0.1.25.0",
"name": "zendframework/zendxml",
"version": "1.0.2",
"version_normalized": "1.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/fguillot/picoFeed.git",
"reference": "2bf5bc40361e788eda6b1bd5d444630986721e69"
"url": "https://github.com/zendframework/ZendXml.git",
"reference": "7b64507bc35d841c9c5802d67f6f87ef8e1a58c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fguillot/picoFeed/zipball/2bf5bc40361e788eda6b1bd5d444630986721e69",
"reference": "2bf5bc40361e788eda6b1bd5d444630986721e69",
"url": "https://api.github.com/repos/zendframework/ZendXml/zipball/7b64507bc35d841c9c5802d67f6f87ef8e1a58c9",
"reference": "7b64507bc35d841c9c5802d67f6f87ef8e1a58c9",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^3.7 || ^4.0",
"squizlabs/php_codesniffer": "^1.5"
},
"time": "2016-02-04T21:02:08+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-0": {
"ZendXml\\": "library/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Utility library for XML usage, best practices, and security in PHP",
"homepage": "http://packages.zendframework.com/",
"keywords": [
"security",
"xml",
"zf2"
]
},
{
"name": "fguillot/picofeed",
"version": "v0.1.35",
"version_normalized": "0.1.35.0",
"source": {
"type": "git",
"url": "https://github.com/miniflux/picoFeed.git",
"reference": "3a27b47de31eedec075c719f961783c5db7a7b08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/miniflux/picoFeed/zipball/3a27b47de31eedec075c719f961783c5db7a7b08",
"reference": "3a27b47de31eedec075c719f961783c5db7a7b08",
"shasum": ""
},
"require": {
@@ -134,10 +134,15 @@
"php": ">=5.3.0",
"zendframework/zendxml": "^1.0"
},
"require-dev": {
"phpdocumentor/reflection-docblock": "2.0.4",
"phpunit/phpunit": "4.8.26",
"symfony/yaml": "2.8.7"
},
"suggest": {
"ext-curl": "PicoFeed will use cURL if present"
},
"time": "2016-08-30 01:33:18",
"time": "2017-06-20T22:54:47+00:00",
"bin": [
"picofeed"
],
@@ -158,6 +163,59 @@
}
],
"description": "Modern library to handle RSS/Atom feeds",
"homepage": "https://github.com/fguillot/picoFeed"
"homepage": "https://github.com/miniflux/picoFeed"
},
{
"name": "robthree/twofactorauth",
"version": "1.6",
"version_normalized": "1.6.0.0",
"source": {
"type": "git",
"url": "https://github.com/RobThree/TwoFactorAuth.git",
"reference": "5093ab230cd8f1296d792afb6a49545f37e7fd5a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/5093ab230cd8f1296d792afb6a49545f37e7fd5a",
"reference": "5093ab230cd8f1296d792afb6a49545f37e7fd5a",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "@stable"
},
"time": "2017-02-17T15:24:54+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"RobThree\\Auth\\": "lib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rob Janssen",
"homepage": "http://robiii.me",
"role": "Developer"
}
],
"description": "Two Factor Authentication",
"homepage": "https://github.com/RobThree/TwoFactorAuth",
"keywords": [
"Authentication",
"MFA",
"Multi Factor Authentication",
"Two Factor Authentication",
"authenticator",
"authy",
"php",
"tfa"
]
}
]

View File

@@ -31,4 +31,8 @@ abstract class Base
$this->config = $config ?: new Config();
Logger::setTimezone($this->config->getTimezone());
}
public function setConfig(Config $config) {
$this->config = $config;
}
}

View File

@@ -2,6 +2,8 @@
namespace PicoFeed\Client;
use DateTime;
use Exception;
use LogicException;
use PicoFeed\Logging\Logger;
use PicoFeed\Config\Config;
@@ -55,6 +57,13 @@ abstract class Client
*/
protected $last_modified = '';
/**
* Expiration DateTime
*
* @var DateTime
*/
protected $expiration = null;
/**
* Proxy hostname.
*
@@ -97,6 +106,13 @@ abstract class Client
*/
protected $password = '';
/**
* CURL options.
*
* @var array
*/
protected $additional_curl_options = array();
/**
* Client connection timeout.
*
@@ -109,7 +125,7 @@ abstract class Client
*
* @var string
*/
protected $user_agent = 'PicoFeed (https://github.com/fguillot/picoFeed)';
protected $user_agent = 'PicoFeed (https://github.com/miniflux/picoFeed)';
/**
* Real URL used (can be changed after a HTTP redirect).
@@ -214,6 +230,9 @@ abstract class Client
$this->handleErrorResponse($response);
$this->handleNormalResponse($response);
$this->expiration = $this->parseExpiration($response['headers']);
Logger::setMessage(get_called_class().' Expiration: '.$this->expiration->format(DATE_ISO8601));
return $this;
}
@@ -241,6 +260,9 @@ abstract class Client
* Handle Http Error codes
*
* @param array $response Client response
* @throws ForbiddenException
* @throws InvalidUrlException
* @throws UnauthorizedException
*/
protected function handleErrorResponse(array $response)
{
@@ -308,7 +330,6 @@ abstract class Client
* Find content type from response headers.
*
* @param array $response Client response
*
* @return string
*/
public function findContentType(array $response)
@@ -324,7 +345,6 @@ abstract class Client
public function findCharset()
{
$result = explode('charset=', $this->content_type);
return isset($result[1]) ? $result[1] : '';
}
@@ -333,7 +353,6 @@ abstract class Client
*
* @param array $response Client response
* @param string $header Header name
*
* @return string
*/
public function getHeader(array $response, $header)
@@ -345,13 +364,11 @@ abstract class Client
* Set the Last-Modified HTTP header.
*
* @param string $last_modified Header value
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setLastModified($last_modified)
{
$this->last_modified = $last_modified;
return $this;
}
@@ -369,13 +386,11 @@ abstract class Client
* Set the value of the Etag HTTP header.
*
* @param string $etag Etag HTTP header value
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setEtag($etag)
{
$this->etag = $etag;
return $this;
}
@@ -402,13 +417,12 @@ abstract class Client
/**
* Set the url.
*
* @param $url
* @return string
* @return \PicoFeed\Client\Client
*/
public function setUrl($url)
{
$this->url = $url;
return $this;
}
@@ -476,13 +490,11 @@ abstract class Client
* Set connection timeout.
*
* @param int $timeout Connection timeout
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setTimeout($timeout)
{
$this->timeout = $timeout ?: $this->timeout;
return $this;
}
@@ -490,13 +502,11 @@ abstract class Client
* Set a custom user agent.
*
* @param string $user_agent User Agent
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setUserAgent($user_agent)
{
$this->user_agent = $user_agent ?: $this->user_agent;
return $this;
}
@@ -504,13 +514,11 @@ abstract class Client
* Set the maximum number of HTTP redirections.
*
* @param int $max Maximum
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setMaxRedirections($max)
{
$this->max_redirects = $max ?: $this->max_redirects;
return $this;
}
@@ -518,13 +526,11 @@ abstract class Client
* Set the maximum size of the HTTP body.
*
* @param int $max Maximum
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setMaxBodySize($max)
{
$this->max_body_size = $max ?: $this->max_body_size;
return $this;
}
@@ -532,13 +538,11 @@ abstract class Client
* Set the proxy hostname.
*
* @param string $hostname Proxy hostname
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setProxyHostname($hostname)
{
$this->proxy_hostname = $hostname ?: $this->proxy_hostname;
return $this;
}
@@ -546,13 +550,11 @@ abstract class Client
* Set the proxy port.
*
* @param int $port Proxy port
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setProxyPort($port)
{
$this->proxy_port = $port ?: $this->proxy_port;
return $this;
}
@@ -560,13 +562,11 @@ abstract class Client
* Set the proxy username.
*
* @param string $username Proxy username
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setProxyUsername($username)
{
$this->proxy_username = $username ?: $this->proxy_username;
return $this;
}
@@ -574,13 +574,11 @@ abstract class Client
* Set the proxy password.
*
* @param string $password Password
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setProxyPassword($password)
{
$this->proxy_password = $password ?: $this->proxy_password;
return $this;
}
@@ -589,12 +587,11 @@ abstract class Client
*
* @param string $username Basic Auth username
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setUsername($username)
{
$this->username = $username ?: $this->username;
return $this;
}
@@ -603,36 +600,46 @@ abstract class Client
*
* @param string $password Basic Auth Password
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setPassword($password)
{
$this->password = $password ?: $this->password;
return $this;
}
/**
* Set the CURL options.
*
* @param array $options
* @return $this
*/
public function setAdditionalCurlOptions(array $options)
{
$this->additional_curl_options = $options ?: $this->additional_curl_options;
return $this;
}
/**
* Enable the passthrough mode.
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function enablePassthroughMode()
{
$this->passthrough = true;
return $this;
}
/**
* Disable the passthrough mode.
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function disablePassthroughMode()
{
$this->passthrough = false;
return $this;
}
@@ -640,8 +647,7 @@ abstract class Client
* Set config object.
*
* @param \PicoFeed\Config\Config $config Config instance
*
* @return \PicoFeed\Client\Client
* @return $this
*/
public function setConfig(Config $config)
{
@@ -654,6 +660,7 @@ abstract class Client
$this->setProxyPort($config->getProxyPort());
$this->setProxyUsername($config->getProxyUsername());
$this->setProxyPassword($config->getProxyPassword());
$this->setAdditionalCurlOptions($config->getAdditionalCurlOptions() ?: array());
}
return $this;
@@ -670,4 +677,36 @@ abstract class Client
{
return $code == 301 || $code == 302 || $code == 303 || $code == 307;
}
public function parseExpiration(HttpHeaders $headers)
{
try {
if (isset($headers['Cache-Control'])) {
if (preg_match('/s-maxage=(\d+)/', $headers['Cache-Control'], $matches)) {
return new DateTime('+' . $matches[1] . ' seconds');
} else if (preg_match('/max-age=(\d+)/', $headers['Cache-Control'], $matches)) {
return new DateTime('+' . $matches[1] . ' seconds');
}
}
if (! empty($headers['Expires'])) {
return new DateTime($headers['Expires']);
}
} catch (Exception $e) {
Logger::setMessage('Unable to parse expiration date: '.$e->getMessage());
}
return new DateTime();
}
/**
* Get expiration date time from "Expires" or "Cache-Control" headers
*
* @return DateTime
*/
public function getExpiration()
{
return $this->expiration ?: new DateTime();
}
}

View File

@@ -11,6 +11,8 @@ use PicoFeed\Logging\Logger;
*/
class Curl extends Client
{
protected $nbRedirects = 0;
/**
* HTTP response body.
*
@@ -108,8 +110,6 @@ class Curl extends Client
return $this->handleRedirection($headers['Location']);
}
header(':', true, $status);
if (isset($headers['Content-Type'])) {
header('Content-Type:' .$headers['Content-Type']);
}
@@ -136,6 +136,7 @@ class Curl extends Client
if ($this->etag) {
$headers[] = 'If-None-Match: '.$this->etag;
$headers[] = 'A-IM: feed';
}
if ($this->last_modified) {
@@ -199,6 +200,9 @@ class Curl extends Client
*/
private function prepareDownloadMode($ch)
{
$this->body = '';
$this->response_headers = array();
$this->response_headers_count = 0;
$write_function = 'readBody';
$header_function = 'readHeaders';
@@ -212,6 +216,20 @@ class Curl extends Client
return $ch;
}
/**
* Set additional CURL options.
*
* @param resource $ch
*
* @return resource $ch
*/
private function prepareAdditionalCurlOptions($ch){
foreach( $this->additional_curl_options as $c_op => $c_val ){
curl_setopt($ch, $c_op, $c_val);
}
return $ch;
}
/**
* Prepare curl context.
*
@@ -245,6 +263,7 @@ class Curl extends Client
$ch = $this->prepareDownloadMode($ch);
$ch = $this->prepareProxyContext($ch);
$ch = $this->prepareAuthContext($ch);
$ch = $this->prepareAdditionalCurlOptions($ch);
return $ch;
}
@@ -290,8 +309,12 @@ class Curl extends Client
list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1]));
if ($this->isRedirection($status)) {
if (empty($headers['Location'])) {
$status = 200;
} else {
return $this->handleRedirection($headers['Location']);
}
}
return array(
'status' => $status,
@@ -304,12 +327,11 @@ class Curl extends Client
* Handle HTTP redirects
*
* @param string $location Redirected URL
*
* @return array
* @throws MaxRedirectException
*/
private function handleRedirection($location)
{
$nb_redirects = 0;
$result = array();
$this->url = Url::resolve($location, $this->url);
$this->body = '';
@@ -318,9 +340,9 @@ class Curl extends Client
$this->response_headers_count = 0;
while (true) {
++$nb_redirects;
$this->nbRedirects++;
if ($nb_redirects >= $this->max_redirects) {
if ($this->nbRedirects >= $this->max_redirects) {
throw new MaxRedirectException('Maximum number of redirections reached');
}
@@ -349,6 +371,11 @@ class Curl extends Client
* @see http://curl.haxx.se/libcurl/c/libcurl-errors.html
*
* @param int $errno cURL error code
* @throws InvalidCertificateException
* @throws InvalidUrlException
* @throws MaxRedirectException
* @throws MaxSizeException
* @throws TimeoutException
*/
private function handleError($errno)
{
@@ -372,8 +399,7 @@ class Curl extends Client
case 66: // CURLE_SSL_ENGINE_INITFAILED
case 77: // CURLE_SSL_CACERT_BADFILE
case 83: // CURLE_SSL_ISSUER_ERROR
$msg = 'Invalid SSL certificate caused by CURL error number ' .
$errno;
$msg = 'Invalid SSL certificate caused by CURL error number ' . $errno;
throw new InvalidCertificateException($msg, $errno);
case 47: // CURLE_TOO_MANY_REDIRECTS
throw new MaxRedirectException('Maximum number of redirections reached', $errno);

View File

@@ -24,7 +24,7 @@ class HttpHeaders implements ArrayAccess
public function offsetGet($offset)
{
return $this->headers[strtolower($offset)];
return $this->offsetExists($offset) ? $this->headers[strtolower($offset)] : '';
}
public function offsetSet($offset, $value)

View File

@@ -31,6 +31,7 @@ class Stream extends Client
if ($this->etag) {
$headers[] = 'If-None-Match: '.$this->etag;
$headers[] = 'A-IM: feed';
}
if ($this->last_modified) {
@@ -104,6 +105,9 @@ class Stream extends Client
* Do the HTTP request.
*
* @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...]
* @throws InvalidUrlException
* @throws MaxSizeException
* @throws TimeoutException
*/
public function doRequest()
{

View File

@@ -50,10 +50,8 @@ class Url
* Shortcut method to get an absolute url from relative url.
*
* @static
*
* @param mixed $item_url Unknown url (can be relative or not)
* @param mixed $website_url Website url
*
* @return string
*/
public static function resolve($item_url, $website_url)
@@ -78,9 +76,7 @@ class Url
* Shortcut method to get a base url.
*
* @static
*
* @param string $url
*
* @return string
*/
public static function base($url)
@@ -94,7 +90,6 @@ class Url
* Get the base URL.
*
* @param string $suffix Add a suffix to the url
*
* @return string
*/
public function getBaseUrl($suffix = '')
@@ -106,7 +101,6 @@ class Url
* Get the absolute URL.
*
* @param string $base_url Use this url as base url
*
* @return string
*/
public function getAbsoluteUrl($base_url = '')
@@ -150,7 +144,8 @@ class Url
* Imported from Guzzle library: https://github.com/guzzle/psr7/blob/master/src/Uri.php#L568-L582
*
* @param $path
*
* @param string $charUnreserved
* @param string $charSubDelims
* @return string
*/
public function filterPath($path, $charUnreserved = 'a-zA-Z0-9_\-\.~', $charSubDelims = '!\$&\'\(\)\*\+,;=')
@@ -226,7 +221,6 @@ class Url
* Get the scheme.
*
* @param string $suffix Suffix to add when there is a scheme
*
* @return string
*/
public function getScheme($suffix = '')
@@ -238,12 +232,12 @@ class Url
* Set the scheme.
*
* @param string $scheme Set a scheme
*
* @return string
* @return $this
*/
public function setScheme($scheme)
{
$this->components['scheme'] = $scheme;
return $this;
}
/**
@@ -260,7 +254,6 @@ class Url
* Get the port.
*
* @param string $prefix Prefix to add when there is a port
*
* @return string
*/
public function getPort($prefix = '')

View File

@@ -7,9 +7,11 @@ namespace PicoFeed\Config;
*
* @author Frederic Guillot
*
* @method \PicoFeed\Config\Config setAdditionalCurlOptions(array $options)
* @method \PicoFeed\Config\Config setClientTimeout(integer $value)
* @method \PicoFeed\Config\Config setClientUserAgent(string $value)
* @method \PicoFeed\Config\Config setMaxRedirections(integer $value)
* @method \PicoFeed\Config\Config setMaxRecursions(integer $value)
* @method \PicoFeed\Config\Config setMaxBodySize(integer $value)
* @method \PicoFeed\Config\Config setProxyHostname(string $value)
* @method \PicoFeed\Config\Config setProxyPort(integer $value)
@@ -36,6 +38,7 @@ namespace PicoFeed\Config;
* @method integer getClientTimeout()
* @method string getClientUserAgent()
* @method integer getMaxRedirections()
* @method integer getMaxRecursions()
* @method integer getMaxBodySize()
* @method string getProxyHostname()
* @method integer getProxyPort()
@@ -59,6 +62,7 @@ namespace PicoFeed\Config;
* @method string getFilterImageProxyUrl()
* @method \Closure getFilterImageProxyCallback()
* @method string getFilterImageProxyProtocol()
* @method array getAdditionalCurlOptions()
*/
class Config
{
@@ -92,5 +96,7 @@ class Config
return isset($this->container[$parameter]) ? $this->container[$parameter] : $default_value;
}
return null;
}
}

View File

@@ -51,6 +51,7 @@ class Attribute
'td' => array(),
'tbody' => array(),
'thead' => array(),
'h1' => array(),
'h2' => array(),
'h3' => array(),
'h4' => array(),

View File

@@ -13,10 +13,8 @@ class Filter
* Get the Html filter instance.
*
* @static
*
* @param string $html HTML content
* @param string $website Site URL (used to build absolute URL)
*
* @return Html
*/
public static function html($html, $website)
@@ -30,7 +28,7 @@ class Filter
* Escape HTML content.
*
* @static
*
* @param string $content
* @return string
*/
public static function escape($content)
@@ -42,7 +40,6 @@ class Filter
* Remove HTML tags.
*
* @param string $data Input data
*
* @return string
*/
public function removeHTMLTags($data)
@@ -54,9 +51,7 @@ class Filter
* Remove the XML tag from a document.
*
* @static
*
* @param string $data Input data
*
* @return string
*/
public static function stripXmlTag($data)
@@ -80,9 +75,7 @@ class Filter
* Strip head tag from the HTML content.
*
* @static
*
* @param string $data Input data
*
* @return string
*/
public static function stripHeadTags($data)
@@ -94,9 +87,7 @@ class Filter
* Trim whitespace from the begining, the end and inside a string and don't break utf-8 string.
*
* @static
*
* @param string $value Raw data
*
* @return string Normalized data
*/
public static function stripWhiteSpace($value)
@@ -112,9 +103,7 @@ class Filter
* Fixes before XML parsing.
*
* @static
*
* @param string $data Raw data
*
* @return string Normalized data
*/
public static function normalizeData($data)

View File

@@ -90,7 +90,6 @@ class Html
* Set config object.
*
* @param \PicoFeed\Config\Config $config Config instance
*
* @return \PicoFeed\Filter\Html
*/
public function setConfig($config)
@@ -160,7 +159,8 @@ class Html
/**
* Called after XML parsing.
*
* @param string $content the content that should be filtered
* @param string $content
* @return string
*/
public function filterRules($content)
{

View File

@@ -42,6 +42,7 @@ class Tag extends Base
'td',
'tbody',
'thead',
'h1',
'h2',
'h3',
'h4',
@@ -67,6 +68,8 @@ class Tag extends Base
'abbr',
'iframe',
'q',
'sup',
'sub',
);
/**
@@ -74,7 +77,6 @@ class Tag extends Base
*
* @param string $tag Tag name
* @param array $attributes Attributes dictionary
*
* @return bool
*/
public function isAllowed($tag, array $attributes)
@@ -87,7 +89,6 @@ class Tag extends Base
*
* @param string $tag Tag name
* @param string $attributes Attributes converted in html
*
* @return string
*/
public function openHtmlTag($tag, $attributes = '')
@@ -99,7 +100,6 @@ class Tag extends Base
* Return the HTML closing tag.
*
* @param string $tag Tag name
*
* @return string
*/
public function closeHtmlTag($tag)
@@ -111,7 +111,6 @@ class Tag extends Base
* Return true is the tag is self-closing.
*
* @param string $tag Tag name
*
* @return bool
*/
public function isSelfClosingTag($tag)
@@ -123,7 +122,6 @@ class Tag extends Base
* Check if a tag is on the whitelist.
*
* @param string $tag Tag name
*
* @return bool
*/
public function isAllowedTag($tag)
@@ -139,7 +137,6 @@ class Tag extends Base
*
* @param string $tag Tag name
* @param array $attributes Tag attributes
*
* @return bool
*/
public function isPixelTracker($tag, array $attributes)
@@ -153,7 +150,6 @@ class Tag extends Base
* Remove script tags.
*
* @param string $data Input data
*
* @return string
*/
public function removeBlacklistedTags($data)
@@ -179,7 +175,6 @@ class Tag extends Base
* Remove empty tags.
*
* @param string $data Input data
*
* @return string
*/
public function removeEmptyTags($data)
@@ -191,7 +186,6 @@ class Tag extends Base
* Replace <br/><br/> by only one.
*
* @param string $data Input data
*
* @return string
*/
public function removeMultipleBreakTags($data)
@@ -203,7 +197,6 @@ class Tag extends Base
* Set whitelisted tags adn attributes for each tag.
*
* @param array $values List of tags: ['video' => ['src', 'cover'], 'img' => ['src']]
*
* @return Tag
*/
public function setWhitelistedTags(array $values)

View File

@@ -25,8 +25,7 @@ class Atom extends Parser
* Get the path to the items XML tree.
*
* @param SimpleXMLElement $xml Feed xml
*
* @return SimpleXMLElement
* @return SimpleXMLElement[]
*/
public function getItemsTree(SimpleXMLElement $xml)
{
@@ -288,12 +287,25 @@ class Atom extends Parser
$item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage());
}
/**
* Find the item categories.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param Feed $feed Feed object
*/
public function findItemCategories(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$categories = XmlParser::getXPathResult($entry, 'atom:category/@term', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'category/@term');
$item->setCategoriesFromXml($categories);
}
/**
* Get the URL from a link tag.
*
* @param SimpleXMLElement $xml XML tag
* @param string $rel Link relationship: alternate, enclosure, related, self, via
*
* @return string
*/
private function getUrl(SimpleXMLElement $xml, $rel, $fallback = false)
@@ -317,7 +329,6 @@ class Atom extends Parser
*
* @param SimpleXMLElement $xml XML tag
* @param string $rel Link relationship: alternate, enclosure, related, self, via
*
* @return SimpleXMLElement|null
*/
private function findLink(SimpleXMLElement $xml, $rel)
@@ -338,7 +349,6 @@ class Atom extends Parser
* Get the entry content.
*
* @param SimpleXMLElement $entry XML Entry
*
* @return string
*/
private function getContent(SimpleXMLElement $entry)

View File

@@ -38,6 +38,7 @@ class DateParser extends Base
DATE_RFC1123 => null,
DATE_RFC2822 => null,
DATE_RFC3339 => null,
'l, d M Y H:i:s' => null,
'D, d M Y H:i:s' => 25,
'D, d M Y h:i:s' => 25,
'D M d Y H:i:s' => 24,

View File

@@ -13,7 +13,7 @@ class Feed
/**
* Feed items.
*
* @var array
* @var Item[]
*/
public $items = array();

View File

@@ -103,6 +103,13 @@ class Item
*/
public $language = '';
/**
* Item categories.
*
* @var array
*/
public $categories = array();
/**
* Raw XML.
*
@@ -169,10 +176,13 @@ class Item
$publishedDate = $this->publishedDate != null ? $this->publishedDate->format(DATE_RFC822) : null;
$updatedDate = $this->updatedDate != null ? $this->updatedDate->format(DATE_RFC822) : null;
$categoryString = $this->categories != null ? implode(',', $this->categories) : null;
$output .= 'Item::date = '.$this->date->format(DATE_RFC822).PHP_EOL;
$output .= 'Item::publishedDate = '.$publishedDate.PHP_EOL;
$output .= 'Item::updatedDate = '.$updatedDate.PHP_EOL;
$output .= 'Item::isRTL() = '.($this->isRTL() ? 'true' : 'false').PHP_EOL;
$output .= 'Item::categories = ['.$categoryString.']'.PHP_EOL;
$output .= 'Item::content = '.strlen($this->content).' bytes'.PHP_EOL;
return $output;
@@ -305,6 +315,16 @@ class Item
return $this->language;
}
/**
* Get categories.
*
* @return array
*/
public function getCategories()
{
return $this->categories;
}
/**
* Get author.
*
@@ -433,6 +453,40 @@ class Item
return $this;
}
/**
* Set item categories.
*
* @param array $categories
* @return Item
*/
public function setCategories($categories)
{
$this->categories = $categories;
return $this;
}
/**
* Set item categories from xml.
*
* @param |SimpleXMLElement[] $categories
* @return Item
*/
public function setCategoriesFromXml($categories)
{
if ($categories !== false) {
$this->setCategories(
array_map(
function ($element) {
return trim((string) $element);
},
$categories
)
);
}
return $this;
}
/**
* Set raw XML.
*

View File

@@ -103,8 +103,8 @@ abstract class Parser implements ParserInterface
/**
* Parse the document.
*
* @return \PicoFeed\Parser\Feed
* @return Feed
* @throws MalformedXmlException
*/
public function execute()
{
@@ -163,6 +163,7 @@ abstract class Parser implements ParserInterface
$this->findItemDate($entry, $item, $feed);
$this->findItemEnclosure($entry, $item, $feed);
$this->findItemLanguage($entry, $item, $feed);
$this->findItemCategories($entry, $item, $feed);
$this->itemPostProcessor->execute($feed, $item);
$feed->items[] = $item;
@@ -222,18 +223,20 @@ abstract class Parser implements ParserInterface
public function findItemDate(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$this->findItemPublishedDate($entry, $item, $feed);
$published = $item->getPublishedDate();
$this->findItemUpdatedDate($entry, $item, $feed);
$updated = $item->getUpdatedDate();
if ($published === null && $updated === null) {
$item->setDate($feed->getDate()); // We use the feed date if there is no date for the item
} elseif ($published !== null && $updated !== null) {
$item->setDate(max($published, $updated)); // We use the most recent date between published and updated
} else {
$item->setDate($updated ?: $published);
if ($item->getPublishedDate() === null) {
// Use the updated date if available, otherwise use the feed date
$item->setPublishedDate($item->getUpdatedDate() ?: $feed->getDate());
}
if ($item->getUpdatedDate() === null) {
// Use the published date as fallback
$item->setUpdatedDate($item->getPublishedDate());
}
// Use the most recent of published and updated dates
$item->setDate(max($item->getPublishedDate(), $item->getUpdatedDate()));
}
/**
@@ -256,7 +259,7 @@ abstract class Parser implements ParserInterface
public function getDateParser()
{
if ($this->dateParser === null) {
return new DateParser($this->config);
$this->dateParser = new DateParser($this->config);
}
return $this->dateParser;
@@ -276,9 +279,7 @@ abstract class Parser implements ParserInterface
* Return true if the given language is "Right to Left".
*
* @static
*
* @param string $language Language: fr-FR, en-US
*
* @return bool
*/
public static function isLanguageRTL($language)
@@ -321,12 +322,12 @@ abstract class Parser implements ParserInterface
* Set config object.
*
* @param \PicoFeed\Config\Config $config Config instance
*
* @return \PicoFeed\Parser\Parser
*/
public function setConfig($config)
{
$this->config = $config;
$this->itemPostProcessor->setConfig($config);
return $this;
}
@@ -348,7 +349,6 @@ abstract class Parser implements ParserInterface
* scraped
* @param null|\Closure $scraperCallback Callback function that gets called for each
* scraper execution
*
* @return \PicoFeed\Parser\Parser
*/
public function enableContentGrabber($needsRuleFile = false, $scraperCallback = null)
@@ -371,7 +371,6 @@ abstract class Parser implements ParserInterface
* Set ignored URLs for the content grabber.
*
* @param array $urls URLs
*
* @return \PicoFeed\Parser\Parser
*/
public function setGrabberIgnoreUrls(array $urls)
@@ -384,7 +383,6 @@ abstract class Parser implements ParserInterface
* Register all supported namespaces to be used within an xpath query.
*
* @param SimpleXMLElement $xml Feed xml
*
* @return SimpleXMLElement
*/
public function registerSupportedNamespaces(SimpleXMLElement $xml)
@@ -395,6 +393,4 @@ abstract class Parser implements ParserInterface
return $xml;
}
}

View File

@@ -170,4 +170,13 @@ interface ParserInterface
* @param Feed $feed Feed object
*/
public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed);
/**
* Find the item categories.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param Feed $feed Feed object
*/
public function findItemCategories(SimpleXMLElement $entry, Item $item, Feed $feed);
}

View File

@@ -27,8 +27,7 @@ class Rss10 extends Parser
* Get the path to the items XML tree.
*
* @param SimpleXMLElement $xml Feed xml
*
* @return SimpleXMLElement
* @return SimpleXMLElement[]
*/
public function getItemsTree(SimpleXMLElement $xml)
{
@@ -290,4 +289,17 @@ class Rss10 extends Parser
$item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage());
}
/**
* Find the item categories.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param Feed $feed Feed object
*/
public function findItemCategories(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$categories = XmlParser::getXPathResult($entry, 'dc:subject', $this->namespaces);
$item->setCategoriesFromXml($categories);
}
}

View File

@@ -28,8 +28,7 @@ class Rss20 extends Parser
* Get the path to the items XML tree.
*
* @param SimpleXMLElement $xml Feed xml
*
* @return SimpleXMLElement
* @return SimpleXMLElement[]
*/
public function getItemsTree(SimpleXMLElement $xml)
{
@@ -302,4 +301,17 @@ class Rss20 extends Parser
$language = XmlParser::getXPathResult($entry, 'dc:language', $this->namespaces);
$item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage());
}
/**
* Find the item categories.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param Feed $feed Feed object
*/
public function findItemCategories(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$categories = XmlParser::getXPathResult($entry, 'category');
$item->setCategoriesFromXml($categories);
}
}

View File

@@ -34,7 +34,7 @@ class XmlParser
*
* @static
* @param string $input XML content
* @return DOMDocument
* @return DOMDocument|bool
*/
public static function getDomDocument($input)
{

View File

@@ -33,5 +33,7 @@ class ContentFilterProcessor extends Base implements ItemProcessorInterface
} else {
Logger::setMessage(get_called_class().': Content filtering disabled');
}
return false;
}
}

View File

@@ -5,6 +5,7 @@ namespace PicoFeed\Processor;
use PicoFeed\Base;
use PicoFeed\Parser\Feed;
use PicoFeed\Parser\Item;
use PicoFeed\Config\Config;
/**
* Item Post Processor
@@ -71,7 +72,7 @@ class ItemPostProcessor extends Base
}
/**
* Checks wheather a specific processor is registered or not
* Checks whether a specific processor is registered or not
*
* @access public
* @param string $class
@@ -93,4 +94,13 @@ class ItemPostProcessor extends Base
{
return isset($this->processors[$class]) ? $this->processors[$class] : null;
}
public function setConfig(Config $config)
{
foreach ($this->processors as $processor) {
$processor->setConfig($config);
}
return false;
}
}

View File

@@ -30,7 +30,7 @@ class Favicon extends Base
'image/x-icon',
'image/jpeg',
'image/jpg',
'image/svg+xml'
'image/svg+xml',
);
/**
@@ -95,8 +95,7 @@ class Favicon extends Base
* Download and check if a resource exists.
*
* @param string $url URL
*
* @return \PicoFeed\Client Client instance
* @return \PicoFeed\Client\Client Client instance
*/
public function download($url)
{
@@ -118,7 +117,6 @@ class Favicon extends Base
* Check if a remote file exists.
*
* @param string $url URL
*
* @return bool
*/
public function exists($url)
@@ -131,7 +129,6 @@ class Favicon extends Base
*
* @param string $website_link URL
* @param string $favicon_link optional URL
*
* @return string
*/
public function find($website_link, $favicon_link = '')
@@ -165,7 +162,6 @@ class Favicon extends Base
* Extract the icon links from the HTML.
*
* @param string $html HTML
*
* @return array
*/
public function extract($html)
@@ -179,7 +175,7 @@ class Favicon extends Base
$dom = XmlParser::getHtmlDocument($html);
$xpath = new DOMXpath($dom);
$elements = $xpath->query('//link[@rel="icon" or @rel="shortcut icon" or @rel="icon shortcut"]');
$elements = $xpath->query('//link[@rel="icon" or @rel="shortcut icon" or @rel="Shortcut Icon" or @rel="icon shortcut"]');
for ($i = 0; $i < $elements->length; ++$i) {
$icons[] = $elements->item($i)->getAttribute('href');

View File

@@ -61,8 +61,8 @@ class Reader extends Base
* @param string $etag Etag HTTP header
* @param string $username HTTP basic auth username
* @param string $password HTTP basic auth password
*
* @return \PicoFeed\Client\Client
* @return Client
* @throws SubscriptionNotFoundException
*/
public function discover($url, $last_modified = '', $etag = '', $username = '', $password = '')
{
@@ -130,8 +130,8 @@ class Reader extends Base
* @param string $url Site url
* @param string $content Feed content
* @param string $encoding HTTP encoding
*
* @return \PicoFeed\Parser\Parser
* @throws UnsupportedFeedFormatException
*/
public function getParser($url, $content, $encoding)
{
@@ -154,7 +154,6 @@ class Reader extends Base
* Detect the feed format.
*
* @param string $content Feed content
*
* @return string
*/
public function detectFormat($content)
@@ -177,7 +176,7 @@ class Reader extends Base
* Add the prefix "http://" if the end-user just enter a domain name.
*
* @param string $url Url
* @retunr string
* @return string
*/
public function prependScheme($url)
{

View File

@@ -6,7 +6,7 @@ return array(
'body' => array(
'//div[@class="content"]',
),
'strip' => array()
)
)
'strip' => array(),
),
),
);

View File

@@ -5,27 +5,40 @@ return array(
'test_url' => 'http://www.wired.com/gamelife/2013/09/ouya-free-the-games/',
'body' => array(
'//div[@data-js="gallerySlides"]',
'//article',
'//div[starts-with(@class,"post")]',
),
'strip' => array(
'//*[@id="linker_widget"]',
'//*[@class="credit"]',
'//div[@data-js="slideCount"]',
'//*[contains(@class="visually-hidden")]',
'//*[@data-slide-number="_endslate"]',
'//*[@id="related"]',
'//*[contains(@class, "bio")]',
'//*[contains(@class, "entry-footer")]',
'//*[contains(@class, "mobify_backtotop_link")]',
'//*[contains(@class, "gallery-navigation")]',
'//*[contains(@class, "gallery-thumbnail")]',
'//img[contains(@src, "1x1")]',
'//a[contains(@href, "creativecommons")]',
'//a[@href="#start-of-content"]',
'//h1',
'//nav',
'//button',
'//figure[starts-with(@class,"rad-slide")]',
'//figure[starts-with(@class,"end-slate")]',
'//div[contains(@class,"mobile-")]',
'//div[starts-with(@class,"mob-gallery-launcher")]',
'//div[contains(@id,"mobile-")]',
'//span[contains(@class,"slide-count")]',
'//div[contains(@class,"show-ipad")]',
'//img[contains(@id,"-hero-bg")]',
'//div[@data-js="overlayWrap"]',
'//ul[contains(@class,"metadata")]',
'//div[@class="opening center"]',
'//p[contains(@class="byline-mob"]',
'//div[@id="o-gallery"]',
'//div[starts-with(@class,"sm-col")]',
'//div[contains(@class,"pad-b-huge")]',
'//a[contains(@class,"visually-hidden")]',
'//*[@class="social"]',
'//i',
'//div[@data-js="mobGalleryAd"]',
'//div[contains(@class,"footer")]',
'//div[contains(@data-js,"fader")]',
'//div[@id="sharing"]',
'//div[contains(@id,"related")]',
'//div[@id="most-pop"]',
'//ul[@id="article-tags"]',
'//style',
'//section[contains(@class,"footer")]'
),
)
)
);

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'filter' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%^/news.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,21 +1,24 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://www.aljazeera.com/news/2015/09/xi-jinping-seattle-china-150922230118373.html',
'body' => array(
'//figure[@class="article-content"]',
'//div[@class="article-body"]',
'//article[@id="main-story"]',
),
'strip' => array(
'//h1',
'//h3',
'//script',
'//header',
'//ul',
'//table[contains(@class, "in-article-item")]',
'//section[contains(@class,"profile")]',
'//a[@target="_self"]',
'//div[contains(@id,"_2")]',
'//div[contains(@id,"_3")]',
'//img[@class="viewMode"]',
'//table[contains(@class,"in-article-item")]',
'//div[@data-embed-type="Brightcove"]',
'//div[@class="QuoteContainer"]',
'//div[@class="BottomByLine"]',
),
),
),

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
@@ -10,6 +9,7 @@ return array(
'strip' => array(
'//p[@class="kindofstory"]',
'//cite[@class="byline"]',
'//div[@class="useful-top"]',
'//div[contains(@class,"related-topics")]',
'//links',
'//sharebar',

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'filter' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,23 +1,25 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://arstechnica.com/tech-policy/2015/09/judge-warners-2m-happy-birthday-copyright-is-bogus/',
'body' => array(
'//header/h2',
'//section[@id="article-guts"]',
'//div[@class="superscroll-content show"]',
'//div[@class="gallery"]',
'//article',
),
'next_page' => '//span[@class="numbers"]/a',
'strip' => array(
'//h4[@class="post-upperdek"]',
'//h1',
'//ul[@class="lSPager lSGallery"]',
'//div[@class="lSAction"]',
'//section[@class="post-meta"]',
'//figcaption',
'//div[@class="post-meta"]',
'//div[@class="gallery-image-credit"]',
'//aside',
'//div[@class="article-expander"]',
'//div[@class="gallery-image-credit"]',
'//section[@class="article-author"]',
'//*[contains(@id,"social-")]',
'//div[contains(@id,"footer")]',
),
),
),
);

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%/index.php.*comic=.*%' => array(

View File

@@ -0,0 +1,18 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'https://medium.com/lessons-learned/917b8b63ae3e',
'body' => array(
'//div[contains(@class,"section-inner")]',
),
'strip' => array(
'//div[contains(@class,"metabar")]',
'//img[contains(@class,"thumbnail")]',
'//h1',
'//blockquote',
'//p[contains(@class,"graf-after--h4")]'
),
),
),
);

View File

@@ -1,11 +1,10 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://www.bangkokpost.com/news/politics/704204/new-us-ambassador-arrives-in-bangkok',
'body' => array(
'//div[@class="articleContents"]',
'//article/div[@class="articleContents"]',
),
'strip' => array(
'//h2',
@@ -13,7 +12,6 @@ return array(
'//div[@class="text-size"]',
'//div[@class="relate-story"]',
'//div[@class="text-ads"]',
'//script',
'//ul',
),
),

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'filter' => array(
'%.*%' => array(

View File

@@ -0,0 +1,31 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://bigpicture.ru/?p=556658',
'body' => array(
'//div[@class="article container"]',
),
'strip' => array(
'//script',
'//form',
'//style',
'//h1',
'//*[@class="wp-smiley"]',
'//div[@class="ipmd"]',
'//div[@class="tags"]',
'//div[@class="social-button"]',
'//div[@class="bottom-share"]',
'//div[@class="raccoonbox"]',
'//div[@class="yndadvert"]',
'//div[@class="we-recommend"]',
'//div[@class="relap-bigpicture_ru-wrapper"]',
'//div[@id="mmail"]',
'//div[@id="mobile-ads-cut"]',
'//div[@id="liquidstorm-alt-html"]',
'//div[contains(@class, "post-tags")]',
'//*[contains(text(),"Смотрите также")]',
),
),
),
);

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -0,0 +1,22 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'https://www.biztimes.com/2017/02/10/settlement-would-revive-fowler-lake-condo-project-in-oconomowoc/',
'body' => array(
'//h2/span[@class="subhead"]',
'//div[contains(@class,"article-content")]',
),
'strip' => array(
'//script',
'//div[contains(@class,"mobile-article-content")]',
'//div[contains(@class,"sharedaddy")]',
'//div[contains(@class,"author-details")]',
'//div[@class="row ad"]',
'//div[contains(@class,"relatedposts")]',
'//div[@class="col-lg-12"]',
'//div[contains(@class,"widget")]',
),
),
),
);

View File

@@ -0,0 +1,15 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'https://www.bleepingcomputer.com/news/google/chromes-sandbox-feature-infringes-on-three-patents-so-google-must-now-pay-20m/',
'body' => array(
'//div[@class="article_section"]',
),
'strip' => array(
'//*[@itemprop="headline"]',
'//div[@class="cz-news-story-title-section"]'
),
),
),
);

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -0,0 +1,22 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://m.brewers.mlb.com/news/article/161364798',
'body' => array(
'//article[contains(@class,"article")]',
),
'strip' => array(
'//div[contains(@class,"ad-slot")]',
'//h1',
'//span[@class="timestamp"]',
'//div[contains(@class,"contributor-bottom")]',
'//div[contains(@class,"video")]',
'//ul[contains(@class,"social")]',
'//p[@class="tagline"]',
'//div[contains(@class,"social")]',
'//div[@class="button-wrap"]',
),
),
),
);

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%/cad/.+%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -0,0 +1,18 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://www.chinafile.com/books/shanghai-faithful?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+chinafile%2FAll+%28ChinaFile%29',
'body' => array(
'//div[contains(@class,"pane-featured-photo-panel-pane-1")]',
'//div[contains(@class,"video-above-fold")]',
'//div[@class="sc-media"]',
'//div[contains(@class,"field-name-body")]',
),
'strip' => array(
'//div[contains(@class,"cboxes")]',
'//div[contains(@class,"l-middle")]',
),
),
),
);

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%/comic.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%^/products.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'filter' => array(
'%.*%' => array(

View File

@@ -0,0 +1,28 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://www.crash.net/motogp/interview/247550/1/exclusive-andrea-dovizioso-interview.html',
'body' => array(
'//div[@id="content"]',
),
'strip' => array(
'//script',
'//style',
'//*[@title="Social Networking"]',
'//*[@class="crash-ad2"]',
'//*[@class="clearfix"]',
'//*[@class="crash-ad2"]',
'//*[contains(@id, "divCB"]',
'//*[@class="pnlComment"]',
'//*[@class="comments-tabs"]',
'//*[contains(@class, "ad-twocol"]',
'//*[@class="stories-list"]',
'//*[contains(@class, "btn")]',
'//*[@class="content"]',
'//h3',
),
),
),
);

View File

@@ -1,18 +1,18 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://www.csmonitor.com/USA/Politics/2015/0925/John-Boehner-steps-down-Self-sacrificing-but-will-it-lead-to-better-government',
'body' => array(
'//figure[@id="image-top-1"]',
'//div[@id="story-body"]',
'//h2[@id="summary"]',
'//div[@class="flex-video youtube"]',
'//div[contains(@class,"eza-body")]',
),
'strip' => array(
'//script',
'//img[@title="hide caption"]',
'//span[@id="breadcrumb"]',
'//div[@id="byline-wrapper"]',
'//div[@class="injection"]',
'//*[contains(@class,"promo_link")]',
'//div[@id="story-embed-column"]',
),
),
),

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,17 +1,25 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://blogs.discovermagazine.com/the-extremo-files/2015/09/11/have-scientists-found-the-worlds-deepest-fish/',
'test_url' => 'http://blogs.discovermagazine.com/neuroskeptic/2017/01/25/publishers-jeffrey-beall/',
'body' => array(
'//div[@class="entry"]',
'//div[@class="contentWell"]',
),
'strip' => array(
'//h1',
'//div[@class="breadcrumbs"]',
'//div[@class="mobile"]',
'//div[@class="fromIssue"]',
'//div[contains(@class,"belowDeck")]',
'//div[@class="meta"]',
'//div[@class="shareIcons"]',
'//div[@class="categories"]',
'//div[@class="navigation"]',
'//div[@class="heading"]',
'//div[contains(@id,"-ad")]',
'//div[@class="relatedArticles"]',
'//div[@id="disqus_thread"]'
),
),
),

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -1,5 +1,4 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(

View File

@@ -0,0 +1,22 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://e-w-e.ru/16-prekrasnyx-izobretenij-zhenshhin/',
'body' => array(
'//div[contains(@class, "post_text")]',
),
'strip' => array(
'//script',
'//form',
'//style',
'//*[@class="views_post"]',
'//*[@class="adman_mobile"]',
'//*[@class="adman_desctop"]',
'//*[contains(@rel, "nofollow")]',
'//*[contains(@class, "wp-smiley")]',
'//*[contains(text(),"Источник:")]',
),
),
),
);

View File

@@ -0,0 +1,25 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://www.economist.com/blogs/buttonwood/2017/02/mixed-signals?fsrc=rss',
'body' => array(
'//article',
),
'strip' => array(
'//span[@class="blog-post__siblings-list-header "]',
'//h1',
'//aside',
'//div[@class="blog-post__asideable-wrapper"]',
'//div[@class="share_inline_header"]',
'//div[@id="column-right"]',
'//div[contains(@class,"blog-post__siblings-list-aside")]',
'//div[@class="video-player__wrapper"]',
'//div[@class="blog-post__bottom-panel"]',
'//div[contains(@class,"latest-updates-panel__container")]',
'//div[contains(@class,"blog-post__asideable-content")]',
'//div[@aria-label="Advertisement"]'
),
),
),
);

View File

@@ -1,5 +1,4 @@
<?php
return array(
'filter' => array(
'%.*%' => array(

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