This commit is contained in:
Loïc Guibert
2022-09-30 20:02:02 +01:00
commit 66dafc36c3
2561 changed files with 454489 additions and 0 deletions

12
user/plugins/admin/vendor/autoload.php vendored Normal file
View File

@@ -0,0 +1,12 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
exit(1);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit38a448a612c5797456d245c809d4a914::getLoader();

107
user/plugins/admin/vendor/bin/picofeed vendored Normal file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../p3k/picofeed/picofeed)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/p3k/picofeed/picofeed');
exit(0);
}
}
include __DIR__ . '/..'.'/p3k/picofeed/picofeed';

120
user/plugins/admin/vendor/bin/pscss vendored Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../scssphp/scssphp/bin/pscss)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/scssphp/scssphp/bin/pscss');
exit(0);
}
}
include __DIR__ . '/..'.'/scssphp/scssphp/bin/pscss';

View File

@@ -0,0 +1,572 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var ?string */
private $vendorDir;
// PSR-4
/**
* @var array[]
* @psalm-var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
*/
private $missingClasses = array();
/** @var ?string */
private $apcuPrefix;
/**
* @var self[]
*/
private static $registeredLoaders = array();
/**
* @param ?string $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
}
/**
* @return string[]
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array[]
* @psalm-return array<string, array<int, string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $classMap
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $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.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
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 (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
*
* @return self[]
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
* @private
*/
function includeFile($file)
{
include $file;
}

View File

@@ -0,0 +1,352 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = require __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
$installed[] = self::$installed;
return $installed;
}
}

View File

@@ -0,0 +1,21 @@
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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,11 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Grav\\Plugin\\AdminPlugin' => $baseDir . '/admin.php',
);

View File

@@ -0,0 +1,10 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'7e9bd612cc444b3eed788ebbe46263a0' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/autoload.php',
);

View File

@@ -0,0 +1,10 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'PicoFeed' => array($vendorDir . '/p3k/picofeed/lib'),
);

View File

@@ -0,0 +1,13 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'ScssPhp\\ScssPhp\\' => array($vendorDir . '/scssphp/scssphp/src'),
'Laminas\\ZendFrameworkBridge\\' => array($vendorDir . '/laminas/laminas-zendframework-bridge/src'),
'Laminas\\Xml\\' => array($vendorDir . '/laminas/laminas-xml/src'),
'Grav\\Plugin\\Admin\\' => array($baseDir . '/classes/plugin'),
);

View File

@@ -0,0 +1,57 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit38a448a612c5797456d245c809d4a914
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit38a448a612c5797456d245c809d4a914', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit38a448a612c5797456d245c809d4a914', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit38a448a612c5797456d245c809d4a914::getInitializer($loader));
$loader->register(true);
$includeFiles = \Composer\Autoload\ComposerStaticInit38a448a612c5797456d245c809d4a914::$files;
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire38a448a612c5797456d245c809d4a914($fileIdentifier, $file);
}
return $loader;
}
}
/**
* @param string $fileIdentifier
* @param string $file
* @return void
*/
function composerRequire38a448a612c5797456d245c809d4a914($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}

View File

@@ -0,0 +1,73 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit38a448a612c5797456d245c809d4a914
{
public static $files = array (
'7e9bd612cc444b3eed788ebbe46263a0' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/autoload.php',
);
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'ScssPhp\\ScssPhp\\' => 16,
),
'L' =>
array (
'Laminas\\ZendFrameworkBridge\\' => 28,
'Laminas\\Xml\\' => 12,
),
'G' =>
array (
'Grav\\Plugin\\Admin\\' => 18,
),
);
public static $prefixDirsPsr4 = array (
'ScssPhp\\ScssPhp\\' =>
array (
0 => __DIR__ . '/..' . '/scssphp/scssphp/src',
),
'Laminas\\ZendFrameworkBridge\\' =>
array (
0 => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src',
),
'Laminas\\Xml\\' =>
array (
0 => __DIR__ . '/..' . '/laminas/laminas-xml/src',
),
'Grav\\Plugin\\Admin\\' =>
array (
0 => __DIR__ . '/../..' . '/classes/plugin',
),
);
public static $prefixesPsr0 = array (
'P' =>
array (
'PicoFeed' =>
array (
0 => __DIR__ . '/..' . '/p3k/picofeed/lib',
),
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Grav\\Plugin\\AdminPlugin' => __DIR__ . '/../..' . '/admin.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit38a448a612c5797456d245c809d4a914::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit38a448a612c5797456d245c809d4a914::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit38a448a612c5797456d245c809d4a914::$prefixesPsr0;
$loader->classMap = ComposerStaticInit38a448a612c5797456d245c809d4a914::$classMap;
}, null, ClassLoader::class);
}
}

View File

@@ -0,0 +1,280 @@
{
"packages": [
{
"name": "laminas/laminas-xml",
"version": "1.4.0",
"version_normalized": "1.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-xml.git",
"reference": "dcadeefdb6d7ed6b39d772b47e3845003d6ea60f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-xml/zipball/dcadeefdb6d7ed6b39d772b47e3845003d6ea60f",
"reference": "dcadeefdb6d7ed6b39d772b47e3845003d6ea60f",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-simplexml": "*",
"php": "^7.3 || ~8.0.0 || ~8.1.0"
},
"conflict": {
"zendframework/zendxml": "*"
},
"require-dev": {
"ext-iconv": "*",
"laminas/laminas-coding-standard": "~1.0.0",
"phpunit/phpunit": "^9.5.8",
"squizlabs/php_codesniffer": "3.6.1 as 2.9999999.9999999"
},
"time": "2021-11-30T02:16:35+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Laminas\\Xml\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Utility library for XML usage, best practices, and security in PHP",
"homepage": "https://laminas.dev",
"keywords": [
"laminas",
"security",
"xml"
],
"support": {
"chat": "https://laminas.dev/chat",
"forum": "https://discourse.laminas.dev",
"issues": "https://github.com/laminas/laminas-xml/issues",
"rss": "https://github.com/laminas/laminas-xml/releases.atom",
"source": "https://github.com/laminas/laminas-xml"
},
"funding": [
{
"url": "https://funding.communitybridge.org/projects/laminas-project",
"type": "community_bridge"
}
],
"install-path": "../laminas/laminas-xml"
},
{
"name": "laminas/laminas-zendframework-bridge",
"version": "1.4.1",
"version_normalized": "1.4.1.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-zendframework-bridge.git",
"reference": "88bf037259869891afce6504cacc4f8a07b24d0f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/88bf037259869891afce6504cacc4f8a07b24d0f",
"reference": "88bf037259869891afce6504cacc4f8a07b24d0f",
"shasum": ""
},
"require": {
"php": "^7.3 || ~8.0.0 || ~8.1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.3",
"psalm/plugin-phpunit": "^0.15.1",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.6"
},
"time": "2021-12-21T14:34:37+00:00",
"type": "library",
"extra": {
"laminas": {
"module": "Laminas\\ZendFrameworkBridge"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"src/autoload.php"
],
"psr-4": {
"Laminas\\ZendFrameworkBridge\\": "src//"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Alias legacy ZF class names to Laminas Project equivalents.",
"keywords": [
"ZendFramework",
"autoloading",
"laminas",
"zf"
],
"support": {
"forum": "https://discourse.laminas.dev/",
"issues": "https://github.com/laminas/laminas-zendframework-bridge/issues",
"rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom",
"source": "https://github.com/laminas/laminas-zendframework-bridge"
},
"funding": [
{
"url": "https://funding.communitybridge.org/projects/laminas-project",
"type": "community_bridge"
}
],
"install-path": "../laminas/laminas-zendframework-bridge"
},
{
"name": "p3k/picofeed",
"version": "v0.1.40",
"version_normalized": "0.1.40.0",
"source": {
"type": "git",
"url": "https://github.com/aaronpk/picofeed.git",
"reference": "356fd66d48779193b10ac28532cb4a4e11bb801c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aaronpk/picofeed/zipball/356fd66d48779193b10ac28532cb4a4e11bb801c",
"reference": "356fd66d48779193b10ac28532cb4a4e11bb801c",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"laminas/laminas-xml": "^1.2",
"php": ">=5.3.0"
},
"replace": {
"miniflux/picofeed": "0.1.35"
},
"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": "2020-04-25T17:48:36+00:00",
"bin": [
"picofeed"
],
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"PicoFeed": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frédéric Guillot"
}
],
"description": "Modern library to handle RSS/Atom feeds",
"homepage": "https://github.com/aaronpk/picoFeed",
"support": {
"issues": "https://github.com/aaronpk/picofeed/issues",
"source": "https://github.com/aaronpk/picofeed/tree/v0.1.40"
},
"install-path": "../p3k/picofeed"
},
{
"name": "scssphp/scssphp",
"version": "v1.10.4",
"version_normalized": "1.10.4.0",
"source": {
"type": "git",
"url": "https://github.com/scssphp/scssphp.git",
"reference": "8ed20753db2d3d82629e6f5d35535bbbd3893b0c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/scssphp/scssphp/zipball/8ed20753db2d3d82629e6f5d35535bbbd3893b0c",
"reference": "8ed20753db2d3d82629e6f5d35535bbbd3893b0c",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-json": "*",
"php": ">=5.6.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4",
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4",
"sass/sass-spec": "*",
"squizlabs/php_codesniffer": "~3.5",
"symfony/phpunit-bridge": "^5.1",
"thoughtbot/bourbon": "^7.0",
"twbs/bootstrap": "~5.0",
"twbs/bootstrap4": "4.6.1",
"zurb/foundation": "~6.5"
},
"suggest": {
"ext-iconv": "Can be used as fallback when ext-mbstring is not available",
"ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv"
},
"time": "2022-07-26T16:28:33+00:00",
"bin": [
"bin/pscss"
],
"type": "library",
"extra": {
"bamarni-bin": {
"forward-command": false,
"bin-links": false
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"ScssPhp\\ScssPhp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Anthon Pang",
"email": "apang@softwaredevelopment.ca",
"homepage": "https://github.com/robocoder"
},
{
"name": "Cédric Morin",
"email": "cedric@yterium.com",
"homepage": "https://github.com/Cerdic"
}
],
"description": "scssphp is a compiler for SCSS written in PHP.",
"homepage": "http://scssphp.github.io/scssphp/",
"keywords": [
"css",
"less",
"sass",
"scss",
"stylesheet"
],
"support": {
"issues": "https://github.com/scssphp/scssphp/issues",
"source": "https://github.com/scssphp/scssphp/tree/v1.10.4"
},
"install-path": "../scssphp/scssphp"
}
],
"dev": false,
"dev-package-names": []
}

View File

@@ -0,0 +1,77 @@
<?php return array(
'root' => array(
'name' => 'getgrav/grav-plugin-admin',
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '713002e2b83b9660bfcefe5bb0d76954eedf2b90',
'type' => 'grav-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => false,
),
'versions' => array(
'getgrav/grav-plugin-admin' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '713002e2b83b9660bfcefe5bb0d76954eedf2b90',
'type' => 'grav-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'laminas/laminas-xml' => array(
'pretty_version' => '1.4.0',
'version' => '1.4.0.0',
'reference' => 'dcadeefdb6d7ed6b39d772b47e3845003d6ea60f',
'type' => 'library',
'install_path' => __DIR__ . '/../laminas/laminas-xml',
'aliases' => array(),
'dev_requirement' => false,
),
'laminas/laminas-zendframework-bridge' => array(
'pretty_version' => '1.4.1',
'version' => '1.4.1.0',
'reference' => '88bf037259869891afce6504cacc4f8a07b24d0f',
'type' => 'library',
'install_path' => __DIR__ . '/../laminas/laminas-zendframework-bridge',
'aliases' => array(),
'dev_requirement' => false,
),
'miniflux/picofeed' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '0.1.35',
),
),
'p3k/picofeed' => array(
'pretty_version' => 'v0.1.40',
'version' => '0.1.40.0',
'reference' => '356fd66d48779193b10ac28532cb4a4e11bb801c',
'type' => 'library',
'install_path' => __DIR__ . '/../p3k/picofeed',
'aliases' => array(),
'dev_requirement' => false,
),
'scssphp/scssphp' => array(
'pretty_version' => 'v1.10.4',
'version' => '1.10.4.0',
'reference' => '8ed20753db2d3d82629e6f5d35535bbbd3893b0c',
'type' => 'library',
'install_path' => __DIR__ . '/../scssphp/scssphp',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php72' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'symfony/polyfill-php73' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
),
);

View File

@@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70306)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.6". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@@ -0,0 +1,22 @@
name: Autocloser
on: [issues, pull_request]
jobs:
autoclose:
runs-on: ubuntu-latest
steps:
- name: Autoclose new issues and PRs
uses: roots/issue-closer@v1.1
with:
repo-token: ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
issue-pattern: "^exact-string-will-never-match$"
pr-pattern: "^exact-string-will-never-match$"
issue-close-message: |
This package is considered feature-complete, and is now in **security-only** maintenance mode, following a [decision by the Technical Steering Committee](https://github.com/laminas/technical-steering-committee/blob/2b55453e172a1b8c9c4c212be7cf7e7a58b9352c/meetings/minutes/2020-08-03-TSC-Minutes.md#vote-on-components-to-mark-as-security-only).
If you have a security issue, please [follow our security reporting guidelines](https://getlaminas.org/security/).
If you wish to take on the role of maintainer, please [nominate yourself](https://github.com/laminas/technical-steering-committee/issues/new?assignees=&labels=Nomination&template=Maintainer_Nomination.md&title=%5BNOMINATION%5D%5BMAINTAINER%5D%3A+%7Bname+of+person+being+nominated%7D)
pr-close-message: |
This package is considered feature-complete, and is now in **security-only** maintenance mode, following a [decision by the Technical Steering Committee](https://github.com/laminas/technical-steering-committee/blob/2b55453e172a1b8c9c4c212be7cf7e7a58b9352c/meetings/minutes/2020-08-03-TSC-Minutes.md#vote-on-components-to-mark-as-security-only).
If you have a security issue, please [follow our security reporting guidelines](https://getlaminas.org/security/).
If you wish to take on the role of maintainer, please [nominate yourself](https://github.com/laminas/technical-steering-committee/issues/new?assignees=&labels=Nomination&template=Maintainer_Nomination.md&title=%5BNOMINATION%5D%5BMAINTAINER%5D%3A+%7Bname+of+person+being+nominated%7D)

View File

@@ -0,0 +1,33 @@
name: "Continuous Integration"
on:
pull_request:
push:
branches:
- '[0-9]+.[0-9]+.x'
- 'refs/pull/*'
tags:
jobs:
matrix:
name: Generate job matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- name: Gather CI configuration
id: matrix
uses: laminas/laminas-ci-matrix-action@v1
qa:
name: QA Checks
needs: [matrix]
runs-on: ${{ matrix.operatingSystem }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }}
steps:
- name: ${{ matrix.name }}
uses: laminas/laminas-continuous-integration-action@v1
with:
job: ${{ matrix.job }}

View File

@@ -0,0 +1,71 @@
# Alternate workflow example.
# This one is identical to the one in release-on-milestone.yml, with one change:
# the Release step uses the ORGANIZATION_ADMIN_TOKEN instead, to allow it to
# trigger a release workflow event. This is useful if you have other actions
# that intercept that event.
name: "Automatic Releases"
on:
milestone:
types:
- "closed"
jobs:
release:
name: "GIT tag, release & create merge-up PR"
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Release"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:release"
env:
"GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: "Create Merge-Up Pull Request"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:create-merge-up-pull-request"
env:
"GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: "Create and/or Switch to new Release Branch"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:switch-default-branch-to-next-minor"
env:
"GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: "Bump Changelog Version On Originating Release Branch"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:bump-changelog"
env:
"GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: "Create new milestones"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:create-milestones"
env:
"GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}

View File

@@ -0,0 +1 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/)

View File

@@ -0,0 +1,26 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of Laminas Foundation nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,51 @@
# laminas-xml
> This package is considered feature-complete, and is now in **security-only** maintenance mode, following a [decision by the Technical Steering Committee](https://github.com/laminas/technical-steering-committee/blob/2b55453e172a1b8c9c4c212be7cf7e7a58b9352c/meetings/minutes/2020-08-03-TSC-Minutes.md#vote-on-components-to-mark-as-security-only).
> If you have a security issue, please [follow our security reporting guidelines](https://getlaminas.org/security/).
> If you wish to take on the role of maintainer, please [nominate yourself](https://github.com/laminas/technical-steering-committee/issues/new?assignees=&labels=Nomination&template=Maintainer_Nomination.md&title=%5BNOMINATION%5D%5BMAINTAINER%5D%3A+%7Bname+of+person+being+nominated%7D)
[![Build Status](https://github.com/laminas/laminas-xml/workflows/Continuous%20Integration/badge.svg)](https://github.com/laminas/laminas-xml/actions?query=workflow%3A"Continuous+Integration")
An utility component for XML usage and best practices in PHP
## Installation
You can install using:
```bash
$ curl -s https://getcomposer.org/installer | php
$ php composer.phar install
```
Notice that this library doesn't have any external dependencies, the usage of composer is for autoloading and standard purpose.
## Laminas\Xml\Security
This is a security component to prevent [XML eXternal Entity](https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing) (XXE) and [XML Entity Expansion](http://projects.webappsec.org/w/page/13247002/XML%20Entity%20Expansion) (XEE) attacks on XML documents.
The XXE attack is prevented disabling the load of external entities in the libxml library used by PHP, using the function [libxml_disable_entity_loader](http://www.php.net/manual/en/function.libxml-disable-entity-loader.php).
The XEE attack is prevented looking inside the XML document for ENTITY usage. If the XML document uses ENTITY the library throw an Exception.
We have two static methods to scan and load XML document from a string (scan) and from a file (scanFile). You can decide to get a SimpleXMLElement or DOMDocument as result, using the following use cases:
```php
use Laminas\Xml\Security as XmlSecurity;
$xml = <<<XML
<?xml version="1.0"?>
<results>
<result>test</result>
</results>
XML;
// SimpleXML use case
$simplexml = XmlSecurity::scan($xml);
printf ("SimpleXMLElement: %s\n", ($simplexml instanceof \SimpleXMLElement) ? 'yes' : 'no');
// DOMDocument use case
$dom = new \DOMDocument('1.0');
$dom = XmlSecurity::scan($xml, $dom);
printf ("DOMDocument: %s\n", ($dom instanceof \DOMDocument) ? 'yes' : 'no');
```

View File

@@ -0,0 +1,55 @@
{
"name": "laminas/laminas-xml",
"description": "Utility library for XML usage, best practices, and security in PHP",
"license": "BSD-3-Clause",
"keywords": [
"laminas",
"xml",
"security"
],
"homepage": "https://laminas.dev",
"support": {
"issues": "https://github.com/laminas/laminas-xml/issues",
"source": "https://github.com/laminas/laminas-xml",
"rss": "https://github.com/laminas/laminas-xml/releases.atom",
"chat": "https://laminas.dev/chat",
"forum": "https://discourse.laminas.dev"
},
"config": {
"sort-packages": true
},
"require": {
"php": "^7.3 || ~8.0.0 || ~8.1.0",
"ext-dom": "*",
"ext-simplexml": "*"
},
"require-dev": {
"laminas/laminas-coding-standard": "~1.0.0",
"squizlabs/php_codesniffer": "3.6.1 as 2.9999999.9999999",
"phpunit/phpunit": "^9.5.8",
"ext-iconv": "*"
},
"autoload": {
"psr-4": {
"Laminas\\Xml\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LaminasTest\\Xml\\": "test/"
}
},
"scripts": {
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
},
"conflict": {
"zendframework/zendxml": "*"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
<?php
namespace Laminas\Xml\Exception;
interface ExceptionInterface
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Laminas\Xml\Exception;
/**
* Invalid argument exception
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Laminas\Xml\Exception;
/**
* Runtime exception
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,407 @@
<?php
namespace Laminas\Xml;
use DOMDocument;
use SimpleXMLElement;
class Security
{
const ENTITY_DETECT = 'Detected use of ENTITY in XML, disabled to prevent XXE/XEE attacks';
/**
* Heuristic scan to detect entity in XML
*
* @param string $xml
* @throws Exception\RuntimeException If entity expansion or external entity declaration was discovered.
*/
protected static function heuristicScan($xml)
{
foreach (self::getEntityComparison($xml) as $compare) {
if (strpos($xml, $compare) !== false) {
throw new Exception\RuntimeException(self::ENTITY_DETECT);
}
}
}
/**
* Scan XML string for potential XXE and XEE attacks
*
* @param string $xml
* @param DomDocument $dom
* @param int $libXmlConstants additional libxml constants to pass in
* @param callable $callback the callback to use to create the dom element
* @throws Exception\RuntimeException
* @return SimpleXMLElement|DomDocument|boolean
*/
private static function scanString($xml, DOMDocument $dom = null, $libXmlConstants, callable $callback)
{
// If running with PHP-FPM we perform an heuristic scan
// We cannot use libxml_disable_entity_loader because of this bug
// @see https://bugs.php.net/bug.php?id=64938
if (self::isPhpFpm()) {
self::heuristicScan($xml);
}
if (null === $dom) {
$simpleXml = true;
$dom = new DOMDocument();
}
if (! self::isPhpFpm()) {
if (\PHP_VERSION_ID < 80000) {
$loadEntities = libxml_disable_entity_loader(true);
}
$useInternalXmlErrors = libxml_use_internal_errors(true);
}
// Load XML with network access disabled (LIBXML_NONET)
// error disabled with @ for PHP-FPM scenario
set_error_handler(function ($errno, $errstr) {
if (substr_count($errstr, 'DOMDocument::loadXML()') > 0) {
return true;
}
return false;
}, E_WARNING);
$result = $callback($xml, $dom, LIBXML_NONET | $libXmlConstants);
restore_error_handler();
if (! $result) {
// Entity load to previous setting
if (! self::isPhpFpm()) {
if (\PHP_VERSION_ID < 80000) {
libxml_disable_entity_loader($loadEntities);
}
libxml_use_internal_errors($useInternalXmlErrors);
}
return false;
}
// Scan for potential XEE attacks using ENTITY, if not PHP-FPM
if (! self::isPhpFpm()) {
foreach ($dom->childNodes as $child) {
if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
if ($child->entities->length > 0) {
throw new Exception\RuntimeException(self::ENTITY_DETECT);
}
}
}
}
// Entity load to previous setting
if (! self::isPhpFpm()) {
if (\PHP_VERSION_ID < 80000) {
libxml_disable_entity_loader($loadEntities);
}
libxml_use_internal_errors($useInternalXmlErrors);
}
if (isset($simpleXml)) {
$result = simplexml_import_dom($dom);
if (! $result instanceof SimpleXMLElement) {
return false;
}
return $result;
}
return $dom;
}
/**
* Scan XML string for potential XXE and XEE attacks
*
* @param string $xml
* @param DomDocument $dom
* @param int $libXmlConstants additional libxml constants to pass in
* @throws Exception\RuntimeException
* @return SimpleXMLElement|DomDocument|boolean
*/
public static function scan($xml, DOMDocument $dom = null, $libXmlConstants = 0)
{
$callback = function ($xml, $dom, $constants) {
return $dom->loadXml($xml, $constants);
};
return self::scanString($xml, $dom, $libXmlConstants, $callback);
}
/**
* Scan HTML string for potential XXE and XEE attacks
*
* @param string $xml
* @param DomDocument $dom
* @param int $libXmlConstants additional libxml constants to pass in
* @throws Exception\RuntimeException
* @return SimpleXMLElement|DomDocument|boolean
*/
public static function scanHtml($html, DOMDocument $dom = null, $libXmlConstants = 0)
{
$callback = function ($html, $dom, $constants) {
return $dom->loadHtml($html, $constants);
};
return self::scanString($html, $dom, $libXmlConstants, $callback);
}
/**
* Scan XML file for potential XXE/XEE attacks
*
* @param string $file
* @param DOMDocument $dom
* @throws Exception\InvalidArgumentException
* @return SimpleXMLElement|DomDocument
*/
public static function scanFile($file, DOMDocument $dom = null)
{
if (! file_exists($file)) {
throw new Exception\InvalidArgumentException(
"The file $file specified doesn't exist"
);
}
return self::scan(file_get_contents($file), $dom);
}
/**
* Return true if PHP is running with PHP-FPM
*
* This method is mainly used to determine whether or not heuristic checks
* (vs libxml checks) should be made, due to threading issues in libxml;
* under php-fpm, threading becomes a concern.
*
* However, PHP versions 5.6.6+ contain a patch to the
* libxml support in PHP that makes the libxml checks viable; in such
* versions, this method will return false to enforce those checks, which
* are more strict and accurate than the heuristic checks.
*
* @return boolean
*/
public static function isPhpFpm()
{
$isVulnerableVersion = version_compare(PHP_VERSION, '5.6', 'ge')
&& version_compare(PHP_VERSION, '5.6.6', 'lt');
if (0 === strpos(php_sapi_name(), 'fpm') && $isVulnerableVersion) {
return true;
}
return false;
}
/**
* Determine and return the string(s) to use for the <!ENTITY comparison.
*
* @param string $xml
* @return string[]
*/
protected static function getEntityComparison($xml)
{
$encodingMap = self::getAsciiEncodingMap();
return array_map(function ($encoding) use ($encodingMap) {
$generator = isset($encodingMap[$encoding]) ? $encodingMap[$encoding] : $encodingMap['UTF-8'];
return $generator('<!ENTITY');
}, self::detectXmlEncoding($xml, self::detectStringEncoding($xml)));
}
/**
* Determine the string encoding.
*
* Determines string encoding from either a detected BOM or a
* heuristic.
*
* @param string $xml
* @return string File encoding
*/
protected static function detectStringEncoding($xml)
{
return self::detectBom($xml) ?: self::detectXmlStringEncoding($xml);
}
/**
* Attempt to match a known BOM.
*
* Iterates through the return of getBomMap(), comparing the initial bytes
* of the provided string to the BOM of each; if a match is determined,
* it returns the encoding.
*
* @param string $string
* @return false|string Returns encoding on success.
*/
protected static function detectBom($string)
{
foreach (self::getBomMap() as $criteria) {
if (0 === strncmp($string, $criteria['bom'], $criteria['length'])) {
return $criteria['encoding'];
}
}
return false;
}
/**
* Attempt to detect the string encoding of an XML string.
*
* @param string $xml
* @return string Encoding
*/
protected static function detectXmlStringEncoding($xml)
{
foreach (self::getAsciiEncodingMap() as $encoding => $generator) {
$prefix = $generator('<' . '?xml');
if (0 === strncmp($xml, $prefix, strlen($prefix))) {
return $encoding;
}
}
// Fallback
return 'UTF-8';
}
/**
* Attempt to detect the specified XML encoding.
*
* Using the file's encoding, determines if an "encoding" attribute is
* present and well-formed in the XML declaration; if so, it returns a
* list with both the ASCII representation of that declaration and the
* original file encoding.
*
* If not, a list containing only the provided file encoding is returned.
*
* @param string $xml
* @param string $fileEncoding
* @return string[] Potential XML encodings
*/
protected static function detectXmlEncoding($xml, $fileEncoding)
{
$encodingMap = self::getAsciiEncodingMap();
$generator = $encodingMap[$fileEncoding];
$encAttr = $generator('encoding="');
$quote = $generator('"');
$close = $generator('>');
$closePos = strpos($xml, $close);
if (false === $closePos) {
return [$fileEncoding];
}
$encPos = strpos($xml, $encAttr);
if (false === $encPos
|| $encPos > $closePos
) {
return [$fileEncoding];
}
$encPos += strlen($encAttr);
$quotePos = strpos($xml, $quote, $encPos);
if (false === $quotePos) {
return [$fileEncoding];
}
$encoding = self::substr($xml, $encPos, $quotePos);
return [
// Following line works because we're only supporting 8-bit safe encodings at this time.
str_replace('\0', '', $encoding), // detected encoding
$fileEncoding, // file encoding
];
}
/**
* Return a list of BOM maps.
*
* Returns a list of common encoding -> BOM maps, along with the character
* length to compare against.
*
* @link https://en.wikipedia.org/wiki/Byte_order_mark
* @return array
*/
protected static function getBomMap()
{
return [
[
'encoding' => 'UTF-32BE',
'bom' => pack('CCCC', 0x00, 0x00, 0xfe, 0xff),
'length' => 4,
],
[
'encoding' => 'UTF-32LE',
'bom' => pack('CCCC', 0xff, 0xfe, 0x00, 0x00),
'length' => 4,
],
[
'encoding' => 'GB-18030',
'bom' => pack('CCCC', 0x84, 0x31, 0x95, 0x33),
'length' => 4,
],
[
'encoding' => 'UTF-16BE',
'bom' => pack('CC', 0xfe, 0xff),
'length' => 2,
],
[
'encoding' => 'UTF-16LE',
'bom' => pack('CC', 0xff, 0xfe),
'length' => 2,
],
[
'encoding' => 'UTF-8',
'bom' => pack('CCC', 0xef, 0xbb, 0xbf),
'length' => 3,
],
];
}
/**
* Return a map of encoding => generator pairs.
*
* Returns a map of encoding => generator pairs, where the generator is a
* callable that accepts a string and returns the appropriate byte order
* sequence of that string for the encoding.
*
* @return array
*/
protected static function getAsciiEncodingMap()
{
return [
'UTF-32BE' => function ($ascii) {
return preg_replace('/(.)/', "\0\0\0\\1", $ascii);
},
'UTF-32LE' => function ($ascii) {
return preg_replace('/(.)/', "\\1\0\0\0", $ascii);
},
'UTF-32odd1' => function ($ascii) {
return preg_replace('/(.)/', "\0\\1\0\0", $ascii);
},
'UTF-32odd2' => function ($ascii) {
return preg_replace('/(.)/', "\0\0\\1\0", $ascii);
},
'UTF-16BE' => function ($ascii) {
return preg_replace('/(.)/', "\0\\1", $ascii);
},
'UTF-16LE' => function ($ascii) {
return preg_replace('/(.)/', "\\1\0", $ascii);
},
'UTF-8' => function ($ascii) {
return $ascii;
},
'GB-18030' => function ($ascii) {
return $ascii;
},
];
}
/**
* Binary-safe substr.
*
* substr() is not binary-safe; this method loops by character to ensure
* multi-byte characters are aggregated correctly.
*
* @param string $string
* @param int $start
* @param int $end
* @return string
*/
protected static function substr($string, $start, $end)
{
$substr = '';
for ($i = $start; $i < $end; $i += 1) {
$substr .= $string[$i];
}
return $substr;
}
}

View File

@@ -0,0 +1 @@
community_bridge: laminas-project

View File

@@ -0,0 +1,32 @@
name: "Continuous Integration"
on:
pull_request:
push:
branches:
- '[0-9]+.[0-9]+.x'
- 'refs/pull/*'
jobs:
matrix:
name: Generate job matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- name: Gather CI configuration
id: matrix
uses: laminas/laminas-ci-matrix-action@v1
qa:
name: QA Checks
needs: [matrix]
runs-on: ${{ matrix.operatingSystem }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }}
steps:
- name: ${{ matrix.name }}
uses: laminas/laminas-continuous-integration-action@v1
with:
job: ${{ matrix.job }}

View File

@@ -0,0 +1,61 @@
# Alternate workflow example.
# This one is identical to the one in release-on-milestone.yml, with one change:
# the Release step uses the ORGANIZATION_ADMIN_TOKEN instead, to allow it to
# trigger a release workflow event. This is useful if you have other actions
# that intercept that event.
name: "Automatic Releases"
on:
milestone:
types:
- "closed"
jobs:
release:
name: "GIT tag, release & create merge-up PR"
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Release"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:release"
env:
"GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: "Create Merge-Up Pull Request"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:create-merge-up-pull-request"
env:
"GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: "Create and/or Switch to new Release Branch"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:switch-default-branch-to-next-minor"
env:
"GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: "Create new milestones"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:create-milestones"
env:
"GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}

View File

@@ -0,0 +1,5 @@
{
"ignore_php_platform_requirements": {
"8.1": true
}
}

View File

@@ -0,0 +1 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/)

View File

@@ -0,0 +1,26 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of Laminas Foundation nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,30 @@
# laminas-zendframework-bridge
[![Build Status](https://github.com/laminas/laminas-zendframework-bridge/workflows/Continuous%20Integration/badge.svg)](https://github.com/laminas/laminas-zendframework-bridge/actions?query=workflow%3A"Continuous+Integration")
This library provides a custom autoloader that aliases legacy Zend Framework,
Apigility, and Expressive classes to their replacements under the Laminas
Project.
This package should be installed only if you are also using the composer plugin
that installs Laminas packages to replace ZF/Apigility/Expressive packages.
This tool supports:
* Zend Framework MVC projects, all v2 and v3 releases
* Apigility projects, all stable versions
* Expressive versions, all stable versions
## Installation
Run the following to install this library:
```bash
$ composer require laminas/laminas-zendframework-bridge
```
## Support
* [Issues](https://github.com/laminas/laminas-zendframework-bridge/issues/)
* [Forum](https://discourse.laminas.dev/)

View File

@@ -0,0 +1,61 @@
{
"name": "laminas/laminas-zendframework-bridge",
"description": "Alias legacy ZF class names to Laminas Project equivalents.",
"license": "BSD-3-Clause",
"keywords": [
"autoloading",
"laminas",
"zf",
"zendframework"
],
"support": {
"issues": "https://github.com/laminas/laminas-zendframework-bridge/issues",
"source": "https://github.com/laminas/laminas-zendframework-bridge",
"rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom",
"forum": "https://discourse.laminas.dev/"
},
"require": {
"php": "^7.3 || ~8.0.0 || ~8.1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.3",
"psalm/plugin-phpunit": "^0.15.1",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.6"
},
"autoload": {
"files": [
"src/autoload.php"
],
"psr-4": {
"Laminas\\ZendFrameworkBridge\\": "src//"
}
},
"autoload-dev": {
"files": [
"test/classes.php"
],
"psr-4": {
"LaminasTest\\ZendFrameworkBridge\\": "test/",
"LaminasTest\\ZendFrameworkBridge\\TestAsset\\": "test/TestAsset/classes/",
"Laminas\\ApiTools\\": "test/TestAsset/LaminasApiTools/",
"Mezzio\\": "test/TestAsset/Mezzio/",
"Laminas\\": "test/TestAsset/Laminas/"
}
},
"extra": {
"laminas": {
"module": "Laminas\\ZendFrameworkBridge"
}
},
"config": {
"sort-packages": true
},
"scripts": {
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"static-analysis": "psalm --shepherd --stats",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,372 @@
<?php
return [
// NEVER REWRITE
'zendframework/zendframework' => 'zendframework/zendframework',
'zend-developer-tools/toolbar/bjy' => 'zend-developer-tools/toolbar/bjy',
'zend-developer-tools/toolbar/doctrine' => 'zend-developer-tools/toolbar/doctrine',
// NAMESPACES
// Zend Framework components
'Zend\\AuraDi\\Config' => 'Laminas\\AuraDi\\Config',
'Zend\\Authentication' => 'Laminas\\Authentication',
'Zend\\Barcode' => 'Laminas\\Barcode',
'Zend\\Cache' => 'Laminas\\Cache',
'Zend\\Captcha' => 'Laminas\\Captcha',
'Zend\\Code' => 'Laminas\\Code',
'ZendCodingStandard\\Sniffs' => 'LaminasCodingStandard\\Sniffs',
'ZendCodingStandard\\Utils' => 'LaminasCodingStandard\\Utils',
'Zend\\ComponentInstaller' => 'Laminas\\ComponentInstaller',
'Zend\\Config' => 'Laminas\\Config',
'Zend\\ConfigAggregator' => 'Laminas\\ConfigAggregator',
'Zend\\ConfigAggregatorModuleManager' => 'Laminas\\ConfigAggregatorModuleManager',
'Zend\\ConfigAggregatorParameters' => 'Laminas\\ConfigAggregatorParameters',
'Zend\\Console' => 'Laminas\\Console',
'Zend\\ContainerConfigTest' => 'Laminas\\ContainerConfigTest',
'Zend\\Crypt' => 'Laminas\\Crypt',
'Zend\\Db' => 'Laminas\\Db',
'ZendDeveloperTools' => 'Laminas\\DeveloperTools',
'Zend\\Di' => 'Laminas\\Di',
'Zend\\Diactoros' => 'Laminas\\Diactoros',
'ZendDiagnostics\\Check' => 'Laminas\\Diagnostics\\Check',
'ZendDiagnostics\\Result' => 'Laminas\\Diagnostics\\Result',
'ZendDiagnostics\\Runner' => 'Laminas\\Diagnostics\\Runner',
'Zend\\Dom' => 'Laminas\\Dom',
'Zend\\Escaper' => 'Laminas\\Escaper',
'Zend\\EventManager' => 'Laminas\\EventManager',
'Zend\\Feed' => 'Laminas\\Feed',
'Zend\\File' => 'Laminas\\File',
'Zend\\Filter' => 'Laminas\\Filter',
'Zend\\Form' => 'Laminas\\Form',
'Zend\\Http' => 'Laminas\\Http',
'Zend\\HttpHandlerRunner' => 'Laminas\\HttpHandlerRunner',
'Zend\\Hydrator' => 'Laminas\\Hydrator',
'Zend\\I18n' => 'Laminas\\I18n',
'Zend\\InputFilter' => 'Laminas\\InputFilter',
'Zend\\Json' => 'Laminas\\Json',
'Zend\\Ldap' => 'Laminas\\Ldap',
'Zend\\Loader' => 'Laminas\\Loader',
'Zend\\Log' => 'Laminas\\Log',
'Zend\\Mail' => 'Laminas\\Mail',
'Zend\\Math' => 'Laminas\\Math',
'Zend\\Memory' => 'Laminas\\Memory',
'Zend\\Mime' => 'Laminas\\Mime',
'Zend\\ModuleManager' => 'Laminas\\ModuleManager',
'Zend\\Mvc' => 'Laminas\\Mvc',
'Zend\\Navigation' => 'Laminas\\Navigation',
'Zend\\Paginator' => 'Laminas\\Paginator',
'Zend\\Permissions' => 'Laminas\\Permissions',
'Zend\\Pimple\\Config' => 'Laminas\\Pimple\\Config',
'Zend\\ProblemDetails' => 'Mezzio\\ProblemDetails',
'Zend\\ProgressBar' => 'Laminas\\ProgressBar',
'Zend\\Psr7Bridge' => 'Laminas\\Psr7Bridge',
'Zend\\Router' => 'Laminas\\Router',
'Zend\\Serializer' => 'Laminas\\Serializer',
'Zend\\Server' => 'Laminas\\Server',
'Zend\\ServiceManager' => 'Laminas\\ServiceManager',
'ZendService\\ReCaptcha' => 'Laminas\\ReCaptcha',
'ZendService\\Twitter' => 'Laminas\\Twitter',
'Zend\\Session' => 'Laminas\\Session',
'Zend\\SkeletonInstaller' => 'Laminas\\SkeletonInstaller',
'Zend\\Soap' => 'Laminas\\Soap',
'Zend\\Stdlib' => 'Laminas\\Stdlib',
'Zend\\Stratigility' => 'Laminas\\Stratigility',
'Zend\\Tag' => 'Laminas\\Tag',
'Zend\\Test' => 'Laminas\\Test',
'Zend\\Text' => 'Laminas\\Text',
'Zend\\Uri' => 'Laminas\\Uri',
'Zend\\Validator' => 'Laminas\\Validator',
'Zend\\View' => 'Laminas\\View',
'ZendXml' => 'Laminas\\Xml',
'Zend\\Xml2Json' => 'Laminas\\Xml2Json',
'Zend\\XmlRpc' => 'Laminas\\XmlRpc',
'ZendOAuth' => 'Laminas\\OAuth',
// class ZendAcl in zend-expressive-authorization-acl
'ZendAcl' => 'LaminasAcl',
'Zend\\Expressive\\Authorization\\Acl\\ZendAcl' => 'Mezzio\\Authorization\\Acl\\LaminasAcl',
// class ZendHttpClientDecorator in zend-feed
'ZendHttp' => 'LaminasHttp',
// class ZendModuleProvider in zend-config-aggregator-modulemanager
'ZendModule' => 'LaminasModule',
// class ZendRbac in zend-expressive-authorization-rbac
'ZendRbac' => 'LaminasRbac',
'Zend\\Expressive\\Authorization\\Rbac\\ZendRbac' => 'Mezzio\\Authorization\\Rbac\\LaminasRbac',
// class ZendRouter in zend-expressive-router-zendrouter
'ZendRouter' => 'LaminasRouter',
'Zend\\Expressive\\Router\\ZendRouter' => 'Mezzio\\Router\\LaminasRouter',
// class ZendViewRenderer in zend-expressive-zendviewrenderer
'ZendViewRenderer' => 'LaminasViewRenderer',
'Zend\\Expressive\\ZendView\\ZendViewRenderer' => 'Mezzio\\LaminasView\\LaminasViewRenderer',
'a\\Zend' => 'a\\Zend',
'b\\Zend' => 'b\\Zend',
'c\\Zend' => 'c\\Zend',
'd\\Zend' => 'd\\Zend',
'e\\Zend' => 'e\\Zend',
'f\\Zend' => 'f\\Zend',
'g\\Zend' => 'g\\Zend',
'h\\Zend' => 'h\\Zend',
'i\\Zend' => 'i\\Zend',
'j\\Zend' => 'j\\Zend',
'k\\Zend' => 'k\\Zend',
'l\\Zend' => 'l\\Zend',
'm\\Zend' => 'm\\Zend',
'n\\Zend' => 'n\\Zend',
'o\\Zend' => 'o\\Zend',
'p\\Zend' => 'p\\Zend',
'q\\Zend' => 'q\\Zend',
'r\\Zend' => 'r\\Zend',
's\\Zend' => 's\\Zend',
't\\Zend' => 't\\Zend',
'u\\Zend' => 'u\\Zend',
'v\\Zend' => 'v\\Zend',
'w\\Zend' => 'w\\Zend',
'x\\Zend' => 'x\\Zend',
'y\\Zend' => 'y\\Zend',
'z\\Zend' => 'z\\Zend',
// Expressive
'Zend\\Expressive' => 'Mezzio',
'ZendAuthentication' => 'LaminasAuthentication',
'ZendAcl' => 'LaminasAcl',
'ZendRbac' => 'LaminasRbac',
'ZendRouter' => 'LaminasRouter',
'ExpressiveUrlGenerator' => 'MezzioUrlGenerator',
'ExpressiveInstaller' => 'MezzioInstaller',
// Apigility
'ZF\\Apigility' => 'Laminas\\ApiTools',
'ZF\\ApiProblem' => 'Laminas\\ApiTools\\ApiProblem',
'ZF\\AssetManager' => 'Laminas\\ApiTools\\AssetManager',
'ZF\\ComposerAutoloading' => 'Laminas\\ComposerAutoloading',
'ZF\\Configuration' => 'Laminas\\ApiTools\\Configuration',
'ZF\\ContentNegotiation' => 'Laminas\\ApiTools\\ContentNegotiation',
'ZF\\ContentValidation' => 'Laminas\\ApiTools\\ContentValidation',
'ZF\\DevelopmentMode' => 'Laminas\\DevelopmentMode',
'ZF\\Doctrine\\QueryBuilder' => 'Laminas\\ApiTools\\Doctrine\\QueryBuilder',
'ZF\\Hal' => 'Laminas\\ApiTools\\Hal',
'ZF\\HttpCache' => 'Laminas\\ApiTools\\HttpCache',
'ZF\\MvcAuth' => 'Laminas\\ApiTools\\MvcAuth',
'ZF\\OAuth2' => 'Laminas\\ApiTools\\OAuth2',
'ZF\\Rest' => 'Laminas\\ApiTools\\Rest',
'ZF\\Rpc' => 'Laminas\\ApiTools\\Rpc',
'ZF\\Versioning' => 'Laminas\\ApiTools\\Versioning',
'a\\ZF' => 'a\\ZF',
'b\\ZF' => 'b\\ZF',
'c\\ZF' => 'c\\ZF',
'd\\ZF' => 'd\\ZF',
'e\\ZF' => 'e\\ZF',
'f\\ZF' => 'f\\ZF',
'g\\ZF' => 'g\\ZF',
'h\\ZF' => 'h\\ZF',
'i\\ZF' => 'i\\ZF',
'j\\ZF' => 'j\\ZF',
'k\\ZF' => 'k\\ZF',
'l\\ZF' => 'l\\ZF',
'm\\ZF' => 'm\\ZF',
'n\\ZF' => 'n\\ZF',
'o\\ZF' => 'o\\ZF',
'p\\ZF' => 'p\\ZF',
'q\\ZF' => 'q\\ZF',
'r\\ZF' => 'r\\ZF',
's\\ZF' => 's\\ZF',
't\\ZF' => 't\\ZF',
'u\\ZF' => 'u\\ZF',
'v\\ZF' => 'v\\ZF',
'w\\ZF' => 'w\\ZF',
'x\\ZF' => 'x\\ZF',
'y\\ZF' => 'y\\ZF',
'z\\ZF' => 'z\\ZF',
'ApigilityModuleInterface' => 'ApiToolsModuleInterface',
'ApigilityProviderInterface' => 'ApiToolsProviderInterface',
'ApigilityVersionController' => 'ApiToolsVersionController',
// PACKAGES
// ZF components, MVC
'zendframework/skeleton-application' => 'laminas/skeleton-application',
'zendframework/zend-auradi-config' => 'laminas/laminas-auradi-config',
'zendframework/zend-authentication' => 'laminas/laminas-authentication',
'zendframework/zend-barcode' => 'laminas/laminas-barcode',
'zendframework/zend-cache' => 'laminas/laminas-cache',
'zendframework/zend-captcha' => 'laminas/laminas-captcha',
'zendframework/zend-code' => 'laminas/laminas-code',
'zendframework/zend-coding-standard' => 'laminas/laminas-coding-standard',
'zendframework/zend-component-installer' => 'laminas/laminas-component-installer',
'zendframework/zend-composer-autoloading' => 'laminas/laminas-composer-autoloading',
'zendframework/zend-config-aggregator' => 'laminas/laminas-config-aggregator',
'zendframework/zend-config' => 'laminas/laminas-config',
'zendframework/zend-console' => 'laminas/laminas-console',
'zendframework/zend-container-config-test' => 'laminas/laminas-container-config-test',
'zendframework/zend-crypt' => 'laminas/laminas-crypt',
'zendframework/zend-db' => 'laminas/laminas-db',
'zendframework/zend-developer-tools' => 'laminas/laminas-developer-tools',
'zendframework/zend-diactoros' => 'laminas/laminas-diactoros',
'zendframework/zenddiagnostics' => 'laminas/laminas-diagnostics',
'zendframework/zend-di' => 'laminas/laminas-di',
'zendframework/zend-dom' => 'laminas/laminas-dom',
'zendframework/zend-escaper' => 'laminas/laminas-escaper',
'zendframework/zend-eventmanager' => 'laminas/laminas-eventmanager',
'zendframework/zend-feed' => 'laminas/laminas-feed',
'zendframework/zend-file' => 'laminas/laminas-file',
'zendframework/zend-filter' => 'laminas/laminas-filter',
'zendframework/zend-form' => 'laminas/laminas-form',
'zendframework/zend-httphandlerrunner' => 'laminas/laminas-httphandlerrunner',
'zendframework/zend-http' => 'laminas/laminas-http',
'zendframework/zend-hydrator' => 'laminas/laminas-hydrator',
'zendframework/zend-i18n' => 'laminas/laminas-i18n',
'zendframework/zend-i18n-resources' => 'laminas/laminas-i18n-resources',
'zendframework/zend-inputfilter' => 'laminas/laminas-inputfilter',
'zendframework/zend-json' => 'laminas/laminas-json',
'zendframework/zend-json-server' => 'laminas/laminas-json-server',
'zendframework/zend-ldap' => 'laminas/laminas-ldap',
'zendframework/zend-loader' => 'laminas/laminas-loader',
'zendframework/zend-log' => 'laminas/laminas-log',
'zendframework/zend-mail' => 'laminas/laminas-mail',
'zendframework/zend-math' => 'laminas/laminas-math',
'zendframework/zend-memory' => 'laminas/laminas-memory',
'zendframework/zend-mime' => 'laminas/laminas-mime',
'zendframework/zend-modulemanager' => 'laminas/laminas-modulemanager',
'zendframework/zend-mvc' => 'laminas/laminas-mvc',
'zendframework/zend-navigation' => 'laminas/laminas-navigation',
'zendframework/zend-oauth' => 'laminas/laminas-oauth',
'zendframework/zend-paginator' => 'laminas/laminas-paginator',
'zendframework/zend-permissions-acl' => 'laminas/laminas-permissions-acl',
'zendframework/zend-permissions-rbac' => 'laminas/laminas-permissions-rbac',
'zendframework/zend-pimple-config' => 'laminas/laminas-pimple-config',
'zendframework/zend-progressbar' => 'laminas/laminas-progressbar',
'zendframework/zend-psr7bridge' => 'laminas/laminas-psr7bridge',
'zendframework/zend-recaptcha' => 'laminas/laminas-recaptcha',
'zendframework/zend-router' => 'laminas/laminas-router',
'zendframework/zend-serializer' => 'laminas/laminas-serializer',
'zendframework/zend-server' => 'laminas/laminas-server',
'zendframework/zend-servicemanager' => 'laminas/laminas-servicemanager',
'zendframework/zendservice-recaptcha' => 'laminas/laminas-recaptcha',
'zendframework/zendservice-twitter' => 'laminas/laminas-twitter',
'zendframework/zend-session' => 'laminas/laminas-session',
'zendframework/zend-skeleton-installer' => 'laminas/laminas-skeleton-installer',
'zendframework/zend-soap' => 'laminas/laminas-soap',
'zendframework/zend-stdlib' => 'laminas/laminas-stdlib',
'zendframework/zend-stratigility' => 'laminas/laminas-stratigility',
'zendframework/zend-tag' => 'laminas/laminas-tag',
'zendframework/zend-test' => 'laminas/laminas-test',
'zendframework/zend-text' => 'laminas/laminas-text',
'zendframework/zend-uri' => 'laminas/laminas-uri',
'zendframework/zend-validator' => 'laminas/laminas-validator',
'zendframework/zend-view' => 'laminas/laminas-view',
'zendframework/zend-xml2json' => 'laminas/laminas-xml2json',
'zendframework/zend-xml' => 'laminas/laminas-xml',
'zendframework/zend-xmlrpc' => 'laminas/laminas-xmlrpc',
// Expressive packages
'zendframework/zend-expressive' => 'mezzio/mezzio',
'zendframework/zend-expressive-zendrouter' => 'mezzio/mezzio-laminasrouter',
'zendframework/zend-problem-details' => 'mezzio/mezzio-problem-details',
'zendframework/zend-expressive-zendviewrenderer' => 'mezzio/mezzio-laminasviewrenderer',
// Apigility packages
'zfcampus/apigility-documentation' => 'laminas-api-tools/documentation',
'zfcampus/statuslib-example' => 'laminas-api-tools/statuslib-example',
'zfcampus/zf-apigility' => 'laminas-api-tools/api-tools',
'zfcampus/zf-api-problem' => 'laminas-api-tools/api-tools-api-problem',
'zfcampus/zf-asset-manager' => 'laminas-api-tools/api-tools-asset-manager',
'zfcampus/zf-configuration' => 'laminas-api-tools/api-tools-configuration',
'zfcampus/zf-content-negotiation' => 'laminas-api-tools/api-tools-content-negotiation',
'zfcampus/zf-content-validation' => 'laminas-api-tools/api-tools-content-validation',
'zfcampus/zf-development-mode' => 'laminas/laminas-development-mode',
'zfcampus/zf-doctrine-querybuilder' => 'laminas-api-tools/api-tools-doctrine-querybuilder',
'zfcampus/zf-hal' => 'laminas-api-tools/api-tools-hal',
'zfcampus/zf-http-cache' => 'laminas-api-tools/api-tools-http-cache',
'zfcampus/zf-mvc-auth' => 'laminas-api-tools/api-tools-mvc-auth',
'zfcampus/zf-oauth2' => 'laminas-api-tools/api-tools-oauth2',
'zfcampus/zf-rest' => 'laminas-api-tools/api-tools-rest',
'zfcampus/zf-rpc' => 'laminas-api-tools/api-tools-rpc',
'zfcampus/zf-versioning' => 'laminas-api-tools/api-tools-versioning',
// CONFIG KEYS, SCRIPT NAMES, ETC
// ZF components
'::fromZend' => '::fromLaminas', // psr7bridge
'::toZend' => '::toLaminas', // psr7bridge
'use_zend_loader' => 'use_laminas_loader', // zend-modulemanager
'zend-config' => 'laminas-config',
'zend-developer-tools/' => 'laminas-developer-tools/',
'zend-tag-cloud' => 'laminas-tag-cloud',
'zenddevelopertools' => 'laminas-developer-tools',
'zendbarcode' => 'laminasbarcode',
'ZendBarcode' => 'LaminasBarcode',
'zendcache' => 'laminascache',
'ZendCache' => 'LaminasCache',
'zendconfig' => 'laminasconfig',
'ZendConfig' => 'LaminasConfig',
'zendfeed' => 'laminasfeed',
'ZendFeed' => 'LaminasFeed',
'zendfilter' => 'laminasfilter',
'ZendFilter' => 'LaminasFilter',
'zendform' => 'laminasform',
'ZendForm' => 'LaminasForm',
'zendi18n' => 'laminasi18n',
'ZendI18n' => 'LaminasI18n',
'zendinputfilter' => 'laminasinputfilter',
'ZendInputFilter' => 'LaminasInputFilter',
'zendlog' => 'laminaslog',
'ZendLog' => 'LaminasLog',
'zendmail' => 'laminasmail',
'ZendMail' => 'LaminasMail',
'zendmvc' => 'laminasmvc',
'ZendMvc' => 'LaminasMvc',
'zendpaginator' => 'laminaspaginator',
'ZendPaginator' => 'LaminasPaginator',
'zendserializer' => 'laminasserializer',
'ZendSerializer' => 'LaminasSerializer',
'zendtag' => 'laminastag',
'ZendTag' => 'LaminasTag',
'zendtext' => 'laminastext',
'ZendText' => 'LaminasText',
'zendvalidator' => 'laminasvalidator',
'ZendValidator' => 'LaminasValidator',
'zendview' => 'laminasview',
'ZendView' => 'LaminasView',
'zend-framework.flf' => 'laminas-project.flf',
// Expressive-related
"'zend-expressive'" => "'mezzio'",
'"zend-expressive"' => '"mezzio"',
'zend-expressive.' => 'mezzio.',
'zend-expressive-authorization' => 'mezzio-authorization',
'zend-expressive-hal' => 'mezzio-hal',
'zend-expressive-session' => 'mezzio-session',
'zend-expressive-swoole' => 'mezzio-swoole',
'zend-expressive-tooling' => 'mezzio-tooling',
// Apigility-related
"'zf-apigility'" => "'api-tools'",
'"zf-apigility"' => '"api-tools"',
'zf-apigility/' => 'api-tools/',
'zf-apigility-admin' => 'api-tools-admin',
'zf-content-negotiation' => 'api-tools-content-negotiation',
'zf-hal' => 'api-tools-hal',
'zf-rest' => 'api-tools-rest',
'zf-rpc' => 'api-tools-rpc',
'zf-content-validation' => 'api-tools-content-validation',
'zf-apigility-ui' => 'api-tools-ui',
'zf-apigility-documentation-blueprint' => 'api-tools-documentation-blueprint',
'zf-apigility-documentation-swagger' => 'api-tools-documentation-swagger',
'zf-apigility-welcome' => 'api-tools-welcome',
'zf-api-problem' => 'api-tools-api-problem',
'zf-configuration' => 'api-tools-configuration',
'zf-http-cache' => 'api-tools-http-cache',
'zf-mvc-auth' => 'api-tools-mvc-auth',
'zf-oauth2' => 'api-tools-oauth2',
'zf-versioning' => 'api-tools-versioning',
'ZfApigilityDoctrineQueryProviderManager' => 'LaminasApiToolsDoctrineQueryProviderManager',
'ZfApigilityDoctrineQueryCreateFilterManager' => 'LaminasApiToolsDoctrineQueryCreateFilterManager',
'zf-apigility-doctrine' => 'api-tools-doctrine',
'zf-development-mode' => 'laminas-development-mode',
'zf-doctrine-querybuilder' => 'api-tools-doctrine-querybuilder',
// 3rd party Apigility packages
'api-skeletons/zf-' => 'api-skeletons/zf-', // api-skeletons packages
'zf-oauth2-' => 'zf-oauth2-', // api-skeletons OAuth2-related packages
'ZF\\OAuth2\\Client' => 'ZF\\OAuth2\\Client', // api-skeletons/zf-oauth2-client
'ZF\\OAuth2\\Doctrine' => 'ZF\\OAuth2\\Doctrine', // api-skeletons/zf-oauth2-doctrine
];

View File

@@ -0,0 +1,345 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="4.9.3@4c262932602b9bbab5020863d1eb22d49de0dbf4">
<file src="config/replacements.php">
<DuplicateArrayKey occurrences="3">
<code>'ZendAcl' =&gt; 'LaminasAcl'</code>
<code>'ZendRbac' =&gt; 'LaminasRbac'</code>
<code>'ZendRouter' =&gt; 'LaminasRouter'</code>
</DuplicateArrayKey>
</file>
<file src="src/Autoloader.php">
<MissingClosureParamType occurrences="2">
<code>$class</code>
<code>$class</code>
</MissingClosureParamType>
<MissingReturnType occurrences="1">
<code>load</code>
</MissingReturnType>
<MixedArgument occurrences="7">
<code>$class</code>
<code>$class</code>
<code>$class</code>
<code>$class</code>
<code>$class</code>
<code>$class</code>
<code>$class</code>
</MixedArgument>
<MixedArgumentTypeCoercion occurrences="2"/>
<MixedArrayOffset occurrences="1">
<code>$loaded[$class]</code>
</MixedArrayOffset>
<MixedOperand occurrences="2">
<code>$namespaces[$check]</code>
<code>$namespaces[$check]</code>
</MixedOperand>
</file>
<file src="src/ConfigPostProcessor.php">
<InvalidArgument occurrences="1">
<code>$keys</code>
</InvalidArgument>
<MissingClosureParamType occurrences="4">
<code>$value</code>
<code>$value</code>
<code>$value</code>
<code>$value</code>
</MissingClosureParamType>
<MissingClosureReturnType occurrences="4">
<code>function ($value) {</code>
<code>function ($value) {</code>
<code>function ($value, array $keys) {</code>
<code>function ($value, array $keys) {</code>
</MissingClosureReturnType>
<MissingReturnType occurrences="3">
<code>replaceDependencyConfiguration</code>
<code>replaceDependencyFactories</code>
<code>replaceDependencyServices</code>
</MissingReturnType>
<MixedArgument occurrences="5">
<code>$config</code>
<code>$newKey</code>
<code>$newKey</code>
<code>$newKey</code>
<code>$target</code>
</MixedArgument>
<MixedArgumentTypeCoercion occurrences="1">
<code>[$key]</code>
</MixedArgumentTypeCoercion>
<MixedArrayAssignment occurrences="4">
<code>$config[$key]</code>
<code>$config['aliases'][$alias]</code>
<code>$config['aliases'][$service]</code>
<code>$config['aliases'][$service]</code>
</MixedArrayAssignment>
<MixedArrayOffset occurrences="7">
<code>$aliases[$name]</code>
<code>$config[$key]</code>
<code>$keys[$key]</code>
<code>$rewritten[$newKey]</code>
<code>$rewritten[$newKey]</code>
<code>$rewritten[$newKey]</code>
<code>$this-&gt;exactReplacements[$value]</code>
</MixedArrayOffset>
<MixedArrayTypeCoercion occurrences="1">
<code>$aliases[$name]</code>
</MixedArrayTypeCoercion>
<MixedAssignment occurrences="26">
<code>$a[$key]</code>
<code>$a[$key]</code>
<code>$a[]</code>
<code>$config</code>
<code>$config</code>
<code>$config[$key]</code>
<code>$config['factories'][$replacedService]</code>
<code>$config['services'][$replacedService]</code>
<code>$data</code>
<code>$factory</code>
<code>$factory</code>
<code>$key</code>
<code>$key</code>
<code>$name</code>
<code>$newKey</code>
<code>$newValue</code>
<code>$notIn[]</code>
<code>$result</code>
<code>$rewritten[$key]</code>
<code>$rewritten[$newKey]</code>
<code>$rewritten[$newKey][]</code>
<code>$serviceInstance</code>
<code>$serviceInstance</code>
<code>$target</code>
<code>$value</code>
<code>$value</code>
</MixedAssignment>
</file>
<file src="src/Module.php">
<MissingReturnType occurrences="2">
<code>init</code>
<code>onMergeConfig</code>
</MissingReturnType>
<UndefinedDocblockClass occurrences="2">
<code>ModuleEvent</code>
<code>ModuleManager</code>
</UndefinedDocblockClass>
</file>
<file src="src/Replacements.php">
<MixedArgument occurrences="2">
<code>$replacement</code>
<code>$replacement</code>
</MixedArgument>
<MixedArgumentTypeCoercion occurrences="3">
<code>$original</code>
<code>$original</code>
<code>$original</code>
</MixedArgumentTypeCoercion>
<MixedAssignment occurrences="1">
<code>$replacement</code>
</MixedAssignment>
<MixedPropertyTypeCoercion occurrences="3">
<code>$this-&gt;replacements</code>
<code>$this-&gt;replacements</code>
</MixedPropertyTypeCoercion>
</file>
<file src="test/AutoloaderTest.php">
<InvalidStringClass occurrences="1">
<code>new $legacy()</code>
</InvalidStringClass>
<MissingReturnType occurrences="3">
<code>testLegacyClassIsAliasToLaminas</code>
<code>testReverseAliasCreated</code>
<code>testTypeHint</code>
</MissingReturnType>
<PossiblyInvalidArgument occurrences="1">
<code>testLegacyClassIsAliasToLaminas</code>
</PossiblyInvalidArgument>
</file>
<file src="test/ConfigPostProcessorTest.php">
<MissingReturnType occurrences="4">
<code>invalidServiceManagerConfiguration</code>
<code>testRewritesNestedKeys</code>
<code>testServiceManagerServiceInstancesCanBeHandled</code>
<code>testWillSkipInvalidConfigurations</code>
</MissingReturnType>
<MixedArgument occurrences="1">
<code>$config</code>
</MixedArgument>
<MixedAssignment occurrences="2">
<code>$config</code>
<code>$expected</code>
</MixedAssignment>
<MixedInferredReturnType occurrences="1">
<code>iterable</code>
</MixedInferredReturnType>
<UnresolvableInclude occurrences="2">
<code>require $configLocation</code>
<code>require $expectedResultLocation</code>
</UnresolvableInclude>
</file>
<file src="test/ModuleTest.php">
<InvalidArgument occurrences="2">
<code>$event</code>
<code>$moduleManager</code>
</InvalidArgument>
<MissingReturnType occurrences="2">
<code>testInitRegistersListenerWithEventManager</code>
<code>testOnMergeConfigProcessesAndReplacesConfigurationPulledFromListener</code>
</MissingReturnType>
<MixedArgument occurrences="1">
<code>$config</code>
</MixedArgument>
<MixedAssignment occurrences="2">
<code>$config</code>
<code>$expected</code>
</MixedAssignment>
<MixedInferredReturnType occurrences="1">
<code>iterable</code>
</MixedInferredReturnType>
<UnresolvableInclude occurrences="2">
<code>require $configFile</code>
<code>require $expectationsFile</code>
</UnresolvableInclude>
</file>
<file src="test/ReplacementsTest.php">
<MissingReturnType occurrences="1">
<code>testEdgeCases</code>
</MissingReturnType>
<MixedInferredReturnType occurrences="1">
<code>iterable</code>
</MixedInferredReturnType>
</file>
<file src="test/TestAsset/ConfigPostProcessor/AbstractFactories.php">
<UndefinedClass occurrences="4">
<code>ConfigAbstractFactory</code>
<code>ConfigAbstractFactory</code>
<code>InvokableFactory</code>
<code>InvokableFactory</code>
</UndefinedClass>
</file>
<file src="test/TestAsset/ConfigPostProcessor/CyclicalAliasing.php">
<DuplicateArrayKey occurrences="1">
<code>'Zend\Db\Adapter\Adapter' =&gt; 'ZF\Apigility\DbConnectedAdapter'</code>
</DuplicateArrayKey>
</file>
<file src="test/TestAsset/ConfigPostProcessor/ExpressiveSlimRouterConfig.php">
<UndefinedClass occurrences="2">
<code>Factory\SlimRouterFactory</code>
<code>RouterInterface</code>
</UndefinedClass>
</file>
<file src="test/TestAsset/ConfigPostProcessor/LazyServices.php">
<UndefinedClass occurrences="1">
<code>LazyServiceFactory</code>
</UndefinedClass>
</file>
<file src="test/TestAsset/ConfigPostProcessor/MwopNetAppConfig.php">
<UndefinedClass occurrences="42">
<code>CacheItemPoolInterface</code>
<code>ConfigFactory</code>
<code>ConfigFactory</code>
<code>ConfigFactory</code>
<code>ConfigFactory</code>
<code>ConfigFactory</code>
<code>ConfigFactory</code>
<code>Csp</code>
<code>DisplayPostHandler</code>
<code>Engine</code>
<code>EventDispatcherInterface</code>
<code>Factory\CachePoolFactory</code>
<code>Factory\EventDispatcherFactory</code>
<code>Factory\MailTransport</code>
<code>Factory\PlatesFunctionsDelegator</code>
<code>FeedReaderHttpClientInterface</code>
<code>Feed\HttpPlugClientFactory</code>
<code>Handler\ComicsPageHandler</code>
<code>Handler\ComicsPageHandlerFactory</code>
<code>Handler\HomePageHandler</code>
<code>Handler\HomePageHandlerFactory</code>
<code>Handler\PageHandlerFactory</code>
<code>Handler\PageHandlerFactory</code>
<code>Handler\ResumePageHandler</code>
<code>Handler\ResumePageHandler</code>
<code>Middleware\ContentSecurityPolicyMiddlewareFactory</code>
<code>Middleware\DisplayBlogPostHandlerDelegator</code>
<code>Middleware\RedirectAmpPagesMiddleware</code>
<code>Middleware\RedirectAmpPagesMiddlewareFactory</code>
<code>Middleware\RedirectsMiddleware</code>
<code>Middleware\RedirectsMiddleware</code>
<code>Middleware\XClacksOverheadMiddleware</code>
<code>Middleware\XClacksOverheadMiddleware</code>
<code>Middleware\XPoweredByMiddleware</code>
<code>Middleware\XPoweredByMiddleware</code>
<code>RequestFactory</code>
<code>RequestFactoryInterface</code>
<code>ResponseFactory</code>
<code>ResponseFactoryInterface</code>
<code>SessionCachePool</code>
<code>SessionCachePool</code>
<code>SessionCachePoolFactory</code>
</UndefinedClass>
</file>
<file src="test/TestAsset/Replacements/TestClass.php">
<UndefinedClass occurrences="1">
<code>\Zend\Expressive\Router</code>
</UndefinedClass>
</file>
<file src="test/TestAsset/Replacements/ZFOAuth2Client.php">
<UndefinedClass occurrences="1">
<code>OAuth2Client</code>
</UndefinedClass>
</file>
<file src="test/TestAsset/classes/ConfigListener.php">
<MissingReturnType occurrences="1">
<code>setMergedConfig</code>
</MissingReturnType>
</file>
<file src="test/TestAsset/classes/EventManager.php">
<InvalidArgument occurrences="1">
<code>$listener</code>
</InvalidArgument>
<MissingPropertyType occurrences="1">
<code>$listeners</code>
</MissingPropertyType>
<MissingReturnType occurrences="1">
<code>attach</code>
</MissingReturnType>
<MixedArgument occurrences="1">
<code>$this-&gt;listeners[$eventName]</code>
</MixedArgument>
<MixedArrayAccess occurrences="1">
<code>$this-&gt;listeners[$eventName]</code>
</MixedArrayAccess>
<MixedArrayAssignment occurrences="1">
<code>$this-&gt;listeners[$eventName]</code>
</MixedArrayAssignment>
<MixedInferredReturnType occurrences="1">
<code>array</code>
</MixedInferredReturnType>
<MixedReturnStatement occurrences="1">
<code>$this-&gt;listeners</code>
</MixedReturnStatement>
</file>
<file src="test/TestAsset/classes/ModuleEvent.php">
<MissingPropertyType occurrences="1">
<code>$listener</code>
</MissingPropertyType>
<MixedInferredReturnType occurrences="1">
<code>ConfigListener</code>
</MixedInferredReturnType>
<MixedReturnStatement occurrences="1">
<code>$this-&gt;listener</code>
</MixedReturnStatement>
</file>
<file src="test/TestAsset/classes/ModuleManager.php">
<MixedInferredReturnType occurrences="1">
<code>EventManager</code>
</MixedInferredReturnType>
<MixedReturnStatement occurrences="1">
<code>$this-&gt;eventManager</code>
</MixedReturnStatement>
<UndefinedThisPropertyAssignment occurrences="1">
<code>$this-&gt;eventManager</code>
</UndefinedThisPropertyAssignment>
<UndefinedThisPropertyFetch occurrences="1">
<code>$this-&gt;eventManager</code>
</UndefinedThisPropertyFetch>
</file>
</files>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0"?>
<psalm
totallyTyped="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorBaseline="psalm-baseline.xml"
>
<projectFiles>
<directory name="src"/>
<directory name="test"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<InternalMethod>
<errorLevel type="suppress">
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::method"/>
</errorLevel>
<errorLevel type="suppress">
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::willReturn"/>
</errorLevel>
<errorLevel type="suppress">
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::with"/>
</errorLevel>
</InternalMethod>
</issueHandlers>
<plugins>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
</plugins>
</psalm>

View File

@@ -0,0 +1,181 @@
<?php
namespace Laminas\ZendFrameworkBridge;
use ArrayObject;
use Composer\Autoload\ClassLoader;
use RuntimeException;
use function array_values;
use function class_alias;
use function class_exists;
use function explode;
use function file_exists;
use function getenv;
use function interface_exists;
use function spl_autoload_register;
use function strlen;
use function strtr;
use function substr;
use function trait_exists;
/**
* Alias legacy Zend Framework project classes/interfaces/traits to Laminas equivalents.
*/
class Autoloader
{
private const UPSTREAM_COMPOSER_VENDOR_DIRECTORY = __DIR__ . '/../../..';
private const LOCAL_COMPOSER_VENDOR_DIRECTORY = __DIR__ . '/../vendor';
/**
* Attach autoloaders for managing legacy ZF artifacts.
*
* We attach two autoloaders:
*
* - The first is _prepended_ to handle new classes and add aliases for
* legacy classes. PHP expects any interfaces implemented, classes
* extended, or traits used when declaring class_alias() to exist and/or
* be autoloadable already at the time of declaration. If not, it will
* raise a fatal error. This autoloader helps mitigate errors in such
* situations.
*
* - The second is _appended_ in order to create aliases for legacy
* classes.
*/
public static function load()
{
$loaded = new ArrayObject([]);
$classLoader = self::getClassLoader();
if ($classLoader === null) {
return;
}
spl_autoload_register(self::createPrependAutoloader(
RewriteRules::namespaceReverse(),
$classLoader,
$loaded
), true, true);
spl_autoload_register(self::createAppendAutoloader(
RewriteRules::namespaceRewrite(),
$loaded
));
}
private static function getClassLoader(): ?ClassLoader
{
$composerVendorDirectory = getenv('COMPOSER_VENDOR_DIR');
if (is_string($composerVendorDirectory)) {
return self::getClassLoaderFromVendorDirectory($composerVendorDirectory);
}
return self::getClassLoaderFromVendorDirectory(self::UPSTREAM_COMPOSER_VENDOR_DIRECTORY)
?? self::getClassLoaderFromVendorDirectory(self::LOCAL_COMPOSER_VENDOR_DIRECTORY);
}
/**
* @return callable
*/
private static function createPrependAutoloader(array $namespaces, ClassLoader $classLoader, ArrayObject $loaded)
{
/**
* @param string $class Class name to autoload
* @return void
*/
return static function ($class) use ($namespaces, $classLoader, $loaded) {
if (isset($loaded[$class])) {
return;
}
$segments = explode('\\', $class);
$i = 0;
$check = '';
while (isset($segments[$i + 1], $namespaces[$check . $segments[$i] . '\\'])) {
$check .= $segments[$i] . '\\';
++$i;
}
if ($check === '') {
return;
}
if ($classLoader->loadClass($class)) {
$legacy = $namespaces[$check]
. strtr(substr($class, strlen($check)), [
'ApiTools' => 'Apigility',
'Mezzio' => 'Expressive',
'Laminas' => 'Zend',
]);
class_alias($class, $legacy);
}
};
}
/**
* @return callable
*/
private static function createAppendAutoloader(array $namespaces, ArrayObject $loaded)
{
/**
* @param string $class Class name to autoload
* @return void
*/
return static function ($class) use ($namespaces, $loaded) {
$segments = explode('\\', $class);
if ($segments[0] === 'ZendService' && isset($segments[1])) {
$segments[0] .= '\\' . $segments[1];
unset($segments[1]);
$segments = array_values($segments);
}
$i = 0;
$check = '';
// We are checking segments of the namespace to match quicker
while (isset($segments[$i + 1], $namespaces[$check . $segments[$i] . '\\'])) {
$check .= $segments[$i] . '\\';
++$i;
}
if ($check === '') {
return;
}
$alias = $namespaces[$check]
. strtr(substr($class, strlen($check)), [
'Apigility' => 'ApiTools',
'Expressive' => 'Mezzio',
'Zend' => 'Laminas',
'AbstractZendServer' => 'AbstractZendServer',
'ZendServerDisk' => 'ZendServerDisk',
'ZendServerShm' => 'ZendServerShm',
'ZendMonitor' => 'ZendMonitor',
]);
$loaded[$alias] = true;
if (class_exists($alias) || interface_exists($alias) || trait_exists($alias)) {
class_alias($alias, $class);
}
};
}
private static function getClassLoaderFromVendorDirectory(string $composerVendorDirectory): ?ClassLoader
{
$filename = rtrim($composerVendorDirectory, '/') . '/autoload.php';
if (!file_exists($filename)) {
return null;
}
/** @psalm-suppress MixedAssignment */
$loader = include $filename;
if (!$loader instanceof ClassLoader) {
return null;
}
return $loader;
}
}

View File

@@ -0,0 +1,426 @@
<?php
namespace Laminas\ZendFrameworkBridge;
use function array_intersect_key;
use function array_key_exists;
use function array_pop;
use function in_array;
use function is_array;
use function is_callable;
use function is_int;
use function is_string;
class ConfigPostProcessor
{
/** @internal */
const SERVICE_MANAGER_KEYS_OF_INTEREST = [
'aliases' => true,
'factories' => true,
'invokables' => true,
'services' => true,
];
/** @var array String keys => string values */
private $exactReplacements = [
'zend-expressive' => 'mezzio',
'zf-apigility' => 'api-tools',
];
/** @var Replacements */
private $replacements;
/** @var callable[] */
private $rulesets;
public function __construct()
{
$this->replacements = new Replacements();
/* Define the rulesets for replacements.
*
* Each ruleset has the following signature:
*
* @param mixed $value
* @param string[] $keys Full nested key hierarchy leading to the value
* @return null|callable
*
* If no match is made, a null is returned, allowing it to fallback to
* the next ruleset in the list. If a match is made, a callback is returned,
* and that will be used to perform the replacement on the value.
*
* The callback should have the following signature:
*
* @param mixed $value
* @param string[] $keys
* @return mixed The transformed value
*/
$this->rulesets = [
// Exact values
function ($value) {
return is_string($value) && isset($this->exactReplacements[$value])
? [$this, 'replaceExactValue']
: null;
},
// Router (MVC applications)
// We do not want to rewrite these.
function ($value, array $keys) {
$key = array_pop($keys);
// Only worried about a top-level "router" key.
return $key === 'router' && $keys === [] && is_array($value)
? [$this, 'noopReplacement']
: null;
},
// service- and pluginmanager handling
function ($value) {
return is_array($value) && array_intersect_key(self::SERVICE_MANAGER_KEYS_OF_INTEREST, $value) !== []
? [$this, 'replaceDependencyConfiguration']
: null;
},
// Array values
function ($value, array $keys) {
return $keys !== [] && is_array($value)
? [$this, '__invoke']
: null;
},
];
}
/**
* @param string[] $keys Hierarchy of keys, for determining location in
* nested configuration.
* @return array
*/
public function __invoke(array $config, array $keys = [])
{
$rewritten = [];
foreach ($config as $key => $value) {
// Determine new key from replacements
$newKey = is_string($key) ? $this->replace($key, $keys) : $key;
// Keep original values with original key, if the key has changed, but only at the top-level.
if (empty($keys) && $newKey !== $key) {
$rewritten[$key] = $value;
}
// Perform value replacements, if any
$newValue = $this->replace($value, $keys, $newKey);
// Key does not already exist and/or is not an array value
if (! array_key_exists($newKey, $rewritten) || ! is_array($rewritten[$newKey])) {
// Do not overwrite existing values with null values
$rewritten[$newKey] = array_key_exists($newKey, $rewritten) && null === $newValue
? $rewritten[$newKey]
: $newValue;
continue;
}
// New value is null; nothing to do.
if (null === $newValue) {
continue;
}
// Key already exists as an array value, but $value is not an array
if (! is_array($newValue)) {
$rewritten[$newKey][] = $newValue;
continue;
}
// Key already exists as an array value, and $value is also an array
$rewritten[$newKey] = static::merge($rewritten[$newKey], $newValue);
}
return $rewritten;
}
/**
* Perform substitutions as needed on an individual value.
*
* The $key is provided to allow fine-grained selection of rewrite rules.
*
* @param mixed $value
* @param string[] $keys Key hierarchy
* @param null|int|string $key
* @return mixed
*/
private function replace($value, array $keys, $key = null)
{
// Add new key to the list of keys.
// We do not need to remove it later, as we are working on a copy of the array.
$keys[] = $key;
// Identify rewrite strategy and perform replacements
$rewriteRule = $this->replacementRuleMatch($value, $keys);
return $rewriteRule($value, $keys);
}
/**
* Merge two arrays together.
*
* If an integer key exists in both arrays, the value from the second array
* will be appended to the first array. If both values are arrays, they are
* merged together, else the value of the second array overwrites the one
* of the first array.
*
* Based on zend-stdlib Zend\Stdlib\ArrayUtils::merge
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
*
* @return array
*/
public static function merge(array $a, array $b)
{
foreach ($b as $key => $value) {
if (! isset($a[$key]) && ! array_key_exists($key, $a)) {
$a[$key] = $value;
continue;
}
if (null === $value && array_key_exists($key, $a)) {
// Leave as-is if value from $b is null
continue;
}
if (is_int($key)) {
$a[] = $value;
continue;
}
if (is_array($value) && is_array($a[$key])) {
$a[$key] = static::merge($a[$key], $value);
continue;
}
$a[$key] = $value;
}
return $a;
}
/**
* @param mixed $value
* @param null|int|string $key
* @return callable Callable to invoke with value
*/
private function replacementRuleMatch($value, $key = null)
{
foreach ($this->rulesets as $ruleset) {
$result = $ruleset($value, $key);
if (is_callable($result)) {
return $result;
}
}
return [$this, 'fallbackReplacement'];
}
/**
* Replace a value using the translation table, if the value is a string.
*
* @param mixed $value
* @return mixed
*/
private function fallbackReplacement($value)
{
return is_string($value)
? $this->replacements->replace($value)
: $value;
}
/**
* Replace a value matched exactly.
*
* @param mixed $value
* @return mixed
*/
private function replaceExactValue($value)
{
return $this->exactReplacements[$value];
}
private function replaceDependencyConfiguration(array $config)
{
$aliases = isset($config['aliases']) && is_array($config['aliases'])
? $this->replaceDependencyAliases($config['aliases'])
: [];
if ($aliases) {
$config['aliases'] = $aliases;
}
$config = $this->replaceDependencyInvokables($config);
$config = $this->replaceDependencyFactories($config);
$config = $this->replaceDependencyServices($config);
$keys = self::SERVICE_MANAGER_KEYS_OF_INTEREST;
foreach ($config as $key => $data) {
if (isset($keys[$key])) {
continue;
}
$config[$key] = is_array($data) ? $this->__invoke($data, [$key]) : $data;
}
return $config;
}
/**
* Rewrite dependency aliases array
*
* In this case, we want to keep the alias as-is, but rewrite the target.
*
* We need also provide an additional alias if the alias key is a legacy class.
*
* @return array
*/
private function replaceDependencyAliases(array $aliases)
{
foreach ($aliases as $alias => $target) {
if (! is_string($alias) || ! is_string($target)) {
continue;
}
$newTarget = $this->replacements->replace($target);
$newAlias = $this->replacements->replace($alias);
$notIn = [$newTarget];
$name = $newTarget;
while (isset($aliases[$name])) {
$notIn[] = $aliases[$name];
$name = $aliases[$name];
}
if ($newAlias === $alias && ! in_array($alias, $notIn, true)) {
$aliases[$alias] = $newTarget;
continue;
}
if (isset($aliases[$newAlias])) {
continue;
}
if (! in_array($newAlias, $notIn, true)) {
$aliases[$alias] = $newAlias;
$aliases[$newAlias] = $newTarget;
}
}
return $aliases;
}
/**
* Rewrite dependency invokables array
*
* In this case, we want to keep the alias as-is, but rewrite the target.
*
* We need also provide an additional alias if invokable is defined with
* an alias which is a legacy class.
*
* @return array
*/
private function replaceDependencyInvokables(array $config)
{
if (empty($config['invokables']) || ! is_array($config['invokables'])) {
return $config;
}
foreach ($config['invokables'] as $alias => $target) {
if (! is_string($alias)) {
continue;
}
$newTarget = $this->replacements->replace($target);
$newAlias = $this->replacements->replace($alias);
if ($alias === $target || isset($config['aliases'][$newAlias])) {
$config['invokables'][$alias] = $newTarget;
continue;
}
$config['invokables'][$newAlias] = $newTarget;
if ($newAlias === $alias) {
continue;
}
$config['aliases'][$alias] = $newAlias;
unset($config['invokables'][$alias]);
}
return $config;
}
/**
* @param mixed $value
* @return mixed Returns $value verbatim.
*/
private function noopReplacement($value)
{
return $value;
}
private function replaceDependencyFactories(array $config)
{
if (empty($config['factories']) || ! is_array($config['factories'])) {
return $config;
}
foreach ($config['factories'] as $service => $factory) {
if (! is_string($service)) {
continue;
}
$replacedService = $this->replacements->replace($service);
$factory = is_string($factory) ? $this->replacements->replace($factory) : $factory;
$config['factories'][$replacedService] = $factory;
if ($replacedService === $service) {
continue;
}
unset($config['factories'][$service]);
if (isset($config['aliases'][$service])) {
continue;
}
$config['aliases'][$service] = $replacedService;
}
return $config;
}
private function replaceDependencyServices(array $config)
{
if (empty($config['services']) || ! is_array($config['services'])) {
return $config;
}
foreach ($config['services'] as $service => $serviceInstance) {
if (! is_string($service)) {
continue;
}
$replacedService = $this->replacements->replace($service);
$serviceInstance = is_array($serviceInstance) ? $this->__invoke($serviceInstance) : $serviceInstance;
$config['services'][$replacedService] = $serviceInstance;
if ($service === $replacedService) {
continue;
}
unset($config['services'][$service]);
if (isset($config['aliases'][$service])) {
continue;
}
$config['aliases'][$service] = $replacedService;
}
return $config;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Laminas\ZendFrameworkBridge;
use Laminas\ModuleManager\Listener\ConfigMergerInterface;
use Laminas\ModuleManager\ModuleEvent;
use Laminas\ModuleManager\ModuleManager;
class Module
{
/**
* Initialize the module.
*
* Type-hinting deliberately omitted to allow unit testing
* without dependencies on packages that do not exist yet.
*
* @param ModuleManager $moduleManager
*/
public function init($moduleManager)
{
$moduleManager
->getEventManager()
->attach('mergeConfig', [$this, 'onMergeConfig']);
}
/**
* Perform substitutions in the merged configuration.
*
* Rewrites keys and values matching known ZF classes, namespaces, and
* configuration keys to their Laminas equivalents.
*
* Type-hinting deliberately omitted to allow unit testing
* without dependencies on packages that do not exist yet.
*
* @param ModuleEvent $event
*/
public function onMergeConfig($event)
{
/** @var ConfigMergerInterface */
$configMerger = $event->getConfigListener();
$processor = new ConfigPostProcessor();
$configMerger->setMergedConfig(
$processor(
$configMerger->getMergedConfig($returnAsObject = false)
)
);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Laminas\ZendFrameworkBridge;
use function array_merge;
use function str_replace;
use function strpos;
use function strtr;
class Replacements
{
/** @var string[] */
private $replacements;
public function __construct(array $additionalReplacements = [])
{
$this->replacements = array_merge(
require __DIR__ . '/../config/replacements.php',
$additionalReplacements
);
// Provide multiple variants of strings containing namespace separators
foreach ($this->replacements as $original => $replacement) {
if (false === strpos($original, '\\')) {
continue;
}
$this->replacements[str_replace('\\', '\\\\', $original)] = str_replace('\\', '\\\\', $replacement);
$this->replacements[str_replace('\\', '\\\\\\\\', $original)] = str_replace('\\', '\\\\\\\\', $replacement);
}
}
/**
* @param string $value
* @return string
*/
public function replace($value)
{
return strtr($value, $this->replacements);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Laminas\ZendFrameworkBridge;
class RewriteRules
{
/**
* @return array
*/
public static function namespaceRewrite()
{
return [
// Expressive
'Zend\\ProblemDetails\\' => 'Mezzio\\ProblemDetails\\',
'Zend\\Expressive\\' => 'Mezzio\\',
// Laminas
'Zend\\' => 'Laminas\\',
'ZF\\ComposerAutoloading\\' => 'Laminas\\ComposerAutoloading\\',
'ZF\\DevelopmentMode\\' => 'Laminas\\DevelopmentMode\\',
// Apigility
'ZF\\Apigility\\' => 'Laminas\\ApiTools\\',
'ZF\\' => 'Laminas\\ApiTools\\',
// ZendXml, API wrappers, zend-http OAuth support, zend-diagnostics, ZendDeveloperTools
'ZendXml\\' => 'Laminas\\Xml\\',
'ZendOAuth\\' => 'Laminas\\OAuth\\',
'ZendDiagnostics\\' => 'Laminas\\Diagnostics\\',
'ZendService\\ReCaptcha\\' => 'Laminas\\ReCaptcha\\',
'ZendService\\Twitter\\' => 'Laminas\\Twitter\\',
'ZendDeveloperTools\\' => 'Laminas\\DeveloperTools\\',
];
}
/**
* @return array
*/
public static function namespaceReverse()
{
return [
// ZendXml, ZendOAuth, ZendDiagnostics, ZendDeveloperTools
'Laminas\\Xml\\' => 'ZendXml\\',
'Laminas\\OAuth\\' => 'ZendOAuth\\',
'Laminas\\Diagnostics\\' => 'ZendDiagnostics\\',
'Laminas\\DeveloperTools\\' => 'ZendDeveloperTools\\',
// Zend Service
'Laminas\\ReCaptcha\\' => 'ZendService\\ReCaptcha\\',
'Laminas\\Twitter\\' => 'ZendService\\Twitter\\',
// Zend
'Laminas\\' => 'Zend\\',
// Expressive
'Mezzio\\ProblemDetails\\' => 'Zend\\ProblemDetails\\',
'Mezzio\\' => 'Zend\\Expressive\\',
// Laminas to ZfCampus
'Laminas\\ComposerAutoloading\\' => 'ZF\\ComposerAutoloading\\',
'Laminas\\DevelopmentMode\\' => 'ZF\\DevelopmentMode\\',
// Apigility
'Laminas\\ApiTools\\Admin\\' => 'ZF\\Apigility\\Admin\\',
'Laminas\\ApiTools\\Doctrine\\' => 'ZF\\Apigility\\Doctrine\\',
'Laminas\\ApiTools\\Documentation\\' => 'ZF\\Apigility\\Documentation\\',
'Laminas\\ApiTools\\Example\\' => 'ZF\\Apigility\\Example\\',
'Laminas\\ApiTools\\Provider\\' => 'ZF\\Apigility\\Provider\\',
'Laminas\\ApiTools\\Welcome\\' => 'ZF\\Apiglity\\Welcome\\',
'Laminas\\ApiTools\\' => 'ZF\\',
];
}
}

View File

@@ -0,0 +1,3 @@
<?php
Laminas\ZendFrameworkBridge\Autoloader::load();

View File

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

View File

@@ -0,0 +1,38 @@
<?php
namespace PicoFeed;
use PicoFeed\Config\Config;
use PicoFeed\Logging\Logger;
/**
* Base class
*
* @package PicoFeed
* @author Frederic Guillot
*/
abstract class Base
{
/**
* Config class instance
*
* @access protected
* @var \PicoFeed\Config\Config
*/
protected $config;
/**
* Constructor.
*
* @param \PicoFeed\Config\Config $config Config class instance
*/
public function __construct(Config $config = null)
{
$this->config = $config ?: new Config();
Logger::setTimezone($this->config->getTimezone());
}
public function setConfig(Config $config) {
$this->config = $config;
}
}

View File

@@ -0,0 +1,712 @@
<?php
namespace PicoFeed\Client;
use DateTime;
use Exception;
use LogicException;
use PicoFeed\Logging\Logger;
use PicoFeed\Config\Config;
/**
* Client class.
*
* @author Frederic Guillot
*/
abstract class Client
{
/**
* Flag that say if the resource have been modified.
*
* @var bool
*/
private $is_modified = true;
/**
* HTTP Content-Type.
*
* @var string
*/
private $content_type = '';
/**
* HTTP encoding.
*
* @var string
*/
private $encoding = '';
/**
* HTTP request headers.
*
* @var array
*/
protected $request_headers = array();
/**
* HTTP Etag header.
*
* @var string
*/
protected $etag = '';
/**
* HTTP Last-Modified header.
*
* @var string
*/
protected $last_modified = '';
/**
* Expiration DateTime
*
* @var DateTime
*/
protected $expiration = null;
/**
* Proxy hostname.
*
* @var string
*/
protected $proxy_hostname = '';
/**
* Proxy port.
*
* @var int
*/
protected $proxy_port = 3128;
/**
* Proxy username.
*
* @var string
*/
protected $proxy_username = '';
/**
* Proxy password.
*
* @var string
*/
protected $proxy_password = '';
/**
* Basic auth username.
*
* @var string
*/
protected $username = '';
/**
* Basic auth password.
*
* @var string
*/
protected $password = '';
/**
* CURL options.
*
* @var array
*/
protected $additional_curl_options = array();
/**
* Client connection timeout.
*
* @var int
*/
protected $timeout = 10;
/**
* User-agent.
*
* @var string
*/
protected $user_agent = 'PicoFeed (https://github.com/miniflux/picoFeed)';
/**
* Real URL used (can be changed after a HTTP redirect).
*
* @var string
*/
protected $url = '';
/**
* Page/Feed content.
*
* @var string
*/
protected $content = '';
/**
* Number maximum of HTTP redirections to avoid infinite loops.
*
* @var int
*/
protected $max_redirects = 5;
/**
* Maximum size of the HTTP body response.
*
* @var int
*/
protected $max_body_size = 2097152; // 2MB
/**
* HTTP response status code.
*
* @var int
*/
protected $status_code = 0;
/**
* Enables direct passthrough to requesting client.
*
* @var bool
*/
protected $passthrough = false;
/**
* Do the HTTP request.
*
* @abstract
*
* @return array
*/
abstract public function doRequest();
/**
* Get client instance: curl or stream driver.
*
* @static
*
* @return \PicoFeed\Client\Client
*/
public static function getInstance()
{
if (function_exists('curl_init')) {
return new Curl();
} elseif (ini_get('allow_url_fopen')) {
return new Stream();
}
throw new LogicException('You must have "allow_url_fopen=1" or curl extension installed');
}
/**
* Add HTTP Header to the request.
*
* @param array $headers
*/
public function setHeaders($headers)
{
$this->request_headers = $headers;
}
/**
* Perform the HTTP request.
*
* @param string $url URL
*
* @return Client
*/
public function execute($url = '')
{
if ($url !== '') {
$this->url = $url;
}
Logger::setMessage(get_called_class().' Fetch URL: '.$this->url);
Logger::setMessage(get_called_class().' Etag provided: '.$this->etag);
Logger::setMessage(get_called_class().' Last-Modified provided: '.$this->last_modified);
$response = $this->doRequest();
$this->status_code = $response['status'];
$this->handleNotModifiedResponse($response);
$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;
}
/**
* Handle not modified response.
*
* @param array $response Client response
*/
protected function handleNotModifiedResponse(array $response)
{
if ($response['status'] == 304) {
$this->is_modified = false;
} elseif ($response['status'] == 200) {
$this->is_modified = $this->hasBeenModified($response, $this->etag, $this->last_modified);
$this->etag = $this->getHeader($response, 'ETag');
$this->last_modified = $this->getHeader($response, 'Last-Modified');
}
if ($this->is_modified === false) {
Logger::setMessage(get_called_class().' Resource not modified');
}
}
/**
* Handle Http Error codes
*
* @param array $response Client response
* @throws ForbiddenException
* @throws InvalidUrlException
* @throws UnauthorizedException
*/
protected function handleErrorResponse(array $response)
{
$status = $response['status'];
if ($status == 401) {
throw new UnauthorizedException('Wrong or missing credentials');
} else if ($status == 403) {
throw new ForbiddenException('Not allowed to access resource');
} else if ($status == 404) {
throw new InvalidUrlException('Resource not found');
}
}
/**
* Handle normal response.
*
* @param array $response Client response
*/
protected function handleNormalResponse(array $response)
{
if ($response['status'] == 200) {
$this->content = $response['body'];
$this->content_type = $this->findContentType($response);
$this->encoding = $this->findCharset();
}
}
/**
* Check if a request has been modified according to the parameters.
*
* @param array $response
* @param string $etag
* @param string $lastModified
*
* @return bool
*/
private function hasBeenModified($response, $etag, $lastModified)
{
$headers = array(
'Etag' => $etag,
'Last-Modified' => $lastModified,
);
// Compare the values for each header that is present
$presentCacheHeaderCount = 0;
foreach ($headers as $key => $value) {
if (isset($response['headers'][$key])) {
if ($response['headers'][$key] !== $value) {
return true;
}
++$presentCacheHeaderCount;
}
}
// If at least one header is present and the values match, the response
// was not modified
if ($presentCacheHeaderCount > 0) {
return false;
}
return true;
}
/**
* Find content type from response headers.
*
* @param array $response Client response
* @return string
*/
public function findContentType(array $response)
{
return strtolower($this->getHeader($response, 'Content-Type'));
}
/**
* Find charset from response headers.
*
* @return string
*/
public function findCharset()
{
$result = explode('charset=', $this->content_type);
return isset($result[1]) ? $result[1] : '';
}
/**
* Get header value from a client response.
*
* @param array $response Client response
* @param string $header Header name
* @return string
*/
public function getHeader(array $response, $header)
{
return isset($response['headers'][$header]) ? $response['headers'][$header] : '';
}
/**
* Set the Last-Modified HTTP header.
*
* @param string $last_modified Header value
* @return $this
*/
public function setLastModified($last_modified)
{
$this->last_modified = $last_modified;
return $this;
}
/**
* Get the value of the Last-Modified HTTP header.
*
* @return string
*/
public function getLastModified()
{
return $this->last_modified;
}
/**
* Set the value of the Etag HTTP header.
*
* @param string $etag Etag HTTP header value
* @return $this
*/
public function setEtag($etag)
{
$this->etag = $etag;
return $this;
}
/**
* Get the Etag HTTP header value.
*
* @return string
*/
public function getEtag()
{
return $this->etag;
}
/**
* Get the final url value.
*
* @return string
*/
public function getUrl()
{
return $this->url;
}
/**
* Set the url.
*
* @param $url
* @return string
*/
public function setUrl($url)
{
$this->url = $url;
return $this;
}
/**
* Get the HTTP response status code.
*
* @return int
*/
public function getStatusCode()
{
return $this->status_code;
}
/**
* Get the body of the HTTP response.
*
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* Get the content type value from HTTP headers.
*
* @return string
*/
public function getContentType()
{
return $this->content_type;
}
/**
* Get the encoding value from HTTP headers.
*
* @return string
*/
public function getEncoding()
{
return $this->encoding;
}
/**
* Return true if the remote resource has changed.
*
* @return bool
*/
public function isModified()
{
return $this->is_modified;
}
/**
* return true if passthrough mode is enabled.
*
* @return bool
*/
public function isPassthroughEnabled()
{
return $this->passthrough;
}
/**
* Set connection timeout.
*
* @param int $timeout Connection timeout
* @return $this
*/
public function setTimeout($timeout)
{
$this->timeout = $timeout ?: $this->timeout;
return $this;
}
/**
* Set a custom user agent.
*
* @param string $user_agent User Agent
* @return $this
*/
public function setUserAgent($user_agent)
{
$this->user_agent = $user_agent ?: $this->user_agent;
return $this;
}
/**
* Set the maximum number of HTTP redirections.
*
* @param int $max Maximum
* @return $this
*/
public function setMaxRedirections($max)
{
$this->max_redirects = $max ?: $this->max_redirects;
return $this;
}
/**
* Set the maximum size of the HTTP body.
*
* @param int $max Maximum
* @return $this
*/
public function setMaxBodySize($max)
{
$this->max_body_size = $max ?: $this->max_body_size;
return $this;
}
/**
* Set the proxy hostname.
*
* @param string $hostname Proxy hostname
* @return $this
*/
public function setProxyHostname($hostname)
{
$this->proxy_hostname = $hostname ?: $this->proxy_hostname;
return $this;
}
/**
* Set the proxy port.
*
* @param int $port Proxy port
* @return $this
*/
public function setProxyPort($port)
{
$this->proxy_port = $port ?: $this->proxy_port;
return $this;
}
/**
* Set the proxy username.
*
* @param string $username Proxy username
* @return $this
*/
public function setProxyUsername($username)
{
$this->proxy_username = $username ?: $this->proxy_username;
return $this;
}
/**
* Set the proxy password.
*
* @param string $password Password
* @return $this
*/
public function setProxyPassword($password)
{
$this->proxy_password = $password ?: $this->proxy_password;
return $this;
}
/**
* Set the username.
*
* @param string $username Basic Auth username
*
* @return $this
*/
public function setUsername($username)
{
$this->username = $username ?: $this->username;
return $this;
}
/**
* Set the password.
*
* @param string $password Basic Auth Password
*
* @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 $this
*/
public function enablePassthroughMode()
{
$this->passthrough = true;
return $this;
}
/**
* Disable the passthrough mode.
*
* @return $this
*/
public function disablePassthroughMode()
{
$this->passthrough = false;
return $this;
}
/**
* Set config object.
*
* @param \PicoFeed\Config\Config $config Config instance
* @return $this
*/
public function setConfig(Config $config)
{
if ($config !== null) {
$this->setTimeout($config->getClientTimeout());
$this->setUserAgent($config->getClientUserAgent());
$this->setMaxRedirections($config->getMaxRedirections());
$this->setMaxBodySize($config->getMaxBodySize());
$this->setProxyHostname($config->getProxyHostname());
$this->setProxyPort($config->getProxyPort());
$this->setProxyUsername($config->getProxyUsername());
$this->setProxyPassword($config->getProxyPassword());
$this->setAdditionalCurlOptions($config->getAdditionalCurlOptions() ?: array());
}
return $this;
}
/**
* Return true if the HTTP status code is a redirection
*
* @access protected
* @param integer $code
* @return boolean
*/
public function isRedirection($code)
{
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

@@ -0,0 +1,14 @@
<?php
namespace PicoFeed\Client;
use PicoFeed\PicoFeedException;
/**
* ClientException Exception.
*
* @author Frederic Guillot
*/
abstract class ClientException extends PicoFeedException
{
}

View File

@@ -0,0 +1,412 @@
<?php
namespace PicoFeed\Client;
use PicoFeed\Logging\Logger;
/**
* cURL HTTP client.
*
* @author Frederic Guillot
*/
class Curl extends Client
{
protected $nbRedirects = 0;
/**
* HTTP response body.
*
* @var string
*/
private $body = '';
/**
* Body size.
*
* @var int
*/
private $body_length = 0;
/**
* HTTP response headers.
*
* @var array
*/
private $response_headers = array();
/**
* Counter on the number of header received.
*
* @var int
*/
private $response_headers_count = 0;
/**
* cURL callback to read the HTTP body.
*
* If the function return -1, curl stop to read the HTTP response
*
* @param resource $ch cURL handler
* @param string $buffer Chunk of data
*
* @return int Length of the buffer
*/
public function readBody($ch, $buffer)
{
$length = strlen($buffer);
$this->body_length += $length;
if ($this->body_length > $this->max_body_size) {
return -1;
}
$this->body .= $buffer;
return $length;
}
/**
* cURL callback to read HTTP headers.
*
* @param resource $ch cURL handler
* @param string $buffer Header line
*
* @return int Length of the buffer
*/
public function readHeaders($ch, $buffer)
{
$length = strlen($buffer);
if ($buffer === "\r\n" || $buffer === "\n") {
++$this->response_headers_count;
} else {
if (!isset($this->response_headers[$this->response_headers_count])) {
$this->response_headers[$this->response_headers_count] = '';
}
$this->response_headers[$this->response_headers_count] .= $buffer;
}
return $length;
}
/**
* cURL callback to passthrough the HTTP body to the client.
*
* If the function return -1, curl stop to read the HTTP response
*
* @param resource $ch cURL handler
* @param string $buffer Chunk of data
*
* @return int Length of the buffer
*/
public function passthroughBody($ch, $buffer)
{
// do it only at the beginning of a transmission
if ($this->body_length === 0) {
list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1]));
if ($this->isRedirection($status)) {
return $this->handleRedirection($headers['Location']);
}
if (isset($headers['Content-Type'])) {
header('Content-Type:' .$headers['Content-Type']);
}
}
$length = strlen($buffer);
$this->body_length += $length;
echo $buffer;
return $length;
}
/**
* Prepare HTTP headers.
*
* @return string[]
*/
private function prepareHeaders()
{
$headers = array(
'Connection: close',
);
if ($this->etag) {
$headers[] = 'If-None-Match: '.$this->etag;
$headers[] = 'A-IM: feed';
}
if ($this->last_modified) {
$headers[] = 'If-Modified-Since: '.$this->last_modified;
}
$headers = array_merge($headers, $this->request_headers);
return $headers;
}
/**
* Prepare curl proxy context.
*
* @param resource $ch
*
* @return resource $ch
*/
private function prepareProxyContext($ch)
{
if ($this->proxy_hostname) {
Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port);
curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxy_port);
curl_setopt($ch, CURLOPT_PROXYTYPE, 'HTTP');
curl_setopt($ch, CURLOPT_PROXY, $this->proxy_hostname);
if ($this->proxy_username) {
Logger::setMessage(get_called_class().' Proxy credentials: Yes');
curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxy_username.':'.$this->proxy_password);
} else {
Logger::setMessage(get_called_class().' Proxy credentials: No');
}
}
return $ch;
}
/**
* Prepare curl auth context.
*
* @param resource $ch
*
* @return resource $ch
*/
private function prepareAuthContext($ch)
{
if ($this->username && $this->password) {
curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password);
}
return $ch;
}
/**
* Set write/header functions.
*
* @param resource $ch
*
* @return resource $ch
*/
private function prepareDownloadMode($ch)
{
$this->body = '';
$this->response_headers = array();
$this->response_headers_count = 0;
$write_function = 'readBody';
$header_function = 'readHeaders';
if ($this->isPassthroughEnabled()) {
$write_function = 'passthroughBody';
}
curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($this, $write_function));
curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, $header_function));
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.
*
* @return resource
*/
private function prepareContext()
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_USERAGENT, $this->user_agent);
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->prepareHeaders());
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_ENCODING, '');
curl_setopt($ch, CURLOPT_COOKIEJAR, 'php://memory');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'php://memory');
// Disable SSLv3 by enforcing TLSv1.x for curl >= 7.34.0 and < 7.39.0.
// Versions prior to 7.34 and at least when compiled against openssl
// interpret this parameter as "limit to TLSv1.0" which fails for sites
// which enforce TLS 1.1+.
// Starting with curl 7.39.0 SSLv3 is disabled by default.
$version = curl_version();
if ($version['version_number'] >= 467456 && $version['version_number'] < 468736) {
curl_setopt($ch, CURLOPT_SSLVERSION, 1);
}
$ch = $this->prepareDownloadMode($ch);
$ch = $this->prepareProxyContext($ch);
$ch = $this->prepareAuthContext($ch);
$ch = $this->prepareAdditionalCurlOptions($ch);
return $ch;
}
/**
* Execute curl context.
*/
private function executeContext()
{
$ch = $this->prepareContext();
curl_exec($ch);
Logger::setMessage(get_called_class().' cURL total time: '.curl_getinfo($ch, CURLINFO_TOTAL_TIME));
Logger::setMessage(get_called_class().' cURL dns lookup time: '.curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME));
Logger::setMessage(get_called_class().' cURL connect time: '.curl_getinfo($ch, CURLINFO_CONNECT_TIME));
Logger::setMessage(get_called_class().' cURL speed download: '.curl_getinfo($ch, CURLINFO_SPEED_DOWNLOAD));
Logger::setMessage(get_called_class().' cURL effective url: '.curl_getinfo($ch, CURLINFO_EFFECTIVE_URL));
$curl_errno = curl_errno($ch);
if ($curl_errno) {
Logger::setMessage(get_called_class().' cURL error: '.curl_error($ch));
curl_close($ch);
$this->handleError($curl_errno);
}
// Update the url if there where redirects
$this->url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
curl_close($ch);
}
/**
* Do the HTTP request.
*
* @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...]
*/
public function doRequest()
{
$this->executeContext();
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,
'body' => $this->body,
'headers' => $headers,
);
}
/**
* Handle HTTP redirects
*
* @param string $location Redirected URL
* @return array
* @throws MaxRedirectException
*/
private function handleRedirection($location)
{
$result = array();
$this->url = Url::resolve($location, $this->url);
$this->body = '';
$this->body_length = 0;
$this->response_headers = array();
$this->response_headers_count = 0;
while (true) {
$this->nbRedirects++;
if ($this->nbRedirects >= $this->max_redirects) {
throw new MaxRedirectException('Maximum number of redirections reached');
}
$result = $this->doRequest();
if ($this->isRedirection($result['status'])) {
$this->url = Url::resolve($result['headers']['Location'], $this->url);
$this->body = '';
$this->body_length = 0;
$this->response_headers = array();
$this->response_headers_count = 0;
} else {
break;
}
}
return $result;
}
/**
* Handle cURL errors (throw individual exceptions).
*
* We don't use constants because they are not necessary always available
* (depends of the version of libcurl linked to php)
*
* @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)
{
switch ($errno) {
case 78: // CURLE_REMOTE_FILE_NOT_FOUND
throw new InvalidUrlException('Resource not found', $errno);
case 6: // CURLE_COULDNT_RESOLVE_HOST
throw new InvalidUrlException('Unable to resolve hostname', $errno);
case 7: // CURLE_COULDNT_CONNECT
throw new InvalidUrlException('Unable to connect to the remote host', $errno);
case 23: // CURLE_WRITE_ERROR
throw new MaxSizeException('Maximum response size exceeded', $errno);
case 28: // CURLE_OPERATION_TIMEDOUT
throw new TimeoutException('Operation timeout', $errno);
case 35: // CURLE_SSL_CONNECT_ERROR
case 51: // CURLE_PEER_FAILED_VERIFICATION
case 58: // CURLE_SSL_CERTPROBLEM
case 60: // CURLE_SSL_CACERT
case 59: // CURLE_SSL_CIPHER
case 64: // CURLE_USE_SSL_FAILED
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;
throw new InvalidCertificateException($msg, $errno);
case 47: // CURLE_TOO_MANY_REDIRECTS
throw new MaxRedirectException('Maximum number of redirections reached', $errno);
case 63: // CURLE_FILESIZE_EXCEEDED
throw new MaxSizeException('Maximum response size exceeded', $errno);
default:
throw new InvalidUrlException('Unable to fetch the URL', $errno);
}
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace PicoFeed\Client;
/**
* @author Bernhard Posselt
*/
class ForbiddenException extends ClientException
{
}

View File

@@ -0,0 +1,79 @@
<?php
namespace PicoFeed\Client;
use ArrayAccess;
use PicoFeed\Logging\Logger;
/**
* Class to handle HTTP headers case insensitivity.
*
* @author Bernhard Posselt
* @author Frederic Guillot
*/
class HttpHeaders implements ArrayAccess
{
private $headers = array();
public function __construct(array $headers)
{
foreach ($headers as $key => $value) {
$this->headers[strtolower($key)] = $value;
}
}
public function offsetGet($offset)
{
return $this->offsetExists($offset) ? $this->headers[strtolower($offset)] : '';
}
public function offsetSet($offset, $value)
{
$this->headers[strtolower($offset)] = $value;
}
public function offsetExists($offset)
{
return isset($this->headers[strtolower($offset)]);
}
public function offsetUnset($offset)
{
unset($this->headers[strtolower($offset)]);
}
/**
* Parse HTTP headers.
*
* @static
*
* @param array $lines List of headers
*
* @return array
*/
public static function parse(array $lines)
{
$status = 0;
$headers = array();
foreach ($lines as $line) {
if (strpos($line, 'HTTP/1') === 0) {
$headers = array();
$status = (int) substr($line, 9, 3);
} elseif (strpos($line, ': ') !== false) {
list($name, $value) = explode(': ', $line);
if ($value) {
$headers[trim($name)] = trim($value);
}
}
}
Logger::setMessage(get_called_class().' HTTP status code: '.$status);
foreach ($headers as $name => $value) {
Logger::setMessage(get_called_class().' HTTP header: '.$name.' => '.$value);
}
return array($status, new self($headers));
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace PicoFeed\Client;
/**
* InvalidCertificateException Exception.
*
* @author Frederic Guillot
*/
class InvalidCertificateException extends ClientException
{
}

View File

@@ -0,0 +1,12 @@
<?php
namespace PicoFeed\Client;
/**
* InvalidUrlException Exception.
*
* @author Frederic Guillot
*/
class InvalidUrlException extends ClientException
{
}

View File

@@ -0,0 +1,12 @@
<?php
namespace PicoFeed\Client;
/**
* MaxRedirectException Exception.
*
* @author Frederic Guillot
*/
class MaxRedirectException extends ClientException
{
}

View File

@@ -0,0 +1,12 @@
<?php
namespace PicoFeed\Client;
/**
* MaxSizeException Exception.
*
* @author Frederic Guillot
*/
class MaxSizeException extends ClientException
{
}

View File

@@ -0,0 +1,205 @@
<?php
namespace PicoFeed\Client;
use PicoFeed\Logging\Logger;
/**
* Stream context HTTP client.
*
* @author Frederic Guillot
*/
class Stream extends Client
{
/**
* Prepare HTTP headers.
*
* @return string[]
*/
private function prepareHeaders()
{
$headers = array(
'Connection: close',
'User-Agent: '.$this->user_agent,
);
// disable compression in passthrough mode. It could result in double
// compressed content which isn't decodeable by browsers
if (function_exists('gzdecode') && !$this->isPassthroughEnabled()) {
$headers[] = 'Accept-Encoding: gzip';
}
if ($this->etag) {
$headers[] = 'If-None-Match: '.$this->etag;
$headers[] = 'A-IM: feed';
}
if ($this->last_modified) {
$headers[] = 'If-Modified-Since: '.$this->last_modified;
}
if ($this->proxy_username) {
$headers[] = 'Proxy-Authorization: Basic '.base64_encode($this->proxy_username.':'.$this->proxy_password);
}
if ($this->username && $this->password) {
$headers[] = 'Authorization: Basic '.base64_encode($this->username.':'.$this->password);
}
$headers = array_merge($headers, $this->request_headers);
return $headers;
}
/**
* Construct the final URL from location headers.
*
* @param array $headers List of HTTP response header
*/
private function setEffectiveUrl($headers)
{
foreach ($headers as $header) {
if (stripos($header, 'Location') === 0) {
list(, $value) = explode(': ', $header);
$this->url = Url::resolve($value, $this->url);
}
}
}
/**
* Prepare stream context.
*
* @return array
*/
private function prepareContext()
{
$context = array(
'http' => array(
'method' => 'GET',
'protocol_version' => 1.1,
'timeout' => $this->timeout,
'max_redirects' => $this->max_redirects,
),
);
if ($this->proxy_hostname) {
Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port);
$context['http']['proxy'] = 'tcp://'.$this->proxy_hostname.':'.$this->proxy_port;
$context['http']['request_fulluri'] = true;
if ($this->proxy_username) {
Logger::setMessage(get_called_class().' Proxy credentials: Yes');
} else {
Logger::setMessage(get_called_class().' Proxy credentials: No');
}
}
$context['http']['header'] = implode("\r\n", $this->prepareHeaders());
return $context;
}
/**
* Do the HTTP request.
*
* @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...]
* @throws InvalidUrlException
* @throws MaxSizeException
* @throws TimeoutException
*/
public function doRequest()
{
$body = '';
// Create context
$context = stream_context_create($this->prepareContext());
// Make HTTP request
$stream = @fopen($this->url, 'r', false, $context);
if (!is_resource($stream)) {
throw new InvalidUrlException('Unable to establish a connection');
}
// Get HTTP headers response
$metadata = stream_get_meta_data($stream);
list($status, $headers) = HttpHeaders::parse($metadata['wrapper_data']);
if ($this->isPassthroughEnabled()) {
header(':', true, $status);
if (isset($headers['Content-Type'])) {
header('Content-Type: '.$headers['Content-Type']);
}
fpassthru($stream);
} else {
// Get the entire body until the max size
$body = stream_get_contents($stream, $this->max_body_size + 1);
// If the body size is too large abort everything
if (strlen($body) > $this->max_body_size) {
throw new MaxSizeException('Content size too large');
}
if ($metadata['timed_out']) {
throw new TimeoutException('Operation timeout');
}
}
fclose($stream);
$this->setEffectiveUrl($metadata['wrapper_data']);
return array(
'status' => $status,
'body' => $this->decodeBody($body, $headers),
'headers' => $headers,
);
}
/**
* Decode body response according to the HTTP headers.
*
* @param string $body Raw body
* @param HttpHeaders $headers HTTP headers
*
* @return string
*/
public function decodeBody($body, HttpHeaders $headers)
{
if (isset($headers['Transfer-Encoding']) && $headers['Transfer-Encoding'] === 'chunked') {
$body = $this->decodeChunked($body);
}
if (isset($headers['Content-Encoding']) && $headers['Content-Encoding'] === 'gzip') {
$body = gzdecode($body);
}
return $body;
}
/**
* Decode a chunked body.
*
* @param string $str Raw body
*
* @return string Decoded body
*/
public function decodeChunked($str)
{
for ($result = ''; !empty($str); $str = trim($str)) {
// Get the chunk length
$pos = strpos($str, "\r\n");
$len = hexdec(substr($str, 0, $pos));
// Append the chunk to the result
$result .= substr($str, $pos + 2, $len);
$str = substr($str, $pos + 2 + $len);
}
return $result;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace PicoFeed\Client;
/**
* TimeoutException Exception.
*
* @author Frederic Guillot
*/
class TimeoutException extends ClientException
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace PicoFeed\Client;
/**
* @author Bernhard Posselt
*/
class UnauthorizedException extends ClientException
{
}

View File

@@ -0,0 +1,283 @@
<?php
namespace PicoFeed\Client;
/**
* URL class.
*
* @author Frederic Guillot
*/
class Url
{
/**
* URL.
*
* @var string
*/
private $url = '';
/**
* URL components.
*
* @var array
*/
private $components = array();
/**
* Constructor.
*
* @param string $url URL
*/
public function __construct($url)
{
$this->url = $url;
$this->components = parse_url($url) ?: array();
// Issue with PHP < 5.4.7 and protocol relative url
if (version_compare(PHP_VERSION, '5.4.7', '<') && $this->isProtocolRelative()) {
$pos = strpos($this->components['path'], '/', 2);
if ($pos === false) {
$pos = strlen($this->components['path']);
}
$this->components['host'] = substr($this->components['path'], 2, $pos - 2);
$this->components['path'] = substr($this->components['path'], $pos);
}
}
/**
* 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)
{
$link = is_string($item_url) ? new self($item_url) : $item_url;
$website = is_string($website_url) ? new self($website_url) : $website_url;
if ($link->isRelativeUrl()) {
if ($link->isRelativePath()) {
return $link->getAbsoluteUrl($website->getBaseUrl($website->getBasePath()));
}
return $link->getAbsoluteUrl($website->getBaseUrl());
} elseif ($link->isProtocolRelative()) {
$link->setScheme($website->getScheme());
}
return $link->getAbsoluteUrl();
}
/**
* Shortcut method to get a base url.
*
* @static
* @param string $url
* @return string
*/
public static function base($url)
{
$link = new self($url);
return $link->getBaseUrl();
}
/**
* Get the base URL.
*
* @param string $suffix Add a suffix to the url
* @return string
*/
public function getBaseUrl($suffix = '')
{
return $this->hasHost() ? $this->getScheme('://').$this->getHost().$this->getPort(':').$suffix : '';
}
/**
* Get the absolute URL.
*
* @param string $base_url Use this url as base url
* @return string
*/
public function getAbsoluteUrl($base_url = '')
{
if ($base_url) {
$base = new self($base_url);
$url = $base->getAbsoluteUrl().substr($this->getFullPath(), 1);
} else {
$url = $this->hasHost() ? $this->getBaseUrl().$this->getFullPath() : '';
}
return $url;
}
/**
* Return true if the url is relative.
*
* @return bool
*/
public function isRelativeUrl()
{
return !$this->hasScheme() && !$this->isProtocolRelative();
}
/**
* Return true if the path is relative.
*
* @return bool
*/
public function isRelativePath()
{
$path = $this->getPath();
return empty($path) || $path[0]
!== '/';
}
/**
* Filters the path of a URI.
*
* 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 = '!\$&\'\(\)\*\+,;=')
{
return preg_replace_callback(
'/(?:[^'.$charUnreserved.$charSubDelims.':@\/%]+|%(?![A-Fa-f0-9]{2}))/',
function (array $matches) { return rawurlencode($matches[0]); },
$path
);
}
/**
* Get the path.
*
* @return string
*/
public function getPath()
{
return $this->filterPath(empty($this->components['path']) ? '' : $this->components['path']);
}
/**
* Get the base path.
*
* @return string
*/
public function getBasePath()
{
$current_path = $this->getPath();
$path = $this->isRelativePath() ? '/' : '';
$path .= substr($current_path, -1) === '/' ? $current_path : dirname($current_path);
return preg_replace('/\\\\\/|\/\//', '/', $path.'/');
}
/**
* Get the full path (path + querystring + fragment).
*
* @return string
*/
public function getFullPath()
{
$path = $this->isRelativePath() ? '/' : '';
$path .= $this->getPath();
$path .= empty($this->components['query']) ? '' : '?'.$this->components['query'];
$path .= empty($this->components['fragment']) ? '' : '#'.$this->components['fragment'];
return $path;
}
/**
* Get the hostname.
*
* @return string
*/
public function getHost()
{
return empty($this->components['host']) ? '' : $this->components['host'];
}
/**
* Return true if the url has a hostname.
*
* @return bool
*/
public function hasHost()
{
return !empty($this->components['host']);
}
/**
* Get the scheme.
*
* @param string $suffix Suffix to add when there is a scheme
* @return string
*/
public function getScheme($suffix = '')
{
return ($this->hasScheme() ? $this->components['scheme'] : 'http').$suffix;
}
/**
* Set the scheme.
*
* @param string $scheme Set a scheme
* @return $this
*/
public function setScheme($scheme)
{
$this->components['scheme'] = $scheme;
return $this;
}
/**
* Return true if the url has a scheme.
*
* @return bool
*/
public function hasScheme()
{
return !empty($this->components['scheme']);
}
/**
* Get the port.
*
* @param string $prefix Prefix to add when there is a port
* @return string
*/
public function getPort($prefix = '')
{
return $this->hasPort() ? $prefix.$this->components['port'] : '';
}
/**
* Return true if the url has a port.
*
* @return bool
*/
public function hasPort()
{
return !empty($this->components['port']);
}
/**
* Return true if the url is protocol relative (start with //).
*
* @return bool
*/
public function isProtocolRelative()
{
return strpos($this->url, '//') === 0;
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace PicoFeed\Config;
/**
* Config class.
*
* @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)
* @method \PicoFeed\Config\Config setProxyUsername(string $value)
* @method \PicoFeed\Config\Config setProxyPassword(string $value)
* @method \PicoFeed\Config\Config setGrabberRulesFolder(string $value)
* @method \PicoFeed\Config\Config setGrabberTimeout(integer $value)
* @method \PicoFeed\Config\Config setGrabberUserAgent(string $value)
* @method \PicoFeed\Config\Config setParserHashAlgo(string $value)
* @method \PicoFeed\Config\Config setContentFiltering(boolean $value)
* @method \PicoFeed\Config\Config setTimezone(string $value)
* @method \PicoFeed\Config\Config setFilterIframeWhitelist(array $value)
* @method \PicoFeed\Config\Config setFilterIntegerAttributes(array $value)
* @method \PicoFeed\Config\Config setFilterAttributeOverrides(array $value)
* @method \PicoFeed\Config\Config setFilterRequiredAttributes(array $value)
* @method \PicoFeed\Config\Config setFilterMediaBlacklist(array $value)
* @method \PicoFeed\Config\Config setFilterMediaAttributes(array $value)
* @method \PicoFeed\Config\Config setFilterSchemeWhitelist(array $value)
* @method \PicoFeed\Config\Config setFilterWhitelistedTags(array $value)
* @method \PicoFeed\Config\Config setFilterBlacklistedTags(array $value)
* @method \PicoFeed\Config\Config setFilterImageProxyUrl($value)
* @method \PicoFeed\Config\Config setFilterImageProxyCallback($closure)
* @method \PicoFeed\Config\Config setFilterImageProxyProtocol($value)
* @method integer getClientTimeout()
* @method string getClientUserAgent()
* @method integer getMaxRedirections()
* @method integer getMaxRecursions()
* @method integer getMaxBodySize()
* @method string getProxyHostname()
* @method integer getProxyPort()
* @method string getProxyUsername()
* @method string getProxyPassword()
* @method string getGrabberRulesFolder()
* @method integer getGrabberTimeout()
* @method string getGrabberUserAgent()
* @method string getParserHashAlgo()
* @method boolean getContentFiltering(bool $default_value)
* @method string getTimezone()
* @method array getFilterIframeWhitelist(array $default_value)
* @method array getFilterIntegerAttributes(array $default_value)
* @method array getFilterAttributeOverrides(array $default_value)
* @method array getFilterRequiredAttributes(array $default_value)
* @method array getFilterMediaBlacklist(array $default_value)
* @method array getFilterMediaAttributes(array $default_value)
* @method array getFilterSchemeWhitelist(array $default_value)
* @method array getFilterWhitelistedTags(array $default_value)
* @method array getFilterBlacklistedTags(array $default_value)
* @method string getFilterImageProxyUrl()
* @method \Closure getFilterImageProxyCallback()
* @method string getFilterImageProxyProtocol()
* @method array getAdditionalCurlOptions()
*/
class Config
{
/**
* Contains all parameters.
*
* @var array
*/
private $container = array();
/**
* Magic method to have any kind of setters or getters.
*
* @param string $name Getter/Setter name
* @param array $arguments Method arguments
*
* @return mixed
*/
public function __call($name, array $arguments)
{
$name = strtolower($name);
$prefix = substr($name, 0, 3);
$parameter = substr($name, 3);
if ($prefix === 'set' && isset($arguments[0])) {
$this->container[$parameter] = $arguments[0];
return $this;
} elseif ($prefix === 'get') {
$default_value = isset($arguments[0]) ? $arguments[0] : null;
return isset($this->container[$parameter]) ? $this->container[$parameter] : $default_value;
}
return null;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace PicoFeed\Encoding;
/**
* Encoding class.
*/
class Encoding
{
public static function convert($input, $encoding)
{
if ($encoding === 'utf-8' || $encoding === '') {
return $input;
}
// suppress all notices since it isn't possible to silence only the
// notice "Wrong charset, conversion from $in_encoding to $out_encoding is not allowed"
set_error_handler(function () {}, E_NOTICE);
// convert input to utf-8 and strip invalid characters
$value = iconv($encoding, 'UTF-8//IGNORE', $input);
// stop silencing of notices
restore_error_handler();
// return input if something went wrong, maybe it's usable anyway
if ($value === false) {
return $input;
}
return $value;
}
}

View File

@@ -0,0 +1,700 @@
<?php
namespace PicoFeed\Filter;
use PicoFeed\Client\Url;
/**
* Attribute Filter class.
*
* @author Frederic Guillot
*/
class Attribute
{
/**
* Image proxy url.
*
* @var string
*/
private $image_proxy_url = '';
/**
* Image proxy callback.
*
* @var \Closure|null
*/
private $image_proxy_callback = null;
/**
* limits the image proxy usage to this protocol.
*
* @var string
*/
private $image_proxy_limit_protocol = '';
/**
* Tags and attribute whitelist.
*
* @var array
*/
private $attribute_whitelist = array(
'audio' => array('controls', 'src'),
'video' => array('poster', 'controls', 'height', 'width', 'src'),
'source' => array('src', 'type'),
'dt' => array(),
'dd' => array(),
'dl' => array(),
'table' => array(),
'caption' => array(),
'tr' => array(),
'th' => array(),
'td' => array(),
'tbody' => array(),
'thead' => array(),
'h1' => array(),
'h2' => array(),
'h3' => array(),
'h4' => array(),
'h5' => array(),
'h6' => array(),
'strong' => array(),
'em' => array(),
'code' => array(),
'pre' => array(),
'blockquote' => array(),
'p' => array(),
'ul' => array(),
'li' => array(),
'ol' => array(),
'br' => array(),
'del' => array(),
'a' => array('href'),
'img' => array('src', 'title', 'alt'),
'figure' => array(),
'figcaption' => array(),
'cite' => array(),
'time' => array('datetime'),
'abbr' => array('title'),
'iframe' => array('width', 'height', 'frameborder', 'src', 'allowfullscreen'),
'q' => array('cite'),
);
/**
* Scheme whitelist.
*
* For a complete list go to http://en.wikipedia.org/wiki/URI_scheme
*
* @var array
*/
private $scheme_whitelist = array(
'bitcoin:',
'callto:',
'ed2k://',
'facetime://',
'feed:',
'ftp://',
'geo:',
'git://',
'http://',
'https://',
'irc://',
'irc6://',
'ircs://',
'jabber:',
'magnet:',
'mailto:',
'nntp://',
'rtmp://',
'sftp://',
'sip:',
'sips:',
'skype:',
'smb://',
'sms:',
'spotify:',
'ssh:',
'steam:',
'svn://',
'tel:',
);
/**
* Iframe source whitelist, everything else is ignored.
*
* @var array
*/
private $iframe_whitelist = array(
'http://www.youtube.com',
'https://www.youtube.com',
'http://player.vimeo.com',
'https://player.vimeo.com',
'http://www.dailymotion.com',
'https://www.dailymotion.com',
'http://vk.com',
'https://vk.com',
);
/**
* Blacklisted resources.
*
* @var array
*/
private $media_blacklist = array(
'api.flattr.com',
'feeds.feedburner.com',
'share.feedsportal.com',
'da.feedsportal.com',
'rc.feedsportal.com',
'rss.feedsportal.com',
'res.feedsportal.com',
'res1.feedsportal.com',
'res2.feedsportal.com',
'res3.feedsportal.com',
'pi.feedsportal.com',
'rss.nytimes.com',
'feeds.wordpress.com',
'stats.wordpress.com',
'rss.cnn.com',
'twitter.com/home?status=',
'twitter.com/share',
'twitter_icon_large.png',
'www.facebook.com/sharer.php',
'facebook_icon_large.png',
'plus.google.com/share',
'www.gstatic.com/images/icons/gplus-16.png',
'www.gstatic.com/images/icons/gplus-32.png',
'www.gstatic.com/images/icons/gplus-64.png',
);
/**
* Attributes used for external resources.
*
* @var array
*/
private $media_attributes = array(
'src',
'href',
'poster',
);
/**
* Attributes that must be integer.
*
* @var array
*/
private $integer_attributes = array(
'width',
'height',
'frameborder',
);
/**
* Mandatory attributes for specified tags.
*
* @var array
*/
private $required_attributes = array(
'a' => array('href'),
'img' => array('src'),
'iframe' => array('src'),
'audio' => array('src'),
'source' => array('src'),
);
/**
* Add attributes to specified tags.
*
* @var array
*/
private $add_attributes = array(
'a' => array('rel' => 'noreferrer', 'target' => '_blank'),
'video' => array('controls' => 'true'),
);
/**
* List of filters to apply.
*
* @var array
*/
private $filters = array(
'filterAllowedAttribute',
'filterIntegerAttribute',
'rewriteAbsoluteUrl',
'filterIframeAttribute',
'filterBlacklistResourceAttribute',
'filterProtocolUrlAttribute',
'rewriteImageProxyUrl',
'secureIframeSrc',
'removeYouTubeAutoplay',
);
/**
* Add attributes to specified tags.
*
* @var \PicoFeed\Client\Url
*/
private $website;
/**
* Constructor.
*
* @param \PicoFeed\Client\Url $website Website url instance
*/
public function __construct(Url $website)
{
$this->website = $website;
}
/**
* Apply filters to the attributes list.
*
* @param string $tag Tag name
* @param array $attributes Attributes dictionary
*
* @return array Filtered attributes
*/
public function filter($tag, array $attributes)
{
foreach ($attributes as $attribute => &$value) {
foreach ($this->filters as $filter) {
if (!$this->$filter($tag, $attribute, $value)) {
unset($attributes[$attribute]);
break;
}
}
}
return $attributes;
}
/**
* Return true if the value is allowed (remove not allowed attributes).
*
* @param string $tag Tag name
* @param string $attribute Attribute name
* @param string $value Attribute value
*
* @return bool
*/
public function filterAllowedAttribute($tag, $attribute, $value)
{
return isset($this->attribute_whitelist[$tag]) && in_array($attribute, $this->attribute_whitelist[$tag]);
}
/**
* Return true if the value is not integer (remove attributes that should have an integer value).
*
* @param string $tag Tag name
* @param string $attribute Attribute name
* @param string $value Attribute value
*
* @return bool
*/
public function filterIntegerAttribute($tag, $attribute, $value)
{
if (in_array($attribute, $this->integer_attributes)) {
return ctype_digit($value);
}
return true;
}
/**
* Return true if the iframe source is allowed (remove not allowed iframe).
*
* @param string $tag Tag name
* @param string $attribute Attribute name
* @param string $value Attribute value
*
* @return bool
*/
public function filterIframeAttribute($tag, $attribute, $value)
{
if ($tag === 'iframe' && $attribute === 'src') {
foreach ($this->iframe_whitelist as $url) {
if (strpos($value, $url) === 0) {
return true;
}
}
return false;
}
return true;
}
/**
* Return true if the resource is not blacklisted (remove blacklisted resource attributes).
*
* @param string $tag Tag name
* @param string $attribute Attribute name
* @param string $value Attribute value
*
* @return bool
*/
public function filterBlacklistResourceAttribute($tag, $attribute, $value)
{
if ($this->isResource($attribute) && $this->isBlacklistedMedia($value)) {
return false;
}
return true;
}
/**
* Convert all relative links to absolute url.
*
* @param string $tag Tag name
* @param string $attribute Attribute name
* @param string $value Attribute value
*
* @return bool
*/
public function rewriteAbsoluteUrl($tag, $attribute, &$value)
{
if ($this->isResource($attribute)) {
$value = Url::resolve($value, $this->website);
}
return true;
}
/**
* Turns iframes' src attribute from http to https to prevent
* mixed active content.
*
* @param string $tag Tag name
* @param array $attribute Atttributes name
* @param string $value Attribute value
*
* @return bool
*/
public function secureIframeSrc($tag, $attribute, &$value)
{
if ($tag === 'iframe' && $attribute === 'src' && strpos($value, 'http://') === 0) {
$value = substr_replace($value, 's', 4, 0);
}
return true;
}
/**
* Removes YouTube autoplay from iframes.
*
* @param string $tag Tag name
* @param array $attribute Atttributes name
* @param string $value Attribute value
*
* @return bool
*/
public function removeYouTubeAutoplay($tag, $attribute, &$value)
{
$regex = '%^(https://(?:www\.)?youtube.com/.*\?.*autoplay=)(1)(.*)%i';
if ($tag === 'iframe' && $attribute === 'src' && preg_match($regex, $value)) {
$value = preg_replace($regex, '${1}0$3', $value);
}
return true;
}
/**
* Rewrite image url to use with a proxy.
*
* @param string $tag Tag name
* @param string $attribute Attribute name
* @param string $value Attribute value
*
* @return bool
*/
public function rewriteImageProxyUrl($tag, $attribute, &$value)
{
if ($tag === 'img' && $attribute === 'src'
&& !($this->image_proxy_limit_protocol !== '' && stripos($value, $this->image_proxy_limit_protocol.':') !== 0)) {
if ($this->image_proxy_url) {
$value = sprintf($this->image_proxy_url, rawurlencode($value));
} elseif (is_callable($this->image_proxy_callback)) {
$value = call_user_func($this->image_proxy_callback, $value);
}
}
return true;
}
/**
* Return true if the scheme is authorized.
*
* @param string $tag Tag name
* @param string $attribute Attribute name
* @param string $value Attribute value
*
* @return bool
*/
public function filterProtocolUrlAttribute($tag, $attribute, $value)
{
if ($this->isResource($attribute) && !$this->isAllowedProtocol($value)) {
return false;
}
return true;
}
/**
* Automatically add/override some attributes for specific tags.
*
* @param string $tag Tag name
* @param array $attributes Attributes list
*
* @return array
*/
public function addAttributes($tag, array $attributes)
{
if (isset($this->add_attributes[$tag])) {
$attributes += $this->add_attributes[$tag];
}
return $attributes;
}
/**
* Return true if all required attributes are present.
*
* @param string $tag Tag name
* @param array $attributes Attributes list
*
* @return bool
*/
public function hasRequiredAttributes($tag, array $attributes)
{
if (isset($this->required_attributes[$tag])) {
foreach ($this->required_attributes[$tag] as $attribute) {
if (!isset($attributes[$attribute])) {
return false;
}
}
}
return true;
}
/**
* Check if an attribute name is an external resource.
*
* @param string $attribute Attribute name
*
* @return bool
*/
public function isResource($attribute)
{
return in_array($attribute, $this->media_attributes);
}
/**
* Detect if the protocol is allowed or not.
*
* @param string $value Attribute value
*
* @return bool
*/
public function isAllowedProtocol($value)
{
foreach ($this->scheme_whitelist as $protocol) {
if (strpos($value, $protocol) === 0) {
return true;
}
}
return false;
}
/**
* Detect if an url is blacklisted.
*
* @param string $resource Attribute value (URL)
*
* @return bool
*/
public function isBlacklistedMedia($resource)
{
foreach ($this->media_blacklist as $name) {
if (strpos($resource, $name) !== false) {
return true;
}
}
return false;
}
/**
* Convert the attribute list to html.
*
* @param array $attributes Attributes
*
* @return string
*/
public function toHtml(array $attributes)
{
$html = array();
foreach ($attributes as $attribute => $value) {
$html[] = sprintf('%s="%s"', $attribute, Filter::escape($value));
}
return implode(' ', $html);
}
/**
* Set whitelisted tags and attributes for each tag.
*
* @param array $values List of tags: ['video' => ['src', 'cover'], 'img' => ['src']]
*
* @return Attribute
*/
public function setWhitelistedAttributes(array $values)
{
$this->attribute_whitelist = $values ?: $this->attribute_whitelist;
return $this;
}
/**
* Set scheme whitelist.
*
* @param array $values List of scheme: ['http://', 'ftp://']
*
* @return Attribute
*/
public function setSchemeWhitelist(array $values)
{
$this->scheme_whitelist = $values ?: $this->scheme_whitelist;
return $this;
}
/**
* Set media attributes (used to load external resources).
*
* @param array $values List of values: ['src', 'href']
*
* @return Attribute
*/
public function setMediaAttributes(array $values)
{
$this->media_attributes = $values ?: $this->media_attributes;
return $this;
}
/**
* Set blacklisted external resources.
*
* @param array $values List of tags: ['http://google.com/', '...']
*
* @return Attribute
*/
public function setMediaBlacklist(array $values)
{
$this->media_blacklist = $values ?: $this->media_blacklist;
return $this;
}
/**
* Set mandatory attributes for whitelisted tags.
*
* @param array $values List of tags: ['img' => 'src']
*
* @return Attribute
*/
public function setRequiredAttributes(array $values)
{
$this->required_attributes = $values ?: $this->required_attributes;
return $this;
}
/**
* Set attributes to automatically to specific tags.
*
* @param array $values List of tags: ['a' => 'target="_blank"']
*
* @return Attribute
*/
public function setAttributeOverrides(array $values)
{
$this->add_attributes = $values ?: $this->add_attributes;
return $this;
}
/**
* Set attributes that must be an integer.
*
* @param array $values List of tags: ['width', 'height']
*
* @return Attribute
*/
public function setIntegerAttributes(array $values)
{
$this->integer_attributes = $values ?: $this->integer_attributes;
return $this;
}
/**
* Set allowed iframe resources.
*
* @param array $values List of tags: ['http://www.youtube.com']
*
* @return Attribute
*/
public function setIframeWhitelist(array $values)
{
$this->iframe_whitelist = $values ?: $this->iframe_whitelist;
return $this;
}
/**
* Set image proxy URL.
*
* The original image url will be urlencoded
*
* @param string $url Proxy URL
*
* @return Attribute
*/
public function setImageProxyUrl($url)
{
$this->image_proxy_url = $url ?: $this->image_proxy_url;
return $this;
}
/**
* Set image proxy callback.
*
* @param \Closure $callback
*
* @return Attribute
*/
public function setImageProxyCallback($callback)
{
$this->image_proxy_callback = $callback ?: $this->image_proxy_callback;
return $this;
}
/**
* Set image proxy protocol restriction.
*
* @param string $value
*
* @return Attribute
*/
public function setImageProxyProtocol($value)
{
$this->image_proxy_limit_protocol = $value ?: $this->image_proxy_limit_protocol;
return $this;
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace PicoFeed\Filter;
/**
* Filter class.
*
* @author Frederic Guillot
*/
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)
{
$filter = new Html($html, $website);
return $filter;
}
/**
* Escape HTML content.
*
* @static
* @param string $content
* @return string
*/
public static function escape($content)
{
return htmlspecialchars($content, ENT_QUOTES, 'UTF-8', false);
}
/**
* Remove HTML tags.
*
* @param string $data Input data
* @return string
*/
public function removeHTMLTags($data)
{
return preg_replace('~<(?:!DOCTYPE|/?(?:html|head|body))[^>]*>\s*~i', '', $data);
}
/**
* Remove the XML tag from a document.
*
* @static
* @param string $data Input data
* @return string
*/
public static function stripXmlTag($data)
{
if (strpos($data, '<?xml') !== false) {
$data = ltrim(substr($data, strpos($data, '?>') + 2));
}
do {
$pos = strpos($data, '<?xml-stylesheet ');
if ($pos !== false) {
$data = ltrim(substr($data, strpos($data, '?>') + 2));
}
} while ($pos !== false && $pos < 200);
return $data;
}
/**
* Strip head tag from the HTML content.
*
* @static
* @param string $data Input data
* @return string
*/
public static function stripHeadTags($data)
{
return preg_replace('@<head[^>]*?>.*?</head>@siu', '', $data);
}
/**
* 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)
{
$value = str_replace("\r", ' ', $value);
$value = str_replace("\t", ' ', $value);
$value = str_replace("\n", ' ', $value);
// $value = preg_replace('/\s+/', ' ', $value); <= break utf-8
return trim($value);
}
/**
* Fixes before XML parsing.
*
* @static
* @param string $data Raw data
* @return string Normalized data
*/
public static function normalizeData($data)
{
$entities = array(
'/(&#)(\d+);/m', // decimal encoded
'/(&#x)([a-f0-9]+);/mi', // hex encoded
);
// strip invalid XML 1.0 characters which are encoded as entities
$data = preg_replace_callback($entities, function ($matches) {
$code_point = $matches[2];
// convert hex entity to decimal
if (strtolower($matches[1]) === '&#x') {
$code_point = hexdec($code_point);
}
$code_point = (int) $code_point;
// replace invalid characters
if ($code_point < 9
|| ($code_point > 10 && $code_point < 13)
|| ($code_point > 13 && $code_point < 32)
|| ($code_point > 55295 && $code_point < 57344)
|| ($code_point > 65533 && $code_point < 65536)
|| $code_point > 1114111
) {
return '';
};
return $matches[0];
}, $data);
// strip every utf-8 character than isn't in the range of valid XML 1.0 characters
return (string) preg_replace('/[^\x{0009}\x{000A}\x{000D}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', '', $data);
}
}

View File

@@ -0,0 +1,243 @@
<?php
namespace PicoFeed\Filter;
use PicoFeed\Config\Config;
use PicoFeed\Client\Url;
use PicoFeed\Scraper\RuleLoader;
use PicoFeed\Parser\XmlParser;
/**
* HTML Filter class.
*
* @author Frederic Guillot
*/
class Html
{
/**
* Config object.
*
* @var \PicoFeed\Config\Config
*/
private $config;
/**
* Unfiltered XML data.
*
* @var string
*/
private $input = '';
/**
* Filtered XML data.
*
* @var string
*/
private $output = '';
/**
* List of empty tags.
*
* @var array
*/
private $empty_tags = array();
/**
* Empty flag.
*
* @var bool
*/
private $empty = true;
/**
* Tag instance.
*
* @var \PicoFeed\Filter\Tag
*/
public $tag = '';
/**
* Attribute instance.
*
* @var \PicoFeed\Filter\Attribute
*/
public $attribute = '';
/**
* The website to filter.
*
* @var string
*/
private $website;
/**
* Initialize the filter, all inputs data must be encoded in UTF-8 before.
*
* @param string $html HTML content
* @param string $website Site URL (used to build absolute URL)
*/
public function __construct($html, $website)
{
$this->config = new Config();
$this->input = XmlParser::htmlToXml($html);
$this->output = '';
$this->tag = new Tag($this->config);
$this->website = $website;
$this->attribute = new Attribute(new Url($website));
}
/**
* Set config object.
*
* @param \PicoFeed\Config\Config $config Config instance
* @return \PicoFeed\Filter\Html
*/
public function setConfig($config)
{
$this->config = $config;
if ($this->config !== null) {
$this->attribute->setImageProxyCallback($this->config->getFilterImageProxyCallback());
$this->attribute->setImageProxyUrl($this->config->getFilterImageProxyUrl());
$this->attribute->setImageProxyProtocol($this->config->getFilterImageProxyProtocol());
$this->attribute->setIframeWhitelist($this->config->getFilterIframeWhitelist(array()));
$this->attribute->setIntegerAttributes($this->config->getFilterIntegerAttributes(array()));
$this->attribute->setAttributeOverrides($this->config->getFilterAttributeOverrides(array()));
$this->attribute->setRequiredAttributes($this->config->getFilterRequiredAttributes(array()));
$this->attribute->setMediaBlacklist($this->config->getFilterMediaBlacklist(array()));
$this->attribute->setMediaAttributes($this->config->getFilterMediaAttributes(array()));
$this->attribute->setSchemeWhitelist($this->config->getFilterSchemeWhitelist(array()));
$this->attribute->setWhitelistedAttributes($this->config->getFilterWhitelistedTags(array()));
$this->tag->setWhitelistedTags(array_keys($this->config->getFilterWhitelistedTags(array())));
}
return $this;
}
/**
* Run tags/attributes filtering.
*
* @return string
*/
public function execute()
{
$this->preFilter();
$parser = xml_parser_create();
xml_set_object($parser, $this);
xml_set_element_handler($parser, 'startTag', 'endTag');
xml_set_character_data_handler($parser, 'dataTag');
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
xml_parse($parser, $this->input, true);
xml_parser_free($parser);
$this->postFilter();
return $this->output;
}
/**
* Called before XML parsing.
*/
public function preFilter()
{
$this->input = $this->tag->removeBlacklistedTags($this->input);
}
/**
* Called after XML parsing.
*/
public function postFilter()
{
$this->output = $this->tag->removeEmptyTags($this->output);
$this->output = $this->filterRules($this->output);
$this->output = $this->tag->removeMultipleBreakTags($this->output);
$this->output = trim($this->output);
}
/**
* Called after XML parsing.
*
* @param string $content
* @return string
*/
public function filterRules($content)
{
// the constructor should require a config, then this if can be removed
if ($this->config === null) {
$config = new Config();
} else {
$config = $this->config;
}
$loader = new RuleLoader($config);
$rules = $loader->getRules($this->website);
$url = new Url($this->website);
$sub_url = $url->getFullPath();
if (isset($rules['filter'])) {
foreach ($rules['filter'] as $pattern => $rule) {
if (preg_match($pattern, $sub_url)) {
foreach ($rule as $search => $replace) {
$content = preg_replace($search, $replace, $content);
}
}
}
}
return $content;
}
/**
* Parse opening tag.
*
* @param resource $parser XML parser
* @param string $tag Tag name
* @param array $attributes Tag attributes
*/
public function startTag($parser, $tag, array $attributes)
{
$this->empty = true;
if ($this->tag->isAllowed($tag, $attributes)) {
$attributes = $this->attribute->filter($tag, $attributes);
if ($this->attribute->hasRequiredAttributes($tag, $attributes)) {
$attributes = $this->attribute->addAttributes($tag, $attributes);
$this->output .= $this->tag->openHtmlTag($tag, $this->attribute->toHtml($attributes));
$this->empty = false;
}
}
$this->empty_tags[] = $this->empty;
}
/**
* Parse closing tag.
*
* @param resource $parser XML parser
* @param string $tag Tag name
*/
public function endTag($parser, $tag)
{
if (!array_pop($this->empty_tags) && $this->tag->isAllowedTag($tag)) {
$this->output .= $this->tag->closeHtmlTag($tag);
}
}
/**
* Parse tag content.
*
* @param resource $parser XML parser
* @param string $content Tag content
*/
public function dataTag($parser, $content)
{
// Replace &nbsp; with normal space
$content = str_replace("\xc2\xa0", ' ', $content);
$this->output .= Filter::escape($content);
}
}

View File

@@ -0,0 +1,208 @@
<?php
namespace PicoFeed\Filter;
use DOMXPath;
use PicoFeed\Base;
use PicoFeed\Parser\XmlParser;
/**
* Tag Filter class.
*
* @author Frederic Guillot
*/
class Tag extends Base
{
/**
* Tags blacklist (Xpath expressions).
*
* @var array
*/
private $tag_blacklist = array(
'//script',
'//style',
);
/**
* Tags whitelist.
*
* @var array
*/
private $tag_whitelist = array(
'audio',
'video',
'source',
'dt',
'dd',
'dl',
'table',
'caption',
'tr',
'th',
'td',
'tbody',
'thead',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'strong',
'em',
'code',
'pre',
'blockquote',
'p',
'ul',
'li',
'ol',
'br',
'del',
'a',
'img',
'figure',
'figcaption',
'cite',
'time',
'abbr',
'iframe',
'q',
'sup',
'sub',
);
/**
* Check if the tag is allowed and is not a pixel tracker.
*
* @param string $tag Tag name
* @param array $attributes Attributes dictionary
* @return bool
*/
public function isAllowed($tag, array $attributes)
{
return $this->isAllowedTag($tag) && !$this->isPixelTracker($tag, $attributes);
}
/**
* Return the HTML opening tag.
*
* @param string $tag Tag name
* @param string $attributes Attributes converted in html
* @return string
*/
public function openHtmlTag($tag, $attributes = '')
{
return '<'.$tag.(empty($attributes) ? '' : ' '.$attributes).($this->isSelfClosingTag($tag) ? '/>' : '>');
}
/**
* Return the HTML closing tag.
*
* @param string $tag Tag name
* @return string
*/
public function closeHtmlTag($tag)
{
return $this->isSelfClosingTag($tag) ? '' : '</'.$tag.'>';
}
/**
* Return true is the tag is self-closing.
*
* @param string $tag Tag name
* @return bool
*/
public function isSelfClosingTag($tag)
{
return $tag === 'br' || $tag === 'img';
}
/**
* Check if a tag is on the whitelist.
*
* @param string $tag Tag name
* @return bool
*/
public function isAllowedTag($tag)
{
return in_array($tag, array_merge(
$this->tag_whitelist,
array_keys($this->config->getFilterWhitelistedTags(array()))
));
}
/**
* Detect if an image tag is a pixel tracker.
*
* @param string $tag Tag name
* @param array $attributes Tag attributes
* @return bool
*/
public function isPixelTracker($tag, array $attributes)
{
return $tag === 'img' &&
isset($attributes['height']) && isset($attributes['width']) &&
$attributes['height'] == 1 && $attributes['width'] == 1;
}
/**
* Remove script tags.
*
* @param string $data Input data
* @return string
*/
public function removeBlacklistedTags($data)
{
$dom = XmlParser::getDomDocument($data);
if ($dom === false) {
return '';
}
$xpath = new DOMXpath($dom);
$nodes = $xpath->query(implode(' | ', $this->tag_blacklist));
foreach ($nodes as $node) {
$node->parentNode->removeChild($node);
}
return $dom->saveXML();
}
/**
* Remove empty tags.
*
* @param string $data Input data
* @return string
*/
public function removeEmptyTags($data)
{
return preg_replace('/<([^<\/>]*)>([\s]*?|(?R))<\/\1>/imsU', '', $data);
}
/**
* Replace <br/><br/> by only one.
*
* @param string $data Input data
* @return string
*/
public function removeMultipleBreakTags($data)
{
return preg_replace("/(<br\s*\/?>\s*)+/", '<br/>', $data);
}
/**
* 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)
{
$this->tag_whitelist = $values ?: $this->tag_whitelist;
return $this;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace PicoFeed\Generator;
use PicoFeed\Parser\Item;
/**
* Content Generator Interface
*
* @package PicoFeed\Generator
* @author Frederic Guillot
*/
interface ContentGeneratorInterface
{
/**
* Execute Content Generator
*
* @access public
* @param Item $item
* @return boolean
*/
public function execute(Item $item);
}

View File

@@ -0,0 +1,36 @@
<?php
namespace PicoFeed\Generator;
use PicoFeed\Base;
use PicoFeed\Parser\Item;
/**
* File Content Generator
*
* @package PicoFeed\Generator
* @author Frederic Guillot
*/
class FileContentGenerator extends Base implements ContentGeneratorInterface
{
private $extensions = array('pdf');
/**
* Execute Content Generator
*
* @access public
* @param Item $item
* @return boolean
*/
public function execute(Item $item)
{
foreach ($this->extensions as $extension) {
if (substr($item->getUrl(), - strlen($extension)) === $extension) {
$item->setContent('<a href="'.$item->getUrl().'" target="_blank">'.$item->getUrl().'</a>');
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace PicoFeed\Generator;
use PicoFeed\Base;
use PicoFeed\Parser\Item;
/**
* Youtube Content Generator
*
* @package PicoFeed\Generator
* @author Frederic Guillot
*/
class YoutubeContentGenerator extends Base implements ContentGeneratorInterface
{
/**
* Execute Content Generator
*
* @access public
* @param Item $item
* @return boolean
*/
public function execute(Item $item)
{
if ($item->hasNamespace('yt')) {
return $this->generateHtmlFromXml($item);
}
return $this->generateHtmlFromUrl($item);
}
/**
* Generate HTML
*
* @access public
* @param Item $item
* @return boolean
*/
private function generateHtmlFromXml(Item $item)
{
$videoId = $item->getTag('yt:videoId');
if (! empty($videoId)) {
$item->setContent('<iframe width="560" height="315" src="//www.youtube.com/embed/'.$videoId[0].'" frameborder="0"></iframe>');
return true;
}
return false;
}
/**
* Generate HTML from item URL
*
* @access public
* @param Item $item
* @return bool
*/
public function generateHtmlFromUrl(Item $item)
{
if (preg_match('/youtube\.com\/watch\?v=(.*)/', $item->getUrl(), $matches)) {
$item->setContent('<iframe width="560" height="315" src="//www.youtube.com/embed/'.$matches[1].'" frameborder="0"></iframe>');
return true;
}
return false;
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace PicoFeed\Logging;
use DateTime;
use DateTimeZone;
/**
* Logging class.
*
* @author Frederic Guillot
*/
class Logger
{
/**
* List of messages.
*
* @static
*
* @var array
*/
private static $messages = array();
/**
* Default timezone.
*
* @static
*
* @var string
*/
private static $timezone = 'UTC';
/**
* Enable or disable logging.
*
* @static
*
* @var bool
*/
public static $enable = false;
/**
* Enable logging.
*
* @static
*/
public static function enable()
{
self::$enable = true;
}
/**
* Add a new message.
*
* @static
*
* @param string $message Message
*/
public static function setMessage($message)
{
if (self::$enable) {
$date = new DateTime('now', new DateTimeZone(self::$timezone));
self::$messages[] = '['.$date->format('Y-m-d H:i:s').'] '.$message;
}
}
/**
* Get all logged messages.
*
* @static
*
* @return array
*/
public static function getMessages()
{
return self::$messages;
}
/**
* Remove all logged messages.
*
* @static
*/
public static function deleteMessages()
{
self::$messages = array();
}
/**
* Set a different timezone.
*
* @static
*
* @see http://php.net/manual/en/timezones.php
*
* @param string $timezone Timezone
*/
public static function setTimeZone($timezone)
{
self::$timezone = $timezone ?: self::$timezone;
}
/**
* Get all messages serialized into a string.
*
* @static
*
* @return string
*/
public static function toString()
{
return implode(PHP_EOL, self::$messages).PHP_EOL;
}
}

View File

@@ -0,0 +1,395 @@
<?php
namespace PicoFeed\Parser;
use SimpleXMLElement;
use PicoFeed\Filter\Filter;
use PicoFeed\Client\Url;
/**
* Atom parser.
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
class Atom extends Parser
{
/**
* Supported namespaces.
*/
protected $namespaces = array(
'atom' => 'http://www.w3.org/2005/Atom',
);
/**
* Get the path to the items XML tree.
*
* @param SimpleXMLElement $xml Feed xml
* @return SimpleXMLElement[]
*/
public function getItemsTree(SimpleXMLElement $xml)
{
return XmlParser::getXPathResult($xml, 'atom:entry', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'entry');
}
/**
* Find the feed url.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedUrl(SimpleXMLElement $xml, Feed $feed)
{
$feed->setFeedUrl($this->getUrl($xml, 'self'));
}
/**
* Find the site url.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findSiteUrl(SimpleXMLElement $xml, Feed $feed)
{
$feed->setSiteUrl($this->getUrl($xml, 'alternate', true));
}
/**
* Find the feed description.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedDescription(SimpleXMLElement $xml, Feed $feed)
{
$description = XmlParser::getXPathResult($xml, 'atom:subtitle', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'subtitle');
$feed->setDescription(XmlParser::getValue($description));
}
/**
* Find the feed logo url.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedLogo(SimpleXMLElement $xml, Feed $feed)
{
$logo = XmlParser::getXPathResult($xml, 'atom:logo', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'logo');
$feed->setLogo(XmlParser::getValue($logo));
}
/**
* Find the feed icon.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedIcon(SimpleXMLElement $xml, Feed $feed)
{
$icon = XmlParser::getXPathResult($xml, 'atom:icon', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'icon');
$feed->setIcon(XmlParser::getValue($icon));
}
/**
* Find the feed title.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedTitle(SimpleXMLElement $xml, Feed $feed)
{
$title = XmlParser::getXPathResult($xml, 'atom:title', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'title');
$feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl());
}
/**
* Find the feed language.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed)
{
$language = XmlParser::getXPathResult($xml, '*[not(self::atom:entry)]/@xml:lang', $this->namespaces)
?: XmlParser::getXPathResult($xml, '@xml:lang');
$feed->setLanguage(XmlParser::getValue($language));
}
/**
* Find the feed id.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedId(SimpleXMLElement $xml, Feed $feed)
{
$id = XmlParser::getXPathResult($xml, 'atom:id', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'id');
$feed->setId(XmlParser::getValue($id));
}
/**
* Find the feed date.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedDate(SimpleXMLElement $xml, Feed $feed)
{
$updated = XmlParser::getXPathResult($xml, 'atom:updated', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'updated');
$feed->setDate($this->getDateParser()->getDateTime(XmlParser::getValue($updated)));
}
/**
* Find the item published date.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemPublishedDate(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$date = XmlParser::getXPathResult($entry, 'atom:published', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'published');
$item->setPublishedDate(!empty($date) ? $this->getDateParser()->getDateTime((string) current($date)) : null);
}
/**
* Find the item updated date.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemUpdatedDate(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$date = XmlParser::getXPathResult($entry, 'atom:updated', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'updated');
$item->setUpdatedDate(!empty($date) ? $this->getDateParser()->getDateTime((string) current($date)) : null);
}
/**
* Find the item title.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
*/
public function findItemTitle(SimpleXMLElement $entry, Item $item)
{
$title = XmlParser::getXPathResult($entry, 'atom:title', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'title');
$item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $item->getUrl());
}
/**
* Find the item author.
*
* @param SimpleXMLElement $xml Feed
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item)
{
$author = XmlParser::getXPathResult($entry, 'atom:author/atom:name', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'author/name')
?: XmlParser::getXPathResult($xml, 'atom:author/atom:name', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'author/name');
$item->setAuthor(XmlParser::getValue($author));
}
/**
* Find the item author URL.
*
* @param SimpleXMLElement $xml Feed
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemAuthorUrl(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item)
{
$authorUrl = XmlParser::getXPathResult($entry, 'atom:author/atom:uri', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'author/uri')
?: XmlParser::getXPathResult($xml, 'atom:author/atom:uri', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'author/uri');
$item->setAuthorUrl(XmlParser::getValue($authorUrl));
}
/**
* Find the item content.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemContent(SimpleXMLElement $entry, Item $item)
{
$item->setContent($this->getContent($entry));
}
/**
* Find the item URL.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemUrl(SimpleXMLElement $entry, Item $item)
{
$item->setUrl($this->getUrl($entry, 'alternate', true));
}
/**
* Genereate the item id.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$id = XmlParser::getXPathResult($entry, 'atom:id', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'id');
if (!empty($id)) {
$item->setId($this->generateId(XmlParser::getValue($id)));
} else {
$item->setId($this->generateId(
$item->getTitle(), $item->getUrl(), $item->getContent()
));
}
}
/**
* Find the item enclosure.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$enclosure = $this->findLink($entry, 'enclosure');
if ($enclosure) {
$item->setEnclosureUrl(Url::resolve((string) $enclosure['href'], $feed->getSiteUrl()));
$item->setEnclosureType((string) $enclosure['type']);
}
}
/**
* Find the item language.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$language = XmlParser::getXPathResult($entry, './/@xml:lang');
$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)
{
$link = $this->findLink($xml, $rel);
if ($link) {
return (string) $link['href'];
}
if ($fallback) {
$link = $this->findLink($xml, '');
return $link ? (string) $link['href'] : '';
}
return '';
}
/**
* Get a link tag that match a relationship.
*
* @param SimpleXMLElement $xml XML tag
* @param string $rel Link relationship: alternate, enclosure, related, self, via
* @return SimpleXMLElement|null
*/
private function findLink(SimpleXMLElement $xml, $rel)
{
$links = XmlParser::getXPathResult($xml, 'atom:link', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'link');
foreach ($links as $link) {
if ($rel === (string) $link['rel']) {
return $link;
}
}
return null;
}
/**
* Get the entry content.
*
* @param SimpleXMLElement $entry XML Entry
* @return string
*/
private function getContent(SimpleXMLElement $entry)
{
$content = current(
XmlParser::getXPathResult($entry, 'atom:content', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'content')
);
if (!empty($content) && count($content->children())) {
$xml_string = '';
foreach ($content->children() as $child) {
$xml_string .= $child->asXML();
}
return $xml_string;
} elseif (trim((string) $content) !== '') {
return (string) $content;
}
$summary = XmlParser::getXPathResult($entry, 'atom:summary', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'summary');
return (string) current($summary);
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace PicoFeed\Parser;
use DateTime;
use DateTimeZone;
use PicoFeed\Base;
/**
* Date Parser.
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
class DateParser extends Base
{
/**
* Timezone used to parse feed dates.
*
* @access private
* @var string
*/
private $timezone = 'UTC';
/**
* Supported formats [ 'format' => length ].
*
* @var array
*/
public $formats = array(
DATE_ATOM => null,
DATE_RSS => null,
DATE_COOKIE => null,
DATE_ISO8601 => null,
DATE_RFC822 => null,
DATE_RFC850 => null,
DATE_RFC1036 => null,
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,
'j M Y H:i:s' => 20,
'Y-m-d H:i:s' => 19,
'Y-m-d\TH:i:s' => 19,
'd/m/Y H:i:s' => 19,
'D, d M Y' => 16,
'Y-m-d' => 10,
'd-m-Y' => 10,
'm-d-Y' => 10,
'd.m.Y' => 10,
'm.d.Y' => 10,
'd/m/Y' => 10,
'm/d/Y' => 10,
);
/**
* Try to parse all date format for broken feeds.
*
* @param string $value Original date format
*
* @return DateTime
*/
public function getDateTime($value)
{
$value = trim($value);
foreach ($this->formats as $format => $length) {
$truncated_value = $value;
if ($length !== null) {
$truncated_value = substr($truncated_value, 0, $length);
}
$date = $this->getValidDate($format, $truncated_value);
if ($date !== false) {
return $date;
}
}
return $this->getCurrentDateTime();
}
/**
* Get a valid date from a given format.
*
* @param string $format Date format
* @param string $value Original date value
*
* @return DateTime|bool
*/
public function getValidDate($format, $value)
{
$date = DateTime::createFromFormat($format, $value, $this->getTimeZone());
if ($date !== false) {
$errors = DateTime::getLastErrors();
if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) {
return $date;
}
}
return false;
}
/**
* Get the current datetime.
*
* @return DateTime
*/
public function getCurrentDateTime()
{
return new DateTime('now', $this->getTimeZone());
}
/**
* Get DateTimeZone instance
*
* @access public
* @return DateTimeZone
*/
public function getTimeZone()
{
return new DateTimeZone($this->config->getTimezone() ?: $this->timezone);
}
}

View File

@@ -0,0 +1,315 @@
<?php
namespace PicoFeed\Parser;
/**
* Feed.
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
class Feed
{
/**
* Feed items.
*
* @var Item[]
*/
public $items = array();
/**
* Feed id.
*
* @var string
*/
public $id = '';
/**
* Feed title.
*
* @var string
*/
public $title = '';
/**
* Feed description.
*
* @var string
*/
public $description = '';
/**
* Feed url.
*
* @var string
*/
public $feedUrl = '';
/**
* Site url.
*
* @var string
*/
public $siteUrl = '';
/**
* Feed date.
*
* @var \DateTime
*/
public $date = null;
/**
* Feed language.
*
* @var string
*/
public $language = '';
/**
* Feed logo URL.
*
* @var string
*/
public $logo = '';
/**
* Feed icon URL.
*
* @var string
*/
public $icon = '';
/**
* Return feed information.
*/
public function __toString()
{
$output = '';
foreach (array('id', 'title', 'feedUrl', 'siteUrl', 'language', 'description', 'logo') as $property) {
$output .= 'Feed::'.$property.' = '.$this->$property.PHP_EOL;
}
$output .= 'Feed::date = '.$this->date->format(DATE_RFC822).PHP_EOL;
$output .= 'Feed::isRTL() = '.($this->isRTL() ? 'true' : 'false').PHP_EOL;
$output .= 'Feed::items = '.count($this->items).' items'.PHP_EOL;
foreach ($this->items as $item) {
$output .= '----'.PHP_EOL;
$output .= $item;
}
return $output;
}
/**
* Get title.
*/
public function getTitle()
{
return $this->title;
}
/**
* Get description.
*/
public function getDescription()
{
return $this->description;
}
/**
* Get the logo url.
*/
public function getLogo()
{
return $this->logo;
}
/**
* Get the icon url.
*/
public function getIcon()
{
return $this->icon;
}
/**
* Get feed url.
*/
public function getFeedUrl()
{
return $this->feedUrl;
}
/**
* Get site url.
*/
public function getSiteUrl()
{
return $this->siteUrl;
}
/**
* Get date.
*/
public function getDate()
{
return $this->date;
}
/**
* Get language.
*/
public function getLanguage()
{
return $this->language;
}
/**
* Get id.
*/
public function getId()
{
return $this->id;
}
/**
* Get feed items.
*/
public function getItems()
{
return $this->items;
}
/**
* Return true if the feed is "Right to Left".
*
* @return bool
*/
public function isRTL()
{
return Parser::isLanguageRTL($this->language);
}
/**
* Set feed items.
*
* @param Item[] $items
* @return Feed
*/
public function setItems(array $items)
{
$this->items = $items;
return $this;
}
/**
* Set feed id.
*
* @param string $id
* @return Feed
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Set feed title.
*
* @param string $title
* @return Feed
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Set feed description.
*
* @param string $description
* @return Feed
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Set feed url.
*
* @param string $feedUrl
* @return Feed
*/
public function setFeedUrl($feedUrl)
{
$this->feedUrl = $feedUrl;
return $this;
}
/**
* Set feed website url.
*
* @param string $siteUrl
* @return Feed
*/
public function setSiteUrl($siteUrl)
{
$this->siteUrl = $siteUrl;
return $this;
}
/**
* Set feed date.
*
* @param \DateTime $date
* @return Feed
*/
public function setDate($date)
{
$this->date = $date;
return $this;
}
/**
* Set feed language.
*
* @param string $language
* @return Feed
*/
public function setLanguage($language)
{
$this->language = $language;
return $this;
}
/**
* Set feed logo.
*
* @param string $logo
* @return Feed
*/
public function setLogo($logo)
{
$this->logo = $logo;
return $this;
}
/**
* Set feed icon.
*
* @param string $icon
* @return Feed
*/
public function setIcon($icon)
{
$this->icon = $icon;
return $this;
}
}

View File

@@ -0,0 +1,562 @@
<?php
namespace PicoFeed\Parser;
/**
* Feed Item.
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
class Item
{
/**
* List of known RTL languages.
*
* @var string[]
*/
public $rtl = array(
'ar', // Arabic (ar-**)
'fa', // Farsi (fa-**)
'ur', // Urdu (ur-**)
'ps', // Pashtu (ps-**)
'syr', // Syriac (syr-**)
'dv', // Divehi (dv-**)
'he', // Hebrew (he-**)
'yi', // Yiddish (yi-**)
);
/**
* Item id.
*
* @var string
*/
public $id = '';
/**
* Item title.
*
* @var string
*/
public $title = '';
/**
* Item url.
*
* @var string
*/
public $url = '';
/**
* Item author.
*
* @var string
*/
public $author = '';
/**
* Item author URL.
*
* @var string
*/
public $authorUrl = '';
/**
* Item date.
*
* @var \DateTime
*/
public $date = null;
/**
* Item published date.
*
* @var \DateTime
*/
public $publishedDate = null;
/**
* Item updated date.
*
* @var \DateTime
*/
public $updatedDate = null;
/**
* Item content.
*
* @var string
*/
public $content = '';
/**
* Item enclosure url.
*
* @var string
*/
public $enclosureUrl = '';
/**
* Item enclusure type.
*
* @var string
*/
public $enclosureType = '';
/**
* Item language.
*
* @var string
*/
public $language = '';
/**
* Item categories.
*
* @var array
*/
public $categories = array();
/**
* Raw XML.
*
* @var \SimpleXMLElement
*/
public $xml;
/**
* List of namespaces.
*
* @var array
*/
public $namespaces = array();
/**
* Check if a XML namespace exists
*
* @access public
* @param string $namespace
* @return bool
*/
public function hasNamespace($namespace)
{
return array_key_exists($namespace, $this->namespaces);
}
/**
* Get specific XML tag or attribute value.
*
* @param string $tag Tag name (examples: guid, media:content)
* @param string $attribute Tag attribute
*
* @return array|false Tag values or error
*/
public function getTag($tag, $attribute = '')
{
if ($attribute !== '') {
$attribute = '/@'.$attribute;
}
$query = './/'.$tag.$attribute;
$elements = XmlParser::getXPathResult($this->xml, $query, $this->namespaces);
if ($elements === false) { // xPath error
return false;
}
return array_map(function ($element) { return (string) $element;}, $elements);
}
/**
* Return item information.
*
* @return string
*/
public function __toString()
{
$output = '';
foreach (array('id', 'title', 'url', 'language', 'author', 'enclosureUrl', 'enclosureType') as $property) {
$output .= 'Item::'.$property.' = '.$this->$property.PHP_EOL;
}
$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;
}
/**
* Get title.
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Get URL
*
* @access public
* @return string
*/
public function getUrl()
{
return $this->url;
}
/**
* Set URL
*
* @access public
* @param string $url
* @return Item
*/
public function setUrl($url)
{
$this->url = $url;
return $this;
}
/**
* Get id.
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Get date.
*
* @return \DateTime
*/
public function getDate()
{
return $this->date;
}
/**
* Get published date.
*
* @return \DateTime
*/
public function getPublishedDate()
{
return $this->publishedDate;
}
/**
* Get updated date.
*
* @return \DateTime
*/
public function getUpdatedDate()
{
return $this->updatedDate;
}
/**
* Get content.
*
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* Set content
*
* @access public
* @param string $value
* @return Item
*/
public function setContent($value)
{
$this->content = $value;
return $this;
}
/**
* Get enclosure url.
*
* @return string
*/
public function getEnclosureUrl()
{
return $this->enclosureUrl;
}
/**
* Get enclosure type.
*
* @return string
*/
public function getEnclosureType()
{
return $this->enclosureType;
}
/**
* Get language.
*
* @return string
*/
public function getLanguage()
{
return $this->language;
}
/**
* Get categories.
*
* @return array
*/
public function getCategories()
{
return $this->categories;
}
/**
* Get author.
*
* @return string
*/
public function getAuthor()
{
return $this->author;
}
/**
* Get author URL.
*
* @return string
*/
public function getAuthorUrl()
{
return $this->authorUrl;
}
/**
* Return true if the item is "Right to Left".
*
* @return bool
*/
public function isRTL()
{
return Parser::isLanguageRTL($this->language);
}
/**
* Set item id.
*
* @param string $id
* @return Item
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Set item title.
*
* @param string $title
* @return Item
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Set author.
*
* @param string $author
* @return Item
*/
public function setAuthor($author)
{
$this->author = $author;
return $this;
}
/**
* Set author URL.
*
* @param string $authorUrl
* @return Item
*/
public function setAuthorUrl($authorUrl)
{
$this->authorUrl = $authorUrl;
return $this;
}
/**
* Set item date.
*
* @param \DateTime $date
* @return Item
*/
public function setDate($date)
{
$this->date = $date;
return $this;
}
/**
* Set item published date.
*
* @param \DateTime $publishedDate
* @return Item
*/
public function setPublishedDate($publishedDate)
{
$this->publishedDate = $publishedDate;
return $this;
}
/**
* Set item updated date.
*
* @param \DateTime $updatedDate
* @return Item
*/
public function setUpdatedDate($updatedDate)
{
$this->updatedDate = $updatedDate;
return $this;
}
/**
* Set enclosure url.
*
* @param string $enclosureUrl
* @return Item
*/
public function setEnclosureUrl($enclosureUrl)
{
$this->enclosureUrl = $enclosureUrl;
return $this;
}
/**
* Set enclosure type.
*
* @param string $enclosureType
* @return Item
*/
public function setEnclosureType($enclosureType)
{
$this->enclosureType = $enclosureType;
return $this;
}
/**
* Set item language.
*
* @param string $language
* @return Item
*/
public function setLanguage($language)
{
$this->language = $language;
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.
*
* @param \SimpleXMLElement $xml
* @return Item
*/
public function setXml($xml)
{
$this->xml = $xml;
return $this;
}
/**
* Get raw XML.
*
* @return \SimpleXMLElement
*/
public function getXml()
{
return $this->xml;
}
/**
* Set XML namespaces.
*
* @param array $namespaces
* @return Item
*/
public function setNamespaces($namespaces)
{
$this->namespaces = $namespaces;
return $this;
}
/**
* Get XML namespaces.
*
* @return array
*/
public function getNamespaces()
{
return $this->namespaces;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Parser;
/**
* MalformedXmlException Exception.
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
class MalformedXmlException extends ParserException
{
}

View File

@@ -0,0 +1,397 @@
<?php
namespace PicoFeed\Parser;
use PicoFeed\Processor\ContentFilterProcessor;
use PicoFeed\Processor\ContentGeneratorProcessor;
use PicoFeed\Processor\ItemPostProcessor;
use PicoFeed\Processor\ScraperProcessor;
use SimpleXMLElement;
use PicoFeed\Client\Url;
use PicoFeed\Encoding\Encoding;
use PicoFeed\Filter\Filter;
use PicoFeed\Logging\Logger;
/**
* Base parser class.
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
abstract class Parser implements ParserInterface
{
/**
* Config object.
*
* @var \PicoFeed\Config\Config
*/
private $config;
/**
* DateParser object.
*
* @var \PicoFeed\Parser\DateParser
*/
private $dateParser;
/**
* Hash algorithm used to generate item id, any value supported by PHP, see hash_algos().
*
* @var string
*/
private $hash_algo = 'sha256';
/**
* Feed content (XML data).
*
* @var string
*/
protected $content = '';
/**
* Fallback url.
*
* @var string
*/
protected $fallback_url = '';
/**
* XML namespaces supported by parser.
*
* @var array
*/
protected $namespaces = array();
/**
* XML namespaces used in document.
*
* @var array
*/
protected $used_namespaces = array();
/**
* Item Post Processor instance
*
* @access private
* @var ItemPostProcessor
*/
private $itemPostProcessor;
/**
* Constructor.
*
* @param string $content Feed content
* @param string $http_encoding HTTP encoding (headers)
* @param string $fallback_url Fallback url when the feed provide relative or broken url
*/
public function __construct($content, $http_encoding = '', $fallback_url = '')
{
$this->fallback_url = $fallback_url;
$xml_encoding = XmlParser::getEncodingFromXmlTag($content);
// Strip XML tag to avoid multiple encoding/decoding in the next XML processing
$this->content = Filter::stripXmlTag($content);
// Encode everything in UTF-8
Logger::setMessage(get_called_class().': HTTP Encoding "'.$http_encoding.'" ; XML Encoding "'.$xml_encoding.'"');
$this->content = Encoding::convert($this->content, $xml_encoding ?: $http_encoding);
$this->itemPostProcessor = new ItemPostProcessor($this->config);
$this->itemPostProcessor->register(new ContentGeneratorProcessor($this->config));
$this->itemPostProcessor->register(new ContentFilterProcessor($this->config));
}
/**
* Parse the document.
* @return Feed
* @throws MalformedXmlException
*/
public function execute()
{
Logger::setMessage(get_called_class().': begin parsing');
$xml = XmlParser::getSimpleXml($this->content);
if ($xml === false) {
Logger::setMessage(get_called_class().': Applying XML workarounds');
$this->content = Filter::normalizeData($this->content);
$xml = XmlParser::getSimpleXml($this->content);
if ($xml === false) {
Logger::setMessage(get_called_class().': XML parsing error');
Logger::setMessage(XmlParser::getErrors());
throw new MalformedXmlException('XML parsing error');
}
}
$this->used_namespaces = $xml->getNamespaces(true);
$xml = $this->registerSupportedNamespaces($xml);
$feed = new Feed();
$this->findFeedUrl($xml, $feed);
$this->checkFeedUrl($feed);
$this->findSiteUrl($xml, $feed);
$this->checkSiteUrl($feed);
$this->findFeedTitle($xml, $feed);
$this->findFeedDescription($xml, $feed);
$this->findFeedLanguage($xml, $feed);
$this->findFeedId($xml, $feed);
$this->findFeedDate($xml, $feed);
$this->findFeedLogo($xml, $feed);
$this->findFeedIcon($xml, $feed);
foreach ($this->getItemsTree($xml) as $entry) {
$entry = $this->registerSupportedNamespaces($entry);
$item = new Item();
$item->xml = $entry;
$item->namespaces = $this->used_namespaces;
$this->findItemAuthor($xml, $entry, $item);
$this->findItemAuthorUrl($xml, $entry, $item);
$this->findItemUrl($entry, $item);
$this->checkItemUrl($feed, $item);
$this->findItemTitle($entry, $item);
$this->findItemContent($entry, $item);
// Id generation can use the item url/title/content (order is important)
$this->findItemId($entry, $item, $feed);
$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;
}
Logger::setMessage(get_called_class().PHP_EOL.$feed);
return $feed;
}
/**
* Check if the feed url is correct.
*
* @param Feed $feed Feed object
*/
public function checkFeedUrl(Feed $feed)
{
if ($feed->getFeedUrl() === '') {
$feed->feedUrl = $this->fallback_url;
} else {
$feed->feedUrl = Url::resolve($feed->getFeedUrl(), $this->fallback_url);
}
}
/**
* Check if the site url is correct.
*
* @param Feed $feed Feed object
*/
public function checkSiteUrl(Feed $feed)
{
if ($feed->getSiteUrl() === '') {
$feed->siteUrl = Url::base($feed->getFeedUrl());
} else {
$feed->siteUrl = Url::resolve($feed->getSiteUrl(), $this->fallback_url);
}
}
/**
* Check if the item url is correct.
*
* @param Feed $feed Feed object
* @param Item $item Item object
*/
public function checkItemUrl(Feed $feed, Item $item)
{
$item->url = Url::resolve($item->getUrl(), $feed->getSiteUrl());
}
/**
* Find the item date.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemDate(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$this->findItemPublishedDate($entry, $item, $feed);
$this->findItemUpdatedDate($entry, $item, $feed);
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()));
}
/**
* Get Item Post Processor instance
*
* @access public
* @return ItemPostProcessor
*/
public function getItemPostProcessor()
{
return $this->itemPostProcessor;
}
/**
* Get DateParser instance
*
* @access public
* @return DateParser
*/
public function getDateParser()
{
if ($this->dateParser === null) {
$this->dateParser = new DateParser($this->config);
}
return $this->dateParser;
}
/**
* Generate a unique id for an entry (hash all arguments).
*
* @return string
*/
public function generateId()
{
return hash($this->hash_algo, implode(func_get_args()));
}
/**
* 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)
{
$language = strtolower($language);
$rtl_languages = array(
'ar', // Arabic (ar-**)
'fa', // Farsi (fa-**)
'ur', // Urdu (ur-**)
'ps', // Pashtu (ps-**)
'syr', // Syriac (syr-**)
'dv', // Divehi (dv-**)
'he', // Hebrew (he-**)
'yi', // Yiddish (yi-**)
);
foreach ($rtl_languages as $prefix) {
if (strpos($language, $prefix) === 0) {
return true;
}
}
return false;
}
/**
* Set Hash algorithm used for id generation.
*
* @param string $algo Algorithm name
* @return \PicoFeed\Parser\Parser
*/
public function setHashAlgo($algo)
{
$this->hash_algo = $algo ?: $this->hash_algo;
return $this;
}
/**
* 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;
}
/**
* Enable the content grabber.
*
* @return \PicoFeed\Parser\Parser
*/
public function disableContentFiltering()
{
$this->itemPostProcessor->unregister('PicoFeed\Processor\ContentFilterProcessor');
return $this;
}
/**
* Enable the content grabber.
*
* @param bool $needsRuleFile true if only pages with rule files should be
* 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)
{
$processor = new ScraperProcessor($this->config);
if ($needsRuleFile) {
$processor->getScraper()->disableCandidateParser();
}
if ($scraperCallback !== null) {
$processor->setExecutionCallback($scraperCallback);
}
$this->itemPostProcessor->register($processor);
return $this;
}
/**
* Set ignored URLs for the content grabber.
*
* @param array $urls URLs
* @return \PicoFeed\Parser\Parser
*/
public function setGrabberIgnoreUrls(array $urls)
{
$this->itemPostProcessor->getProcessor('PicoFeed\Processor\ScraperProcessor')->ignoreUrls($urls);
return $this;
}
/**
* Register all supported namespaces to be used within an xpath query.
*
* @param SimpleXMLElement $xml Feed xml
* @return SimpleXMLElement
*/
public function registerSupportedNamespaces(SimpleXMLElement $xml)
{
foreach ($this->namespaces as $prefix => $ns) {
$xml->registerXPathNamespace($prefix, $ns);
}
return $xml;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace PicoFeed\Parser;
use PicoFeed\PicoFeedException;
/**
* ParserException Exception.
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
abstract class ParserException extends PicoFeedException
{
}

View File

@@ -0,0 +1,191 @@
<?php
namespace PicoFeed\Parser;
use SimpleXMLElement;
/**
* Interface ParserInterface
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
interface ParserInterface
{
/**
* Find the feed url.
*
* @param SimpleXMLElement $xml Feed xml
* @param Feed $feed Feed object
*/
public function findFeedUrl(SimpleXMLElement $xml, Feed $feed);
/**
* Find the site url.
*
* @param SimpleXMLElement $xml Feed xml
* @param Feed $feed Feed object
*/
public function findSiteUrl(SimpleXMLElement $xml, Feed $feed);
/**
* Find the feed title.
*
* @param SimpleXMLElement $xml Feed xml
* @param Feed $feed Feed object
*/
public function findFeedTitle(SimpleXMLElement $xml, Feed $feed);
/**
* Find the feed description.
*
* @param SimpleXMLElement $xml Feed xml
* @param Feed $feed Feed object
*/
public function findFeedDescription(SimpleXMLElement $xml, Feed $feed);
/**
* Find the feed language.
*
* @param SimpleXMLElement $xml Feed xml
* @param Feed $feed Feed object
*/
public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed);
/**
* Find the feed id.
*
* @param SimpleXMLElement $xml Feed xml
* @param Feed $feed Feed object
*/
public function findFeedId(SimpleXMLElement $xml, Feed $feed);
/**
* Find the feed date.
*
* @param SimpleXMLElement $xml Feed xml
* @param Feed $feed Feed object
*/
public function findFeedDate(SimpleXMLElement $xml, Feed $feed);
/**
* Find the feed logo url.
*
* @param SimpleXMLElement $xml Feed xml
* @param Feed $feed Feed object
*/
public function findFeedLogo(SimpleXMLElement $xml, Feed $feed);
/**
* Find the feed icon.
*
* @param SimpleXMLElement $xml Feed xml
* @param Feed $feed Feed object
*/
public function findFeedIcon(SimpleXMLElement $xml, Feed $feed);
/**
* Get the path to the items XML tree.
*
* @param SimpleXMLElement $xml Feed xml
*
* @return SimpleXMLElement
*/
public function getItemsTree(SimpleXMLElement $xml);
/**
* Find the item author.
*
* @param SimpleXMLElement $xml Feed
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
*/
public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item);
/**
* Find the item author URL.
*
* @param SimpleXMLElement $xml Feed
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
*/
public function findItemAuthorUrl(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item);
/**
* Find the item URL.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
*/
public function findItemUrl(SimpleXMLElement $entry, Item $item);
/**
* Find the item title.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
*/
public function findItemTitle(SimpleXMLElement $entry, Item $item);
/**
* Genereate the item id.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param Feed $feed Feed object
*/
public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed);
/**
* Find the item published date.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param Feed $feed Feed object
*/
public function findItemPublishedDate(SimpleXMLElement $entry, Item $item, Feed $feed);
/**
* Find the item updated date.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param Feed $feed Feed object
*/
public function findItemUpdatedDate(SimpleXMLElement $entry, Item $item, Feed $feed);
/**
* Find the item content.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
*/
public function findItemContent(SimpleXMLElement $entry, Item $item);
/**
* Find the item enclosure.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param Feed $feed Feed object
*/
public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed);
/**
* Find the item language.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @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

@@ -0,0 +1,318 @@
<?php
namespace PicoFeed\Parser;
use SimpleXMLElement;
use PicoFeed\Filter\Filter;
/**
* RSS 1.0 parser.
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
class Rss10 extends Parser
{
/**
* Supported namespaces.
*/
protected $namespaces = array(
'rss' => 'http://purl.org/rss/1.0/',
'dc' => 'http://purl.org/dc/elements/1.1/',
'content' => 'http://purl.org/rss/1.0/modules/content/',
'feedburner' => 'http://rssnamespace.org/feedburner/ext/1.0',
);
/**
* Get the path to the items XML tree.
*
* @param SimpleXMLElement $xml Feed xml
* @return SimpleXMLElement[]
*/
public function getItemsTree(SimpleXMLElement $xml)
{
return XmlParser::getXPathResult($xml, 'rss:item', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'item')
?: $xml->item;
}
/**
* Find the feed url.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedUrl(SimpleXMLElement $xml, Feed $feed)
{
$feed->setFeedUrl('');
}
/**
* Find the site url.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findSiteUrl(SimpleXMLElement $xml, Feed $feed)
{
$value = XmlParser::getXPathResult($xml, 'rss:channel/rss:link', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/link')
?: $xml->channel->link;
$feed->setSiteUrl(XmlParser::getValue($value));
}
/**
* Find the feed description.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedDescription(SimpleXMLElement $xml, Feed $feed)
{
$description = XmlParser::getXPathResult($xml, 'rss:channel/rss:description', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/description')
?: $xml->channel->description;
$feed->setDescription(XmlParser::getValue($description));
}
/**
* Find the feed logo url.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedLogo(SimpleXMLElement $xml, Feed $feed)
{
$logo = XmlParser::getXPathResult($xml, 'rss:image/rss:url', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'image/url');
$feed->setLogo(XmlParser::getValue($logo));
}
/**
* Find the feed icon.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedIcon(SimpleXMLElement $xml, Feed $feed)
{
$feed->setIcon('');
}
/**
* Find the feed title.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedTitle(SimpleXMLElement $xml, Feed $feed)
{
$title = XmlParser::getXPathResult($xml, 'rss:channel/rss:title', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/title')
?: $xml->channel->title;
$feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl());
}
/**
* Find the feed language.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed)
{
$language = XmlParser::getXPathResult($xml, 'rss:channel/dc:language', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/dc:language', $this->namespaces);
$feed->setLanguage(XmlParser::getValue($language));
}
/**
* Find the feed id.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedId(SimpleXMLElement $xml, Feed $feed)
{
$feed->setId($feed->getFeedUrl() ?: $feed->getSiteUrl());
}
/**
* Find the feed date.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedDate(SimpleXMLElement $xml, Feed $feed)
{
$date = XmlParser::getXPathResult($xml, 'rss:channel/dc:date', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/dc:date', $this->namespaces);
$feed->setDate($this->getDateParser()->getDateTime(XmlParser::getValue($date)));
}
/**
* Find the item published date.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemPublishedDate(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$date = XmlParser::getXPathResult($entry, 'dc:date', $this->namespaces);
$item->setPublishedDate(!empty($date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($date)) : null);
}
/**
* Find the item updated date.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemUpdatedDate(SimpleXMLElement $entry, Item $item, Feed $feed)
{
if ($item->publishedDate === null) {
$this->findItemPublishedDate($entry, $item, $feed);
}
$item->setUpdatedDate($item->getPublishedDate()); // No updated date in RSS 1.0 specifications
}
/**
* Find the item title.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemTitle(SimpleXMLElement $entry, Item $item)
{
$title = XmlParser::getXPathResult($entry, 'rss:title', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'title')
?: $entry->title;
$item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $item->getUrl());
}
/**
* Find the item author.
*
* @param SimpleXMLElement $xml Feed
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item)
{
$author = XmlParser::getXPathResult($entry, 'dc:creator', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'rss:channel/dc:creator', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/dc:creator', $this->namespaces);
$item->setAuthor(XmlParser::getValue($author));
}
/**
* Find the item author URL.
*
* @param SimpleXMLElement $xml Feed
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemAuthorUrl(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item)
{
// There appears to be no support for author URL in the dc: terms
$item->setAuthorUrl('');
}
/**
* Find the item content.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemContent(SimpleXMLElement $entry, Item $item)
{
$content = XmlParser::getXPathResult($entry, 'content:encoded', $this->namespaces);
if (XmlParser::getValue($content) === '') {
$content = XmlParser::getXPathResult($entry, 'rss:description', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'description')
?: $entry->description;
}
$item->setContent(XmlParser::getValue($content));
}
/**
* Find the item URL.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemUrl(SimpleXMLElement $entry, Item $item)
{
$link = XmlParser::getXPathResult($entry, 'feedburner:origLink', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'rss:link', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'link')
?: $entry->link;
$item->setUrl(XmlParser::getValue($link));
}
/**
* Genereate the item id.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$item->setId($this->generateId(
$item->getTitle(), $item->getUrl(), $item->getContent()
));
}
/**
* Find the item enclosure.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed)
{
}
/**
* Find the item language.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$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, 'dc:subject', $this->namespaces);
$item->setCategoriesFromXml($categories);
}
}

View File

@@ -0,0 +1,330 @@
<?php
namespace PicoFeed\Parser;
use SimpleXMLElement;
use PicoFeed\Filter\Filter;
use PicoFeed\Client\Url;
/**
* RSS 2.0 Parser.
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
class Rss20 extends Parser
{
/**
* Supported namespaces.
*/
protected $namespaces = array(
'dc' => 'http://purl.org/dc/elements/1.1/',
'content' => 'http://purl.org/rss/1.0/modules/content/',
'feedburner' => 'http://rssnamespace.org/feedburner/ext/1.0',
'atom' => 'http://www.w3.org/2005/Atom',
);
/**
* Get the path to the items XML tree.
*
* @param SimpleXMLElement $xml Feed xml
* @return SimpleXMLElement[]
*/
public function getItemsTree(SimpleXMLElement $xml)
{
return XmlParser::getXPathResult($xml, 'channel/item');
}
/**
* Find the feed url.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedUrl(SimpleXMLElement $xml, Feed $feed)
{
$feed->setFeedUrl('');
}
/**
* Find the site url.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findSiteUrl(SimpleXMLElement $xml, Feed $feed)
{
$value = XmlParser::getXPathResult($xml, 'channel/link');
$feed->setSiteUrl(XmlParser::getValue($value));
}
/**
* Find the feed description.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedDescription(SimpleXMLElement $xml, Feed $feed)
{
$value = XmlParser::getXPathResult($xml, 'channel/description');
$feed->setDescription(XmlParser::getValue($value));
}
/**
* Find the feed logo url.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedLogo(SimpleXMLElement $xml, Feed $feed)
{
$value = XmlParser::getXPathResult($xml, 'channel/image/url');
$feed->setLogo(XmlParser::getValue($value));
}
/**
* Find the feed icon.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedIcon(SimpleXMLElement $xml, Feed $feed)
{
$feed->setIcon('');
}
/**
* Find the feed title.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedTitle(SimpleXMLElement $xml, Feed $feed)
{
$title = XmlParser::getXPathResult($xml, 'channel/title');
$feed->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($title)) ?: $feed->getSiteUrl());
}
/**
* Find the feed language.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedLanguage(SimpleXMLElement $xml, Feed $feed)
{
$value = XmlParser::getXPathResult($xml, 'channel/language');
$feed->setLanguage(XmlParser::getValue($value));
}
/**
* Find the feed id.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedId(SimpleXMLElement $xml, Feed $feed)
{
$feed->setId($feed->getFeedUrl() ?: $feed->getSiteUrl());
}
/**
* Find the feed date.
*
* @param SimpleXMLElement $xml Feed xml
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findFeedDate(SimpleXMLElement $xml, Feed $feed)
{
$publish_date = XmlParser::getXPathResult($xml, 'channel/pubDate');
$update_date = XmlParser::getXPathResult($xml, 'channel/lastBuildDate');
$published = !empty($publish_date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($publish_date)) : null;
$updated = !empty($update_date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($update_date)) : null;
if ($published === null && $updated === null) {
$feed->setDate($this->getDateParser()->getCurrentDateTime()); // We use the current date if there is no date for the feed
} elseif ($published !== null && $updated !== null) {
$feed->setDate(max($published, $updated)); // We use the most recent date between published and updated
} else {
$feed->setDate($updated ?: $published);
}
}
/**
* Find the item published date.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemPublishedDate(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$date = XmlParser::getXPathResult($entry, 'pubDate');
$item->setPublishedDate(!empty($date) ? $this->getDateParser()->getDateTime(XmlParser::getValue($date)) : null);
}
/**
* Find the item updated date.
*
* @param SimpleXMLElement $entry Feed item
* @param Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemUpdatedDate(SimpleXMLElement $entry, Item $item, Feed $feed)
{
if ($item->publishedDate === null) {
$this->findItemPublishedDate($entry, $item, $feed);
}
$item->setUpdatedDate($item->getPublishedDate()); // No updated date in RSS 2.0 specifications
}
/**
* Find the item title.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemTitle(SimpleXMLElement $entry, Item $item)
{
$value = XmlParser::getXPathResult($entry, 'title');
$item->setTitle(Filter::stripWhiteSpace(XmlParser::getValue($value)) ?: $item->getUrl());
}
/**
* Find the item author.
*
* @param SimpleXMLElement $xml Feed
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemAuthor(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item)
{
$value = XmlParser::getXPathResult($entry, 'dc:creator', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'author')
?: XmlParser::getXPathResult($xml, 'channel/dc:creator', $this->namespaces)
?: XmlParser::getXPathResult($xml, 'channel/managingEditor');
$item->setAuthor(XmlParser::getValue($value));
}
/**
* Find the item author URL.
*
* @param SimpleXMLElement $xml Feed
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemAuthorUrl(SimpleXMLElement $xml, SimpleXMLElement $entry, Item $item)
{
// There appears to be no support for author URL in the dc: terms or author element
$item->setAuthorUrl('');
}
/**
* Find the item content.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemContent(SimpleXMLElement $entry, Item $item)
{
$content = XmlParser::getXPathResult($entry, 'content:encoded', $this->namespaces);
if (XmlParser::getValue($content) === '') {
$content = XmlParser::getXPathResult($entry, 'description');
}
$item->setContent(XmlParser::getValue($content));
}
/**
* Find the item URL.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
*/
public function findItemUrl(SimpleXMLElement $entry, Item $item)
{
$link = XmlParser::getXPathResult($entry, 'feedburner:origLink', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'link')
?: XmlParser::getXPathResult($entry, 'atom:link/@href', $this->namespaces);
if (!empty($link)) {
$item->setUrl(XmlParser::getValue($link));
} else {
$link = XmlParser::getXPathResult($entry, 'guid');
$link = XmlParser::getValue($link);
if (filter_var($link, FILTER_VALIDATE_URL) !== false) {
$item->setUrl($link);
}
}
}
/**
* Genereate the item id.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemId(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$id = XmlParser::getValue(XmlParser::getXPathResult($entry, 'guid'));
if ($id) {
$item->setId($this->generateId($id));
} else {
$item->setId($this->generateId(
$item->getTitle(), $item->getUrl(), $item->getContent()
));
}
}
/**
* Find the item enclosure.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemEnclosure(SimpleXMLElement $entry, Item $item, Feed $feed)
{
if (isset($entry->enclosure)) {
$type = XmlParser::getXPathResult($entry, 'enclosure/@type');
$url = XmlParser::getXPathResult($entry, 'feedburner:origEnclosureLink', $this->namespaces)
?: XmlParser::getXPathResult($entry, 'enclosure/@url');
$item->setEnclosureUrl(Url::resolve(XmlParser::getValue($url), $feed->getSiteUrl()));
$item->setEnclosureType(XmlParser::getValue($type));
}
}
/**
* Find the item language.
*
* @param SimpleXMLElement $entry Feed item
* @param \PicoFeed\Parser\Item $item Item object
* @param \PicoFeed\Parser\Feed $feed Feed object
*/
public function findItemLanguage(SimpleXMLElement $entry, Item $item, Feed $feed)
{
$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

@@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Parser;
/**
* RSS 0.91 Parser.
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
class Rss91 extends Rss20
{
}

View File

@@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Parser;
/**
* RSS 0.92 Parser.
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
class Rss92 extends Rss20
{
}

View File

@@ -0,0 +1,13 @@
<?php
namespace PicoFeed\Parser;
/**
* XmlEntityException Exception.
*
* @package PicoFeed\Parser
* @author Bernhard Posselt
*/
class XmlEntityException extends MalformedXmlException
{
}

View File

@@ -0,0 +1,249 @@
<?php
namespace PicoFeed\Parser;
use DOMDocument;
use SimpleXMLElement;
use Laminas\Xml\Exception\RuntimeException;
use Laminas\Xml\Security;
/**
* XML parser class.
*
* Checks for XML eXternal Entity (XXE) and XML Entity Expansion (XEE) attacks on XML documents
*
* @package PicoFeed\Parser
* @author Frederic Guillot
*/
class XmlParser
{
protected static $errors = [];
/**
* Get a SimpleXmlElement instance or return false.
*
* @static
* @param string $input XML content
* @return mixed
*/
public static function getSimpleXml($input)
{
return self::scan($input);
}
/**
* Get a DomDocument instance or return false.
*
* @static
* @param string $input XML content
* @return DOMDocument|bool
*/
public static function getDomDocument($input)
{
if (empty($input)) {
return false;
}
$dom = self::scan($input, new DOMDocument());
// The document is empty, there is probably some parsing errors
if ($dom && $dom->childNodes->length === 0) {
return false;
}
return $dom;
}
/**
* Small wrapper around Laminas Xml to turn their exceptions into PicoFeed exceptions
*
* @static
* @access private
* @param string $input
* @param DOMDocument $dom
* @throws XmlEntityException
* @return SimpleXMLElement|DomDocument|boolean
*/
private static function scan($input, $dom = null)
{
try {
return Security::scan($input, $dom);
} catch(RuntimeException $e) {
throw new XmlEntityException($e->getMessage());
}
}
/**
* Load HTML document by using a DomDocument instance or return false on failure.
*
* @static
* @access public
* @param string $input XML content
* @return DOMDocument
*/
public static function getHtmlDocument($input)
{
$dom = new DomDocument();
if (empty($input)) {
return $dom;
}
libxml_use_internal_errors(true);
if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
$dom->loadHTML($input, LIBXML_NONET);
} else {
$dom->loadHTML($input);
}
self::$errors = [];
foreach (libxml_get_errors() as $error) {
self::$errors[] = sprintf('XML error: %s (Line: %d - Column: %d - Code: %d)',
$error->message,
$error->line,
$error->column,
$error->code
);
}
libxml_use_internal_errors(false);
return $dom;
}
/**
* Convert a HTML document to XML.
*
* @static
* @access public
* @param string $html HTML document
* @return string
*/
public static function htmlToXml($html)
{
$dom = self::getHtmlDocument('<?xml version="1.0" encoding="UTF-8">'.$html);
return $dom->saveXML($dom->getElementsByTagName('body')->item(0));
}
/**
* Get XML parser errors.
*
* @static
* @access public
* @return string
*/
public static function getErrors()
{
return implode(', ', self::$errors);
}
/**
* Get the encoding from a xml tag.
*
* @static
* @access public
* @param string $data Input data
* @return string
*/
public static function getEncodingFromXmlTag($data)
{
$encoding = '';
if (strpos($data, '<?xml') !== false) {
$data = substr($data, 0, strrpos($data, '?>'));
$data = str_replace("'", '"', $data);
$p1 = strpos($data, 'encoding=');
$p2 = strpos($data, '"', $p1 + 10);
if ($p1 !== false && $p2 !== false) {
$encoding = substr($data, $p1 + 10, $p2 - $p1 - 10);
$encoding = strtolower($encoding);
}
}
return $encoding;
}
/**
* Get the charset from a meta tag.
*
* @static
* @access public
* @param string $data Input data
* @return string
*/
public static function getEncodingFromMetaTag($data)
{
$encoding = '';
if (preg_match('/<meta.*?charset\s*=\s*["\']?\s*([^"\'\s\/>;]+)/i', $data, $match) === 1) {
$encoding = strtolower($match[1]);
}
return $encoding;
}
/**
* Rewrite XPath query to use namespace-uri and local-name derived from prefix.
*
* @static
* @access public
* @param string $query XPath query
* @param array $ns Prefix to namespace URI mapping
* @return string
*/
public static function replaceXPathPrefixWithNamespaceURI($query, array $ns)
{
return preg_replace_callback('/([A-Z0-9]+):([A-Z0-9]+)/iu', function ($matches) use ($ns) {
// don't try to map the special prefix XML
if (strtolower($matches[1]) === 'xml') {
return $matches[0];
}
return '*[namespace-uri()="'.$ns[$matches[1]].'" and local-name()="'.$matches[2].'"]';
},
$query);
}
/**
* Get the result elements of a XPath query.
*
* @static
* @access public
* @param SimpleXMLElement $xml XML element
* @param string $query XPath query
* @param array $ns Prefix to namespace URI mapping
* @return SimpleXMLElement[]
*/
public static function getXPathResult(SimpleXMLElement $xml, $query, array $ns = array())
{
if (!empty($ns)) {
$query = static::replaceXPathPrefixWithNamespaceURI($query, $ns);
}
return $xml->xpath($query);
}
/**
* Get the first Xpath result or SimpleXMLElement value
*
* @static
* @access public
* @param mixed $value
* @return string
*/
public static function getValue($value)
{
$result = '';
if (is_array($value) && count($value) > 0) {
$result = (string) $value[0];
} elseif (is_a($value, 'SimpleXMLElement')) {
return $result = (string) $value;
}
return trim($result);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace PicoFeed;
use Exception;
/**
* PicoFeedException Exception.
*
* @author Frederic Guillot
*/
abstract class PicoFeedException extends Exception
{
}

View File

@@ -0,0 +1,39 @@
<?php
namespace PicoFeed\Processor;
use PicoFeed\Base;
use PicoFeed\Filter\Filter;
use PicoFeed\Logging\Logger;
use PicoFeed\Parser\Feed;
use PicoFeed\Parser\Item;
/**
* Item Content Filter
*
* @package PicoFeed\Processor
* @author Frederic Guillot
*/
class ContentFilterProcessor extends Base implements ItemProcessorInterface
{
/**
* Execute Item Processor
*
* @access public
* @param Feed $feed
* @param Item $item
* @return bool
*/
public function execute(Feed $feed, Item $item)
{
if ($this->config->getContentFiltering(true)) {
$filter = Filter::html($item->getContent(), $feed->getSiteUrl());
$filter->setConfig($this->config);
$item->setContent($filter->execute());
} else {
Logger::setMessage(get_called_class().': Content filtering disabled');
}
return false;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace PicoFeed\Processor;
use PicoFeed\Base;
use PicoFeed\Parser\Feed;
use PicoFeed\Parser\Item;
/**
* Item Content Generator
*
* @package PicoFeed\Processor
* @author Frederic Guillot
*/
class ContentGeneratorProcessor extends Base implements ItemProcessorInterface
{
/**
* List of generators
*
* @access protected
* @var array
*/
protected $generators = array(
'youtube',
'file',
);
/**
* Execute Item Processor
*
* @access public
* @param Feed $feed
* @param Item $item
* @return bool
*/
public function execute(Feed $feed, Item $item)
{
foreach ($this->generators as $generator) {
$className = '\PicoFeed\Generator\\'.ucfirst($generator).'ContentGenerator';
$object = new $className($this->config);
if ($object->execute($item)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace PicoFeed\Processor;
use PicoFeed\Base;
use PicoFeed\Parser\Feed;
use PicoFeed\Parser\Item;
use PicoFeed\Config\Config;
/**
* Item Post Processor
*
* @package PicoFeed\Processor
* @author Frederic Guillot
*/
class ItemPostProcessor extends Base
{
/**
* List of processors
*
* @access private
* @var array
*/
private $processors = array();
/**
* Execute all processors
*
* @access public
* @param Feed $feed
* @param Item $item
* @return bool
*/
public function execute(Feed $feed, Item $item)
{
foreach ($this->processors as $processor) {
if ($processor->execute($feed, $item)) {
return true;
}
}
return false;
}
/**
* Register a new Item post-processor
*
* @access public
* @param ItemProcessorInterface $processor
* @return ItemPostProcessor
*/
public function register(ItemProcessorInterface $processor)
{
$this->processors[get_class($processor)] = $processor;
return $this;
}
/**
* Remove Processor instance
*
* @access public
* @param string $class
* @return ItemPostProcessor
*/
public function unregister($class)
{
if (isset($this->processors[$class])) {
unset($this->processors[$class]);
}
return $this;
}
/**
* Checks whether a specific processor is registered or not
*
* @access public
* @param string $class
* @return bool
*/
public function hasProcessor($class)
{
return isset($this->processors[$class]);
}
/**
* Get Processor instance
*
* @access public
* @param string $class
* @return ItemProcessorInterface|null
*/
public function getProcessor($class)
{
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

@@ -0,0 +1,25 @@
<?php
namespace PicoFeed\Processor;
use PicoFeed\Parser\Feed;
use PicoFeed\Parser\Item;
/**
* Item Processor Interface
*
* @package PicoFeed\Processor
* @author Frederic Guillot
*/
interface ItemProcessorInterface
{
/**
* Execute Item Processor
*
* @access public
* @param Feed $feed
* @param Item $item
* @return bool
*/
public function execute(Feed $feed, Item $item);
}

View File

@@ -0,0 +1,96 @@
<?php
namespace PicoFeed\Processor;
use Closure;
use PicoFeed\Base;
use PicoFeed\Parser\Feed;
use PicoFeed\Parser\Item;
use PicoFeed\Scraper\Scraper;
/**
* Scraper Processor
*
* @package PicoFeed\Processor
* @author Frederic Guillot
*/
class ScraperProcessor extends Base implements ItemProcessorInterface
{
private $ignoredUrls = array();
private $scraper;
/**
* Callback function for each scraper execution
*
* @var Closure
*/
private $executionCallback;
/**
* Add a new execution callback
*
* @access public
* @param Closure $executionCallback
* @return $this
*/
public function setExecutionCallback(Closure $executionCallback)
{
$this->executionCallback = $executionCallback;
return $this;
}
/**
* Execute Item Processor
*
* @access public
* @param Feed $feed
* @param Item $item
* @return bool
*/
public function execute(Feed $feed, Item $item)
{
if (!in_array($item->getUrl(), $this->ignoredUrls)) {
$scraper = $this->getScraper();
$scraper->setUrl($item->getUrl());
$scraper->execute();
if ($this->executionCallback && is_callable($this->executionCallback)) {
call_user_func($this->executionCallback, $feed, $item, $scraper);
}
if ($scraper->hasRelevantContent()) {
$item->setContent($scraper->getFilteredContent());
}
}
return false;
}
/**
* Ignore list of URLs
*
* @access public
* @param array $urls
* @return $this
*/
public function ignoreUrls(array $urls)
{
$this->ignoredUrls = $urls;
return $this;
}
/**
* Returns Scraper instance
*
* @access public
* @return Scraper
*/
public function getScraper()
{
if ($this->scraper === null) {
$this->scraper = new Scraper($this->config);
}
return $this->scraper;
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace PicoFeed\Reader;
use DOMXPath;
use PicoFeed\Base;
use PicoFeed\Client\Client;
use PicoFeed\Client\ClientException;
use PicoFeed\Client\Url;
use PicoFeed\Logging\Logger;
use PicoFeed\Parser\XmlParser;
/**
* Favicon class.
*
* https://en.wikipedia.org/wiki/Favicon
*
* @author Frederic Guillot
*/
class Favicon extends Base
{
/**
* Valid types for favicon (supported by browsers).
*
* @var array
*/
private $types = array(
'image/png',
'image/gif',
'image/x-icon',
'image/jpeg',
'image/jpg',
'image/svg+xml',
);
/**
* Icon binary content.
*
* @var string
*/
private $content = '';
/**
* Icon content type.
*
* @var string
*/
private $content_type = '';
/**
* Get the icon file content (available only after the download).
*
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* Get the icon file type (available only after the download).
*
* @return string
*/
public function getType()
{
foreach ($this->types as $type) {
if (strpos($this->content_type, $type) === 0) {
return $type;
}
}
return 'image/x-icon';
}
/**
* Get data URI (http://en.wikipedia.org/wiki/Data_URI_scheme).
*
* @return string
*/
public function getDataUri()
{
if (empty($this->content)) {
return '';
}
return sprintf(
'data:%s;base64,%s',
$this->getType(),
base64_encode($this->content)
);
}
/**
* Download and check if a resource exists.
*
* @param string $url URL
* @return \PicoFeed\Client\Client Client instance
*/
public function download($url)
{
$client = Client::getInstance();
$client->setConfig($this->config);
Logger::setMessage(get_called_class().' Download => '.$url);
try {
$client->execute($url);
} catch (ClientException $e) {
Logger::setMessage(get_called_class().' Download Failed => '.$e->getMessage());
}
return $client;
}
/**
* Check if a remote file exists.
*
* @param string $url URL
* @return bool
*/
public function exists($url)
{
return $this->download($url)->getContent() !== '';
}
/**
* Get the icon link for a website.
*
* @param string $website_link URL
* @param string $favicon_link optional URL
* @return string
*/
public function find($website_link, $favicon_link = '')
{
$website = new Url($website_link);
if ($favicon_link !== '') {
$icons = array($favicon_link);
} else {
$icons = $this->extract($this->download($website->getBaseUrl('/'))->getContent());
$icons[] = $website->getBaseUrl('/favicon.ico');
}
foreach ($icons as $icon_link) {
$icon_link = Url::resolve($icon_link, $website);
$resource = $this->download($icon_link);
$this->content = $resource->getContent();
$this->content_type = $resource->getContentType();
if ($this->content !== '') {
return $icon_link;
} elseif ($favicon_link !== '') {
return $this->find($website_link);
}
}
return '';
}
/**
* Extract the icon links from the HTML.
*
* @param string $html HTML
* @return array
*/
public function extract($html)
{
$icons = array();
if (empty($html)) {
return $icons;
}
$dom = XmlParser::getHtmlDocument($html);
$xpath = new DOMXpath($dom);
$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');
}
return $icons;
}
}

View File

@@ -0,0 +1,189 @@
<?php
namespace PicoFeed\Reader;
use DOMXPath;
use PicoFeed\Base;
use PicoFeed\Client\Client;
use PicoFeed\Client\Url;
use PicoFeed\Logging\Logger;
use PicoFeed\Parser\XmlParser;
/**
* Reader class.
*
* @author Frederic Guillot
*/
class Reader extends Base
{
/**
* Feed formats for detection.
*
* @var array
*/
private $formats = array(
'Atom' => '//feed',
'Rss20' => '//rss[@version="2.0"]',
'Rss92' => '//rss[@version="0.92"]',
'Rss91' => '//rss[@version="0.91"]',
'Rss10' => '//rdf',
);
/**
* Download a feed (no discovery).
*
* @param string $url Feed url
* @param string $last_modified Last modified HTTP header
* @param string $etag Etag HTTP header
* @param string $username HTTP basic auth username
* @param string $password HTTP basic auth password
*
* @return \PicoFeed\Client\Client
*/
public function download($url, $last_modified = '', $etag = '', $username = '', $password = '')
{
$url = $this->prependScheme($url);
return Client::getInstance()
->setConfig($this->config)
->setLastModified($last_modified)
->setEtag($etag)
->setUsername($username)
->setPassword($password)
->execute($url);
}
/**
* Discover and download a feed.
*
* @param string $url Feed or website url
* @param string $last_modified Last modified HTTP header
* @param string $etag Etag HTTP header
* @param string $username HTTP basic auth username
* @param string $password HTTP basic auth password
* @return Client
* @throws SubscriptionNotFoundException
*/
public function discover($url, $last_modified = '', $etag = '', $username = '', $password = '')
{
$client = $this->download($url, $last_modified, $etag, $username, $password);
// It's already a feed or the feed was not modified
if (!$client->isModified() || $this->detectFormat($client->getContent())) {
return $client;
}
// Try to find a subscription
$links = $this->find($client->getUrl(), $client->getContent());
if (empty($links)) {
throw new SubscriptionNotFoundException('Unable to find a subscription');
}
return $this->download($links[0], $last_modified, $etag, $username, $password);
}
/**
* Find feed urls inside a HTML document.
*
* @param string $url Website url
* @param string $html HTML content
*
* @return array List of feed links
*/
public function find($url, $html)
{
Logger::setMessage(get_called_class().': Try to discover subscriptions');
$dom = XmlParser::getHtmlDocument($html);
$xpath = new DOMXPath($dom);
$links = array();
$queries = array(
'//link[@type="application/rss+xml"]',
'//link[@type="application/atom+xml"]',
);
foreach ($queries as $query) {
$nodes = $xpath->query($query);
foreach ($nodes as $node) {
$link = $node->getAttribute('href');
if (!empty($link)) {
$feedUrl = new Url($link);
$siteUrl = new Url($url);
$links[] = $feedUrl->getAbsoluteUrl($feedUrl->isRelativeUrl() ? $siteUrl->getBaseUrl() : '');
}
}
}
Logger::setMessage(get_called_class().': '.implode(', ', $links));
return $links;
}
/**
* Get a parser instance.
*
* @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)
{
$format = $this->detectFormat($content);
if (empty($format)) {
throw new UnsupportedFeedFormatException('Unable to detect feed format');
}
$className = '\PicoFeed\Parser\\'.$format;
$parser = new $className($content, $encoding, $url);
$parser->setHashAlgo($this->config->getParserHashAlgo());
$parser->setConfig($this->config);
return $parser;
}
/**
* Detect the feed format.
*
* @param string $content Feed content
* @return string
*/
public function detectFormat($content)
{
$dom = XmlParser::getHtmlDocument($content);
$xpath = new DOMXPath($dom);
foreach ($this->formats as $parser_name => $query) {
$nodes = $xpath->query($query);
if ($nodes->length === 1) {
return $parser_name;
}
}
return '';
}
/**
* Add the prefix "http://" if the end-user just enter a domain name.
*
* @param string $url Url
* @return string
*/
public function prependScheme($url)
{
if (!preg_match('%^https?://%', $url)) {
$url = 'http://'.$url;
}
return $url;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace PicoFeed\Reader;
use PicoFeed\PicoFeedException;
/**
* ReaderException Exception.
*
* @author Frederic Guillot
*/
abstract class ReaderException extends PicoFeedException
{
}

View File

@@ -0,0 +1,12 @@
<?php
namespace PicoFeed\Reader;
/**
* SubscriptionNotFoundException Exception.
*
* @author Frederic Guillot
*/
class SubscriptionNotFoundException extends ReaderException
{
}

View File

@@ -0,0 +1,12 @@
<?php
namespace PicoFeed\Reader;
/**
* UnsupportedFeedFormatException Exception.
*
* @author Frederic Guillot
*/
class UnsupportedFeedFormatException extends ReaderException
{
}

View File

@@ -0,0 +1,14 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://combat.blog.lemonde.fr/2013/08/31/teddy-riner-le-rookie-devenu-rambo/#xtor=RSS-3208',
'body' => array(
'//div[@class="entry-content"]',
),
'strip' => array(
'//*[contains(@class, "fb-like") or contains(@class, "social")]'
),
)
)
);

View File

@@ -0,0 +1,15 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'title' => '//header/h1',
'test_url' => 'http://bits.blogs.nytimes.com/2012/01/16/wikipedia-plans-to-go-dark-on-wednesday-to-protest-sopa/',
'body' => array(
'//div[@class="postContent"]',
),
'strip' => array(
'//*[@class="shareToolsBox"]',
),
)
)
);

View File

@@ -0,0 +1,13 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://www.igen.fr/ailleurs/2014/05/nvidia-va-delaisser-les-smartphones-grand-public-86031',
'body' => array(
'//div[contains(@class, "field-name-body")]'
),
'strip' => array(
),
)
)
);

View File

@@ -0,0 +1,11 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://www.nytimes.com/2011/05/15/world/middleeast/15prince.html',
'body' => array(
'//p[contains(@class, "story-content")] | //div[@class="image"]',
),
)
)
);

View File

@@ -0,0 +1,11 @@
<?php
return array(
'grabber' => array(
'%.*%' => array(
'test_url' => 'http://eliascarpe.over-blog.com/2015/12/re-upload-projets-d-avenir.html',
'body' => array(
'//div[contains(concat(" ", normalize-space(@class), " "), " ob-section ")]',
),
)
)
);

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