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: '' logo_text: ''
body_classes: '' body_classes: ''
content_padding: true content_padding: true
twofa_enabled: true
sidebar: sidebar:
activate: tab activate: tab
hover_delay: 100 hover_delay: 100

View File

@@ -48,6 +48,18 @@ form:
validate: validate:
type: bool 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: route:
type: text type: text
label: Administrator path label: Administrator path

View File

@@ -27,6 +27,7 @@ use RocketTheme\Toolbox\Session\Session;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Composer\Semver\Semver; use Composer\Semver\Semver;
use PicoFeed\Reader\Reader; use PicoFeed\Reader\Reader;
use RobThree\Auth\TwoFactorAuth;
define('LOGIN_REDIRECT_COOKIE', 'grav-login-redirect'); define('LOGIN_REDIRECT_COOKIE', 'grav-login-redirect');
@@ -377,6 +378,17 @@ class Admin
$action = []; $action = [];
if ($user->authorize('admin.login')) { 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; $this->user = $this->session->user = $user;
/** @var Grav $grav */ /** @var Grav $grav */
@@ -1709,4 +1721,9 @@ class Admin
return $pagesWithFiles; return $pagesWithFiles;
} }
public function get2FA()
{
return new TwoFactorAuth($this->grav['config']->get('site.title'));
}
} }

View File

@@ -1,7 +1,8 @@
{ {
"require": { "require": {
"composer/semver": "^1.4", "composer/semver": "^1.4",
"fguillot/picofeed": "@stable" "fguillot/picofeed": "@stable",
"robthree/twofactorauth": "^1.6"
}, },
"require-dev": { "require-dev": {
"codeception/codeception": "^2.1", "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" CONTENT_PADDING_HELP: "Enable/Disable content padding around content area to provide more space"
ENABLE_AUTO_METADATA: "Auto metadata from Exif" ENABLE_AUTO_METADATA: "Auto metadata from Exif"
ENABLE_AUTO_METADATA_HELP: "Automatically generate metadata files for images with exif information" 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 method: post
fields: fields:
- name: username username:
type: text type: text
placeholder: PLUGIN_ADMIN.USERNAME_EMAIL placeholder: PLUGIN_ADMIN.USERNAME_EMAIL
autofocus: true autofocus: true
validate: validate:
required: true required: true
- name: password password:
type: password type: password
placeholder: PLUGIN_ADMIN.PASSWORD placeholder: PLUGIN_ADMIN.PASSWORD
validate: validate:
required: true 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 { &.switch-toggle input:checked + label {
background: #777; color: $content-bg;
color: #fff; background: $content-text;
} }
} }

View File

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

View File

@@ -131,3 +131,17 @@
padding: 1rem 3rem; 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; display: inline-block;
cursor: pointer; cursor: pointer;
padding: 0 15px; 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 %} {% 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 %} {% set maxLen = 0 %}
{% for text in field.options %} {% for text in field.options %}
{% set translation = grav.twig.twig.filters['tu'] is defined ? text|tu : text|t %} {% 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('toYaml', [$this, 'toYamlFilter']),
new \Twig_SimpleFilter('fromYaml', [$this, 'fromYamlFilter']), new \Twig_SimpleFilter('fromYaml', [$this, 'fromYamlFilter']),
new \Twig_SimpleFilter('adminNicetime', [$this, 'adminNicetimeFilter']), 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}"; 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 // 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 $useIncludePath = false;
private $classMap = array(); private $classMap = array();
private $classMapAuthoritative = false; private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
public function getPrefixes() public function getPrefixes()
{ {
@@ -271,6 +272,26 @@ class ClassLoader
return $this->classMapAuthoritative; 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. * Registers this instance as an autoloader.
* *
@@ -313,29 +334,34 @@ class ClassLoader
*/ */
public function findFile($class) 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 // class map lookup
if (isset($this->classMap[$class])) { if (isset($this->classMap[$class])) {
return $this->classMap[$class]; return $this->classMap[$class];
} }
if ($this->classMapAuthoritative) { if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false; return false;
} }
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php'); $file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM // 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'); $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. // Remember that this class does not exist.
return $this->classMap[$class] = false; $this->missingClasses[$class] = true;
} }
return $file; return $file;
@@ -348,9 +374,13 @@ class ClassLoader
$first = $class[0]; $first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) { if (isset($this->prefixLengthsPsr4[$first])) {
foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { $subPath = $class;
if (0 === strpos($class, $prefix)) { while (false !== $lastPos = strrpos($subPath, '\\')) {
foreach ($this->prefixDirsPsr4[$prefix] as $dir) { $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))) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
return $file; return $file;
} }
@@ -399,6 +429,8 @@ class ClassLoader
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file; 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

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

View File

@@ -2,7 +2,7 @@
// autoload_real.php @generated by Composer // autoload_real.php @generated by Composer
class ComposerAutoloaderInitef6e9937a63bd796f32542b02941ee0e class ComposerAutoloaderInitf3438a4bfc092aad40a104edf0a3eb02
{ {
private static $loader; private static $loader;
@@ -19,15 +19,15 @@ class ComposerAutoloaderInitef6e9937a63bd796f32542b02941ee0e
return self::$loader; 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(); 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) { if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php'; require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitef6e9937a63bd796f32542b02941ee0e::getInitializer($loader)); call_user_func(\Composer\Autoload\ComposerStaticInitf3438a4bfc092aad40a104edf0a3eb02::getInitializer($loader));
} else { } else {
$map = require __DIR__ . '/autoload_namespaces.php'; $map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) { foreach ($map as $namespace => $path) {

View File

@@ -4,9 +4,13 @@
namespace Composer\Autoload; namespace Composer\Autoload;
class ComposerStaticInitef6e9937a63bd796f32542b02941ee0e class ComposerStaticInitf3438a4bfc092aad40a104edf0a3eb02
{ {
public static $prefixLengthsPsr4 = array ( public static $prefixLengthsPsr4 = array (
'R' =>
array (
'RobThree\\Auth\\' => 14,
),
'C' => 'C' =>
array ( array (
'Composer\\Semver\\' => 16, 'Composer\\Semver\\' => 16,
@@ -14,6 +18,10 @@ class ComposerStaticInitef6e9937a63bd796f32542b02941ee0e
); );
public static $prefixDirsPsr4 = array ( public static $prefixDirsPsr4 = array (
'RobThree\\Auth\\' =>
array (
0 => __DIR__ . '/..' . '/robthree/twofactorauth/lib',
),
'Composer\\Semver\\' => 'Composer\\Semver\\' =>
array ( array (
0 => __DIR__ . '/..' . '/composer/semver/src', 0 => __DIR__ . '/..' . '/composer/semver/src',
@@ -40,9 +48,9 @@ class ComposerStaticInitef6e9937a63bd796f32542b02941ee0e
public static function getInitializer(ClassLoader $loader) public static function getInitializer(ClassLoader $loader)
{ {
return \Closure::bind(function () use ($loader) { return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitef6e9937a63bd796f32542b02941ee0e::$prefixLengthsPsr4; $loader->prefixLengthsPsr4 = ComposerStaticInitf3438a4bfc092aad40a104edf0a3eb02::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitef6e9937a63bd796f32542b02941ee0e::$prefixDirsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInitf3438a4bfc092aad40a104edf0a3eb02::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInitef6e9937a63bd796f32542b02941ee0e::$prefixesPsr0; $loader->prefixesPsr0 = ComposerStaticInitf3438a4bfc092aad40a104edf0a3eb02::$prefixesPsr0;
}, null, ClassLoader::class); }, 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", "name": "composer/semver",
"version": "1.4.2", "version": "1.4.2",
@@ -68,7 +21,7 @@
"phpunit/phpunit": "^4.5 || ^5.0.5", "phpunit/phpunit": "^4.5 || ^5.0.5",
"phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" "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", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
@@ -111,18 +64,65 @@
] ]
}, },
{ {
"name": "fguillot/picofeed", "name": "zendframework/zendxml",
"version": "v0.1.25", "version": "1.0.2",
"version_normalized": "0.1.25.0", "version_normalized": "1.0.2.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/fguillot/picoFeed.git", "url": "https://github.com/zendframework/ZendXml.git",
"reference": "2bf5bc40361e788eda6b1bd5d444630986721e69" "reference": "7b64507bc35d841c9c5802d67f6f87ef8e1a58c9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/fguillot/picoFeed/zipball/2bf5bc40361e788eda6b1bd5d444630986721e69", "url": "https://api.github.com/repos/zendframework/ZendXml/zipball/7b64507bc35d841c9c5802d67f6f87ef8e1a58c9",
"reference": "2bf5bc40361e788eda6b1bd5d444630986721e69", "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": "" "shasum": ""
}, },
"require": { "require": {
@@ -134,10 +134,15 @@
"php": ">=5.3.0", "php": ">=5.3.0",
"zendframework/zendxml": "^1.0" "zendframework/zendxml": "^1.0"
}, },
"require-dev": {
"phpdocumentor/reflection-docblock": "2.0.4",
"phpunit/phpunit": "4.8.26",
"symfony/yaml": "2.8.7"
},
"suggest": { "suggest": {
"ext-curl": "PicoFeed will use cURL if present" "ext-curl": "PicoFeed will use cURL if present"
}, },
"time": "2016-08-30 01:33:18", "time": "2017-06-20T22:54:47+00:00",
"bin": [ "bin": [
"picofeed" "picofeed"
], ],
@@ -158,6 +163,59 @@
} }
], ],
"description": "Modern library to handle RSS/Atom feeds", "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(); $this->config = $config ?: new Config();
Logger::setTimezone($this->config->getTimezone()); Logger::setTimezone($this->config->getTimezone());
} }
public function setConfig(Config $config) {
$this->config = $config;
}
} }

View File

@@ -2,6 +2,8 @@
namespace PicoFeed\Client; namespace PicoFeed\Client;
use DateTime;
use Exception;
use LogicException; use LogicException;
use PicoFeed\Logging\Logger; use PicoFeed\Logging\Logger;
use PicoFeed\Config\Config; use PicoFeed\Config\Config;
@@ -55,6 +57,13 @@ abstract class Client
*/ */
protected $last_modified = ''; protected $last_modified = '';
/**
* Expiration DateTime
*
* @var DateTime
*/
protected $expiration = null;
/** /**
* Proxy hostname. * Proxy hostname.
* *
@@ -97,6 +106,13 @@ abstract class Client
*/ */
protected $password = ''; protected $password = '';
/**
* CURL options.
*
* @var array
*/
protected $additional_curl_options = array();
/** /**
* Client connection timeout. * Client connection timeout.
* *
@@ -109,7 +125,7 @@ abstract class Client
* *
* @var string * @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). * Real URL used (can be changed after a HTTP redirect).
@@ -214,6 +230,9 @@ abstract class Client
$this->handleErrorResponse($response); $this->handleErrorResponse($response);
$this->handleNormalResponse($response); $this->handleNormalResponse($response);
$this->expiration = $this->parseExpiration($response['headers']);
Logger::setMessage(get_called_class().' Expiration: '.$this->expiration->format(DATE_ISO8601));
return $this; return $this;
} }
@@ -241,6 +260,9 @@ abstract class Client
* Handle Http Error codes * Handle Http Error codes
* *
* @param array $response Client response * @param array $response Client response
* @throws ForbiddenException
* @throws InvalidUrlException
* @throws UnauthorizedException
*/ */
protected function handleErrorResponse(array $response) protected function handleErrorResponse(array $response)
{ {
@@ -308,7 +330,6 @@ abstract class Client
* Find content type from response headers. * Find content type from response headers.
* *
* @param array $response Client response * @param array $response Client response
*
* @return string * @return string
*/ */
public function findContentType(array $response) public function findContentType(array $response)
@@ -324,7 +345,6 @@ abstract class Client
public function findCharset() public function findCharset()
{ {
$result = explode('charset=', $this->content_type); $result = explode('charset=', $this->content_type);
return isset($result[1]) ? $result[1] : ''; return isset($result[1]) ? $result[1] : '';
} }
@@ -333,7 +353,6 @@ abstract class Client
* *
* @param array $response Client response * @param array $response Client response
* @param string $header Header name * @param string $header Header name
*
* @return string * @return string
*/ */
public function getHeader(array $response, $header) public function getHeader(array $response, $header)
@@ -345,13 +364,11 @@ abstract class Client
* Set the Last-Modified HTTP header. * Set the Last-Modified HTTP header.
* *
* @param string $last_modified Header value * @param string $last_modified Header value
* * @return $this
* @return \PicoFeed\Client\Client
*/ */
public function setLastModified($last_modified) public function setLastModified($last_modified)
{ {
$this->last_modified = $last_modified; $this->last_modified = $last_modified;
return $this; return $this;
} }
@@ -369,13 +386,11 @@ abstract class Client
* Set the value of the Etag HTTP header. * Set the value of the Etag HTTP header.
* *
* @param string $etag Etag HTTP header value * @param string $etag Etag HTTP header value
* * @return $this
* @return \PicoFeed\Client\Client
*/ */
public function setEtag($etag) public function setEtag($etag)
{ {
$this->etag = $etag; $this->etag = $etag;
return $this; return $this;
} }
@@ -402,13 +417,12 @@ abstract class Client
/** /**
* Set the url. * Set the url.
* *
* @param $url
* @return string * @return string
* @return \PicoFeed\Client\Client
*/ */
public function setUrl($url) public function setUrl($url)
{ {
$this->url = $url; $this->url = $url;
return $this; return $this;
} }
@@ -476,13 +490,11 @@ abstract class Client
* Set connection timeout. * Set connection timeout.
* *
* @param int $timeout Connection timeout * @param int $timeout Connection timeout
* * @return $this
* @return \PicoFeed\Client\Client
*/ */
public function setTimeout($timeout) public function setTimeout($timeout)
{ {
$this->timeout = $timeout ?: $this->timeout; $this->timeout = $timeout ?: $this->timeout;
return $this; return $this;
} }
@@ -490,13 +502,11 @@ abstract class Client
* Set a custom user agent. * Set a custom user agent.
* *
* @param string $user_agent User Agent * @param string $user_agent User Agent
* * @return $this
* @return \PicoFeed\Client\Client
*/ */
public function setUserAgent($user_agent) public function setUserAgent($user_agent)
{ {
$this->user_agent = $user_agent ?: $this->user_agent; $this->user_agent = $user_agent ?: $this->user_agent;
return $this; return $this;
} }
@@ -504,13 +514,11 @@ abstract class Client
* Set the maximum number of HTTP redirections. * Set the maximum number of HTTP redirections.
* *
* @param int $max Maximum * @param int $max Maximum
* * @return $this
* @return \PicoFeed\Client\Client
*/ */
public function setMaxRedirections($max) public function setMaxRedirections($max)
{ {
$this->max_redirects = $max ?: $this->max_redirects; $this->max_redirects = $max ?: $this->max_redirects;
return $this; return $this;
} }
@@ -518,13 +526,11 @@ abstract class Client
* Set the maximum size of the HTTP body. * Set the maximum size of the HTTP body.
* *
* @param int $max Maximum * @param int $max Maximum
* * @return $this
* @return \PicoFeed\Client\Client
*/ */
public function setMaxBodySize($max) public function setMaxBodySize($max)
{ {
$this->max_body_size = $max ?: $this->max_body_size; $this->max_body_size = $max ?: $this->max_body_size;
return $this; return $this;
} }
@@ -532,13 +538,11 @@ abstract class Client
* Set the proxy hostname. * Set the proxy hostname.
* *
* @param string $hostname Proxy hostname * @param string $hostname Proxy hostname
* * @return $this
* @return \PicoFeed\Client\Client
*/ */
public function setProxyHostname($hostname) public function setProxyHostname($hostname)
{ {
$this->proxy_hostname = $hostname ?: $this->proxy_hostname; $this->proxy_hostname = $hostname ?: $this->proxy_hostname;
return $this; return $this;
} }
@@ -546,13 +550,11 @@ abstract class Client
* Set the proxy port. * Set the proxy port.
* *
* @param int $port Proxy port * @param int $port Proxy port
* * @return $this
* @return \PicoFeed\Client\Client
*/ */
public function setProxyPort($port) public function setProxyPort($port)
{ {
$this->proxy_port = $port ?: $this->proxy_port; $this->proxy_port = $port ?: $this->proxy_port;
return $this; return $this;
} }
@@ -560,13 +562,11 @@ abstract class Client
* Set the proxy username. * Set the proxy username.
* *
* @param string $username Proxy username * @param string $username Proxy username
* * @return $this
* @return \PicoFeed\Client\Client
*/ */
public function setProxyUsername($username) public function setProxyUsername($username)
{ {
$this->proxy_username = $username ?: $this->proxy_username; $this->proxy_username = $username ?: $this->proxy_username;
return $this; return $this;
} }
@@ -574,13 +574,11 @@ abstract class Client
* Set the proxy password. * Set the proxy password.
* *
* @param string $password Password * @param string $password Password
* * @return $this
* @return \PicoFeed\Client\Client
*/ */
public function setProxyPassword($password) public function setProxyPassword($password)
{ {
$this->proxy_password = $password ?: $this->proxy_password; $this->proxy_password = $password ?: $this->proxy_password;
return $this; return $this;
} }
@@ -589,12 +587,11 @@ abstract class Client
* *
* @param string $username Basic Auth username * @param string $username Basic Auth username
* *
* @return \PicoFeed\Client\Client * @return $this
*/ */
public function setUsername($username) public function setUsername($username)
{ {
$this->username = $username ?: $this->username; $this->username = $username ?: $this->username;
return $this; return $this;
} }
@@ -603,36 +600,46 @@ abstract class Client
* *
* @param string $password Basic Auth Password * @param string $password Basic Auth Password
* *
* @return \PicoFeed\Client\Client * @return $this
*/ */
public function setPassword($password) public function setPassword($password)
{ {
$this->password = $password ?: $this->password; $this->password = $password ?: $this->password;
return $this; 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. * Enable the passthrough mode.
* *
* @return \PicoFeed\Client\Client * @return $this
*/ */
public function enablePassthroughMode() public function enablePassthroughMode()
{ {
$this->passthrough = true; $this->passthrough = true;
return $this; return $this;
} }
/** /**
* Disable the passthrough mode. * Disable the passthrough mode.
* *
* @return \PicoFeed\Client\Client * @return $this
*/ */
public function disablePassthroughMode() public function disablePassthroughMode()
{ {
$this->passthrough = false; $this->passthrough = false;
return $this; return $this;
} }
@@ -640,8 +647,7 @@ abstract class Client
* Set config object. * Set config object.
* *
* @param \PicoFeed\Config\Config $config Config instance * @param \PicoFeed\Config\Config $config Config instance
* * @return $this
* @return \PicoFeed\Client\Client
*/ */
public function setConfig(Config $config) public function setConfig(Config $config)
{ {
@@ -654,6 +660,7 @@ abstract class Client
$this->setProxyPort($config->getProxyPort()); $this->setProxyPort($config->getProxyPort());
$this->setProxyUsername($config->getProxyUsername()); $this->setProxyUsername($config->getProxyUsername());
$this->setProxyPassword($config->getProxyPassword()); $this->setProxyPassword($config->getProxyPassword());
$this->setAdditionalCurlOptions($config->getAdditionalCurlOptions() ?: array());
} }
return $this; return $this;
@@ -670,4 +677,36 @@ abstract class Client
{ {
return $code == 301 || $code == 302 || $code == 303 || $code == 307; 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 class Curl extends Client
{ {
protected $nbRedirects = 0;
/** /**
* HTTP response body. * HTTP response body.
* *
@@ -108,8 +110,6 @@ class Curl extends Client
return $this->handleRedirection($headers['Location']); return $this->handleRedirection($headers['Location']);
} }
header(':', true, $status);
if (isset($headers['Content-Type'])) { if (isset($headers['Content-Type'])) {
header('Content-Type:' .$headers['Content-Type']); header('Content-Type:' .$headers['Content-Type']);
} }
@@ -136,6 +136,7 @@ class Curl extends Client
if ($this->etag) { if ($this->etag) {
$headers[] = 'If-None-Match: '.$this->etag; $headers[] = 'If-None-Match: '.$this->etag;
$headers[] = 'A-IM: feed';
} }
if ($this->last_modified) { if ($this->last_modified) {
@@ -199,6 +200,9 @@ class Curl extends Client
*/ */
private function prepareDownloadMode($ch) private function prepareDownloadMode($ch)
{ {
$this->body = '';
$this->response_headers = array();
$this->response_headers_count = 0;
$write_function = 'readBody'; $write_function = 'readBody';
$header_function = 'readHeaders'; $header_function = 'readHeaders';
@@ -212,6 +216,20 @@ class Curl extends Client
return $ch; 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. * Prepare curl context.
* *
@@ -245,6 +263,7 @@ class Curl extends Client
$ch = $this->prepareDownloadMode($ch); $ch = $this->prepareDownloadMode($ch);
$ch = $this->prepareProxyContext($ch); $ch = $this->prepareProxyContext($ch);
$ch = $this->prepareAuthContext($ch); $ch = $this->prepareAuthContext($ch);
$ch = $this->prepareAdditionalCurlOptions($ch);
return $ch; return $ch;
} }
@@ -290,7 +309,11 @@ class Curl extends Client
list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1])); list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1]));
if ($this->isRedirection($status)) { if ($this->isRedirection($status)) {
return $this->handleRedirection($headers['Location']); if (empty($headers['Location'])) {
$status = 200;
} else {
return $this->handleRedirection($headers['Location']);
}
} }
return array( return array(
@@ -304,12 +327,11 @@ class Curl extends Client
* Handle HTTP redirects * Handle HTTP redirects
* *
* @param string $location Redirected URL * @param string $location Redirected URL
*
* @return array * @return array
* @throws MaxRedirectException
*/ */
private function handleRedirection($location) private function handleRedirection($location)
{ {
$nb_redirects = 0;
$result = array(); $result = array();
$this->url = Url::resolve($location, $this->url); $this->url = Url::resolve($location, $this->url);
$this->body = ''; $this->body = '';
@@ -318,9 +340,9 @@ class Curl extends Client
$this->response_headers_count = 0; $this->response_headers_count = 0;
while (true) { 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'); 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 * @see http://curl.haxx.se/libcurl/c/libcurl-errors.html
* *
* @param int $errno cURL error code * @param int $errno cURL error code
* @throws InvalidCertificateException
* @throws InvalidUrlException
* @throws MaxRedirectException
* @throws MaxSizeException
* @throws TimeoutException
*/ */
private function handleError($errno) private function handleError($errno)
{ {
@@ -372,8 +399,7 @@ class Curl extends Client
case 66: // CURLE_SSL_ENGINE_INITFAILED case 66: // CURLE_SSL_ENGINE_INITFAILED
case 77: // CURLE_SSL_CACERT_BADFILE case 77: // CURLE_SSL_CACERT_BADFILE
case 83: // CURLE_SSL_ISSUER_ERROR case 83: // CURLE_SSL_ISSUER_ERROR
$msg = 'Invalid SSL certificate caused by CURL error number ' . $msg = 'Invalid SSL certificate caused by CURL error number ' . $errno;
$errno;
throw new InvalidCertificateException($msg, $errno); throw new InvalidCertificateException($msg, $errno);
case 47: // CURLE_TOO_MANY_REDIRECTS case 47: // CURLE_TOO_MANY_REDIRECTS
throw new MaxRedirectException('Maximum number of redirections reached', $errno); throw new MaxRedirectException('Maximum number of redirections reached', $errno);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -103,6 +103,13 @@ class Item
*/ */
public $language = ''; public $language = '';
/**
* Item categories.
*
* @var array
*/
public $categories = array();
/** /**
* Raw XML. * Raw XML.
* *
@@ -169,10 +176,13 @@ class Item
$publishedDate = $this->publishedDate != null ? $this->publishedDate->format(DATE_RFC822) : null; $publishedDate = $this->publishedDate != null ? $this->publishedDate->format(DATE_RFC822) : null;
$updatedDate = $this->updatedDate != null ? $this->updatedDate->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::date = '.$this->date->format(DATE_RFC822).PHP_EOL;
$output .= 'Item::publishedDate = '.$publishedDate.PHP_EOL; $output .= 'Item::publishedDate = '.$publishedDate.PHP_EOL;
$output .= 'Item::updatedDate = '.$updatedDate.PHP_EOL; $output .= 'Item::updatedDate = '.$updatedDate.PHP_EOL;
$output .= 'Item::isRTL() = '.($this->isRTL() ? 'true' : 'false').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; $output .= 'Item::content = '.strlen($this->content).' bytes'.PHP_EOL;
return $output; return $output;
@@ -305,6 +315,16 @@ class Item
return $this->language; return $this->language;
} }
/**
* Get categories.
*
* @return array
*/
public function getCategories()
{
return $this->categories;
}
/** /**
* Get author. * Get author.
* *
@@ -433,6 +453,40 @@ class Item
return $this; 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. * Set raw XML.
* *

View File

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

View File

@@ -170,4 +170,13 @@ interface ParserInterface
* @param Feed $feed Feed object * @param Feed $feed Feed object
*/ */
public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed); 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. * Get the path to the items XML tree.
* *
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* * @return SimpleXMLElement[]
* @return SimpleXMLElement
*/ */
public function getItemsTree(SimpleXMLElement $xml) public function getItemsTree(SimpleXMLElement $xml)
{ {
@@ -290,4 +289,17 @@ class Rss10 extends Parser
$item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage()); $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. * Get the path to the items XML tree.
* *
* @param SimpleXMLElement $xml Feed xml * @param SimpleXMLElement $xml Feed xml
* * @return SimpleXMLElement[]
* @return SimpleXMLElement
*/ */
public function getItemsTree(SimpleXMLElement $xml) public function getItemsTree(SimpleXMLElement $xml)
{ {
@@ -302,4 +301,17 @@ class Rss20 extends Parser
$language = XmlParser::getXPathResult($entry, 'dc:language', $this->namespaces); $language = XmlParser::getXPathResult($entry, 'dc:language', $this->namespaces);
$item->setLanguage(XmlParser::getValue($language) ?: $feed->getLanguage()); $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 * @static
* @param string $input XML content * @param string $input XML content
* @return DOMDocument * @return DOMDocument|bool
*/ */
public static function getDomDocument($input) public static function getDomDocument($input)
{ {

View File

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

View File

@@ -5,6 +5,7 @@ namespace PicoFeed\Processor;
use PicoFeed\Base; use PicoFeed\Base;
use PicoFeed\Parser\Feed; use PicoFeed\Parser\Feed;
use PicoFeed\Parser\Item; use PicoFeed\Parser\Item;
use PicoFeed\Config\Config;
/** /**
* Item Post Processor * 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 * @access public
* @param string $class * @param string $class
@@ -93,4 +94,13 @@ class ItemPostProcessor extends Base
{ {
return isset($this->processors[$class]) ? $this->processors[$class] : null; 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/x-icon',
'image/jpeg', 'image/jpeg',
'image/jpg', 'image/jpg',
'image/svg+xml' 'image/svg+xml',
); );
/** /**
@@ -95,8 +95,7 @@ class Favicon extends Base
* Download and check if a resource exists. * Download and check if a resource exists.
* *
* @param string $url URL * @param string $url URL
* * @return \PicoFeed\Client\Client Client instance
* @return \PicoFeed\Client Client instance
*/ */
public function download($url) public function download($url)
{ {
@@ -118,7 +117,6 @@ class Favicon extends Base
* Check if a remote file exists. * Check if a remote file exists.
* *
* @param string $url URL * @param string $url URL
*
* @return bool * @return bool
*/ */
public function exists($url) public function exists($url)
@@ -131,7 +129,6 @@ class Favicon extends Base
* *
* @param string $website_link URL * @param string $website_link URL
* @param string $favicon_link optional URL * @param string $favicon_link optional URL
*
* @return string * @return string
*/ */
public function find($website_link, $favicon_link = '') public function find($website_link, $favicon_link = '')
@@ -165,7 +162,6 @@ class Favicon extends Base
* Extract the icon links from the HTML. * Extract the icon links from the HTML.
* *
* @param string $html HTML * @param string $html HTML
*
* @return array * @return array
*/ */
public function extract($html) public function extract($html)
@@ -179,7 +175,7 @@ class Favicon extends Base
$dom = XmlParser::getHtmlDocument($html); $dom = XmlParser::getHtmlDocument($html);
$xpath = new DOMXpath($dom); $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) { for ($i = 0; $i < $elements->length; ++$i) {
$icons[] = $elements->item($i)->getAttribute('href'); $icons[] = $elements->item($i)->getAttribute('href');

View File

@@ -56,13 +56,13 @@ class Reader extends Base
/** /**
* Discover and download a feed. * Discover and download a feed.
* *
* @param string $url Feed or website url * @param string $url Feed or website url
* @param string $last_modified Last modified HTTP header * @param string $last_modified Last modified HTTP header
* @param string $etag Etag HTTP header * @param string $etag Etag HTTP header
* @param string $username HTTP basic auth username * @param string $username HTTP basic auth username
* @param string $password HTTP basic auth password * @param string $password HTTP basic auth password
* * @return Client
* @return \PicoFeed\Client\Client * @throws SubscriptionNotFoundException
*/ */
public function discover($url, $last_modified = '', $etag = '', $username = '', $password = '') public function discover($url, $last_modified = '', $etag = '', $username = '', $password = '')
{ {
@@ -127,11 +127,11 @@ class Reader extends Base
/** /**
* Get a parser instance. * Get a parser instance.
* *
* @param string $url Site url * @param string $url Site url
* @param string $content Feed content * @param string $content Feed content
* @param string $encoding HTTP encoding * @param string $encoding HTTP encoding
*
* @return \PicoFeed\Parser\Parser * @return \PicoFeed\Parser\Parser
* @throws UnsupportedFeedFormatException
*/ */
public function getParser($url, $content, $encoding) public function getParser($url, $content, $encoding)
{ {
@@ -154,7 +154,6 @@ class Reader extends Base
* Detect the feed format. * Detect the feed format.
* *
* @param string $content Feed content * @param string $content Feed content
*
* @return string * @return string
*/ */
public function detectFormat($content) 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. * Add the prefix "http://" if the end-user just enter a domain name.
* *
* @param string $url Url * @param string $url Url
* @retunr string * @return string
*/ */
public function prependScheme($url) public function prependScheme($url)
{ {

View File

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

View File

@@ -4,28 +4,41 @@ return array(
'%.*%' => array( '%.*%' => array(
'test_url' => 'http://www.wired.com/gamelife/2013/09/ouya-free-the-games/', 'test_url' => 'http://www.wired.com/gamelife/2013/09/ouya-free-the-games/',
'body' => array( 'body' => array(
'//div[@data-js="gallerySlides"]', '//div[@data-js="gallerySlides"]',
'//article', '//div[starts-with(@class,"post")]',
), ),
'strip' => array( 'strip' => array(
'//*[@id="linker_widget"]', '//h1',
'//*[@class="credit"]', '//nav',
'//div[@data-js="slideCount"]', '//button',
'//*[contains(@class="visually-hidden")]', '//figure[starts-with(@class,"rad-slide")]',
'//*[@data-slide-number="_endslate"]', '//figure[starts-with(@class,"end-slate")]',
'//*[@id="related"]', '//div[contains(@class,"mobile-")]',
'//*[contains(@class, "bio")]', '//div[starts-with(@class,"mob-gallery-launcher")]',
'//*[contains(@class, "entry-footer")]', '//div[contains(@id,"mobile-")]',
'//*[contains(@class, "mobify_backtotop_link")]', '//span[contains(@class,"slide-count")]',
'//*[contains(@class, "gallery-navigation")]', '//div[contains(@class,"show-ipad")]',
'//*[contains(@class, "gallery-thumbnail")]', '//img[contains(@id,"-hero-bg")]',
'//img[contains(@src, "1x1")]', '//div[@data-js="overlayWrap"]',
'//a[contains(@href, "creativecommons")]', '//ul[contains(@class,"metadata")]',
'//a[@href="#start-of-content"]', '//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"]', '//ul[@id="article-tags"]',
'//style',
'//section[contains(@class,"footer")]'
), ),
) )
) )
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,25 @@
<?php <?php
return array( return array(
'grabber' => array( 'grabber' => array(
'%.*%' => array( '%.*%' => array(
'test_url' => 'http://arstechnica.com/tech-policy/2015/09/judge-warners-2m-happy-birthday-copyright-is-bogus/', 'test_url' => 'http://arstechnica.com/tech-policy/2015/09/judge-warners-2m-happy-birthday-copyright-is-bogus/',
'body' => array( 'body' => array(
'//header/h2', '//article',
'//section[@id="article-guts"]',
'//div[@class="superscroll-content show"]',
'//div[@class="gallery"]',
), ),
'next_page' => '//span[@class="numbers"]/a',
'strip' => array( 'strip' => array(
'//h4[@class="post-upperdek"]',
'//h1',
'//ul[@class="lSPager lSGallery"]',
'//div[@class="lSAction"]',
'//section[@class="post-meta"]',
'//figcaption', '//figcaption',
'//div[@class="post-meta"]',
'//div[@class="gallery-image-credit"]',
'//aside', '//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 <?php
return array( return array(
'grabber' => array( 'grabber' => array(
'%/index.php.*comic=.*%' => 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 <?php
return array( return array(
'grabber' => array( 'grabber' => array(
'%.*%' => array( '%.*%' => array(
'test_url' => 'http://www.bangkokpost.com/news/politics/704204/new-us-ambassador-arrives-in-bangkok', 'test_url' => 'http://www.bangkokpost.com/news/politics/704204/new-us-ambassador-arrives-in-bangkok',
'body' => array( 'body' => array(
'//div[@class="articleContents"]', '//article/div[@class="articleContents"]',
), ),
'strip' => array( 'strip' => array(
'//h2', '//h2',
@@ -13,7 +12,6 @@ return array(
'//div[@class="text-size"]', '//div[@class="text-size"]',
'//div[@class="relate-story"]', '//div[@class="relate-story"]',
'//div[@class="text-ads"]', '//div[@class="text-ads"]',
'//script',
'//ul', '//ul',
), ),
), ),

View File

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

View File

@@ -1,5 +1,4 @@
<?php <?php
return array( return array(
'filter' => array( 'filter' => array(
'%.*%' => 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 <?php
return array( return array(
'grabber' => array( 'grabber' => array(
'%.*%' => 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 <?php
return array( return array(
'grabber' => array( 'grabber' => array(
'%.*%' => array( '%.*%' => array(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
<?php <?php
return array( return array(
'filter' => array( 'filter' => array(
'%.*%' => 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 <?php
return array( return array(
'grabber' => array( 'grabber' => array(
'%.*%' => 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', '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( 'body' => array(
'//figure[@id="image-top-1"]', '//h2[@id="summary"]',
'//div[@id="story-body"]', '//div[@class="flex-video youtube"]',
'//div[contains(@class,"eza-body")]',
), ),
'strip' => array( 'strip' => array(
'//script', '//span[@id="breadcrumb"]',
'//img[@title="hide caption"]', '//div[@id="byline-wrapper"]',
'//*[contains(@class,"promo_link")]', '//div[@class="injection"]',
'//div[@id="story-embed-column"]', '//*[contains(@class,"promo_link")]',
), ),
), ),
), ),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,25 @@
<?php <?php
return array( return array(
'grabber' => array( 'grabber' => array(
'%.*%' => 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( 'body' => array(
'//div[@class="entry"]', '//div[@class="contentWell"]',
), ),
'strip' => array( 'strip' => array(
'//h1', '//h1',
'//div[@class="breadcrumbs"]',
'//div[@class="mobile"]',
'//div[@class="fromIssue"]',
'//div[contains(@class,"belowDeck")]',
'//div[@class="meta"]', '//div[@class="meta"]',
'//div[@class="shareIcons"]', '//div[@class="shareIcons"]',
'//div[@class="categories"]',
'//div[@class="navigation"]', '//div[@class="navigation"]',
'//div[@class="heading"]',
'//div[contains(@id,"-ad")]',
'//div[@class="relatedArticles"]',
'//div[@id="disqus_thread"]'
), ),
), ),
), ),

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
<?php <?php
return array( return array(
'grabber' => array( 'grabber' => array(
'%.*%' => 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 <?php
return array( return array(
'filter' => array( 'filter' => array(
'%.*%' => array( '%.*%' => array(

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