(Grav GitSync) Automatic Commit from GitSync
This commit is contained in:
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = '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;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException($err);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInit8172238b124f456526f1fbb41af7538b::getLoader();
|
||||
+579
@@ -0,0 +1,579 @@
|
||||
<?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 \Closure(string):void */
|
||||
private static $includeFile;
|
||||
|
||||
/** @var string|null */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* List of PSR-0 prefixes
|
||||
*
|
||||
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var string|null */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var array<string, self>
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param string|null $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
self::initializeIncludeClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> Array of classname => path
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $classMap Class to filename map
|
||||
*
|
||||
* @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 list<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)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
$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 list<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)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
$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] = $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
$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 list<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 list<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 = self::$includeFile;
|
||||
$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 keyed by their corresponding vendor directories.
|
||||
*
|
||||
* @return array<string, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeIncludeClosure()
|
||||
{
|
||||
if (self::$includeFile !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
self::$includeFile = \Closure::bind(static function($file) {
|
||||
include $file;
|
||||
}, null, null);
|
||||
}
|
||||
}
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
<?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 string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
|
||||
* @internal
|
||||
*/
|
||||
private static $selfDir = null;
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
private static $installedIsLocalDir;
|
||||
|
||||
/**
|
||||
* @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 || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
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((string) $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();
|
||||
|
||||
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||
// all installed packages for example
|
||||
self::$installedIsLocalDir = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private static function getSelfDir()
|
||||
{
|
||||
if (self::$selfDir === null) {
|
||||
self::$selfDir = strtr(__DIR__, '\\', '/');
|
||||
}
|
||||
|
||||
return self::$selfDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
$copiedLocalDir = false;
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
$selfDir = self::getSelfDir();
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @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[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
self::$installedByVendor[$vendorDir] = $required;
|
||||
$installed[] = $required;
|
||||
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||
self::$installed = $required;
|
||||
self::$installedIsLocalDir = true;
|
||||
}
|
||||
}
|
||||
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||
$copiedLocalDir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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') {
|
||||
/** @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[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
||||
Vendored
+21
@@ -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.
|
||||
|
||||
@@ -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\\FormPlugin' => $baseDir . '/form.php',
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'TrilbyMedia\\Cap\\' => array($vendorDir . '/trilbymedia/cap-php/src'),
|
||||
'ReCaptcha\\' => array($vendorDir . '/google/recaptcha/src/ReCaptcha'),
|
||||
'Grav\\Plugin\\Form\\' => array($baseDir . '/classes'),
|
||||
);
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInit8172238b124f456526f1fbb41af7538b
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInit8172238b124f456526f1fbb41af7538b', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit8172238b124f456526f1fbb41af7538b', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit8172238b124f456526f1fbb41af7538b::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInit8172238b124f456526f1fbb41af7538b
|
||||
{
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'T' =>
|
||||
array (
|
||||
'TrilbyMedia\\Cap\\' => 16,
|
||||
),
|
||||
'R' =>
|
||||
array (
|
||||
'ReCaptcha\\' => 10,
|
||||
),
|
||||
'G' =>
|
||||
array (
|
||||
'Grav\\Plugin\\Form\\' => 17,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'TrilbyMedia\\Cap\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/trilbymedia/cap-php/src',
|
||||
),
|
||||
'ReCaptcha\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/google/recaptcha/src/ReCaptcha',
|
||||
),
|
||||
'Grav\\Plugin\\Form\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/../..' . '/classes',
|
||||
),
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'Grav\\Plugin\\FormPlugin' => __DIR__ . '/../..' . '/form.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInit8172238b124f456526f1fbb41af7538b::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInit8172238b124f456526f1fbb41af7538b::$prefixDirsPsr4;
|
||||
$loader->classMap = ComposerStaticInit8172238b124f456526f1fbb41af7538b::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"packages": [
|
||||
{
|
||||
"name": "google/recaptcha",
|
||||
"version": "1.3.1",
|
||||
"version_normalized": "1.3.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/google/recaptcha.git",
|
||||
"reference": "56522c261d2e8c58ba416c90f81a4cd9f2ed89b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/google/recaptcha/zipball/56522c261d2e8c58ba416c90f81a4cd9f2ed89b9",
|
||||
"reference": "56522c261d2e8c58ba416c90f81a4cd9f2ed89b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.14",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^10"
|
||||
},
|
||||
"time": "2025-06-26T22:21:57+00:00",
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.3.x-dev"
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ReCaptcha\\": "src/ReCaptcha"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.",
|
||||
"homepage": "https://www.google.com/recaptcha/",
|
||||
"keywords": [
|
||||
"Abuse",
|
||||
"captcha",
|
||||
"recaptcha",
|
||||
"spam"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/forum/#!forum/recaptcha",
|
||||
"issues": "https://github.com/google/recaptcha/issues",
|
||||
"source": "https://github.com/google/recaptcha"
|
||||
},
|
||||
"install-path": "../google/recaptcha"
|
||||
},
|
||||
{
|
||||
"name": "trilbymedia/cap-php",
|
||||
"version": "1.0.0",
|
||||
"version_normalized": "1.0.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/trilbymedia/cap-php.git",
|
||||
"reference": "88dbea9eeca2a73ba1576f60eaabd869396ad00d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/trilbymedia/cap-php/zipball/88dbea9eeca2a73ba1576f60eaabd869396ad00d",
|
||||
"reference": "88dbea9eeca2a73ba1576f60eaabd869396ad00d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-hash": "*",
|
||||
"ext-json": "*",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"psr/simple-cache": "^2.0 || ^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/simple-cache": "To use Psr16Storage with any PSR-16 cache implementation."
|
||||
},
|
||||
"time": "2026-05-06T11:15:46+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"TrilbyMedia\\Cap\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Trilby Media",
|
||||
"homepage": "https://trilby.media"
|
||||
}
|
||||
],
|
||||
"description": "PHP port of the Cap proof-of-work captcha server. Wire-compatible with @cap.js/widget.",
|
||||
"homepage": "https://github.com/trilbymedia/cap-php",
|
||||
"keywords": [
|
||||
"cap",
|
||||
"cap.js",
|
||||
"captcha",
|
||||
"pow",
|
||||
"proof-of-work",
|
||||
"security",
|
||||
"sha256"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/trilbymedia/cap-php/issues",
|
||||
"source": "https://github.com/trilbymedia/cap-php/tree/1.0.0"
|
||||
},
|
||||
"install-path": "../trilbymedia/cap-php"
|
||||
}
|
||||
],
|
||||
"dev": true,
|
||||
"dev-package-names": []
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => 'getgrav/grav-plugin-form',
|
||||
'pretty_version' => 'dev-develop',
|
||||
'version' => 'dev-develop',
|
||||
'reference' => 'f56eda2e183066bc0309bdc7c9cce0ed8f59e63b',
|
||||
'type' => 'grav-plugin',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'getgrav/grav-plugin-form' => array(
|
||||
'pretty_version' => 'dev-develop',
|
||||
'version' => 'dev-develop',
|
||||
'reference' => 'f56eda2e183066bc0309bdc7c9cce0ed8f59e63b',
|
||||
'type' => 'grav-plugin',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'google/recaptcha' => array(
|
||||
'pretty_version' => '1.3.1',
|
||||
'version' => '1.3.1.0',
|
||||
'reference' => '56522c261d2e8c58ba416c90f81a4cd9f2ed89b9',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../google/recaptcha',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'trilbymedia/cap-php' => array(
|
||||
'pretty_version' => '1.0.0',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => '88dbea9eeca2a73ba1576f60eaabd869396ad00d',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../trilbymedia/cap-php',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2019, Google Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. 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.
|
||||
|
||||
3. Neither the name of the copyright holder 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 HOLDER 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.
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
# reCAPTCHA PHP client library
|
||||
|
||||
[](https://travis-ci.org/google/recaptcha)
|
||||
[](https://coveralls.io/github/google/recaptcha)
|
||||
[](https://packagist.org/packages/google/recaptcha)
|
||||
[](https://packagist.org/packages/google/recaptcha)
|
||||
|
||||
reCAPTCHA is a free CAPTCHA service that protects websites from spam and abuse.
|
||||
This is a PHP library that wraps up the server-side verification step required
|
||||
to process responses from the reCAPTCHA service. This client supports both v2
|
||||
and v3.
|
||||
|
||||
- reCAPTCHA: https://www.google.com/recaptcha
|
||||
- This repo: https://github.com/google/recaptcha
|
||||
- Hosted demo: https://recaptcha-demo.appspot.com/
|
||||
- Version: 1.3.1
|
||||
- License: BSD, see [LICENSE](LICENSE)
|
||||
|
||||
## Installation
|
||||
|
||||
### Composer (recommended)
|
||||
|
||||
Use [Composer](https://getcomposer.org) to install this library from Packagist:
|
||||
[`google/recaptcha`](https://packagist.org/packages/google/recaptcha)
|
||||
|
||||
Run the following command from your project directory to add the dependency:
|
||||
|
||||
```sh
|
||||
composer require google/recaptcha "^1.3"
|
||||
```
|
||||
|
||||
Alternatively, add the dependency directly to your `composer.json` file:
|
||||
|
||||
```json
|
||||
"require": {
|
||||
"google/recaptcha": "^1.3"
|
||||
}
|
||||
```
|
||||
|
||||
### Support for earlier versions of PHP
|
||||
|
||||
The 1.3 release moves to PHP 8 and up. For earlier versions, you will need to
|
||||
stay with the 1.2 releases.
|
||||
|
||||
### Direct download
|
||||
|
||||
Download the [ZIP file](https://github.com/google/recaptcha/archive/master.zip)
|
||||
and extract into your project. An autoloader script is provided in
|
||||
`src/autoload.php` which you can require into your script. For example:
|
||||
|
||||
```php
|
||||
require_once '/path/to/recaptcha/src/autoload.php';
|
||||
$recaptcha = new \ReCaptcha\ReCaptcha($secret);
|
||||
```
|
||||
|
||||
The classes in the project are structured according to the
|
||||
[PSR-4](https://www.php-fig.org/psr/psr-4/) standard, so you can also use your
|
||||
own autoloader or require the needed files directly in your code.
|
||||
|
||||
## Usage
|
||||
|
||||
First obtain the appropriate keys for the type of reCAPTCHA you wish to
|
||||
integrate for v2 at https://www.google.com/recaptcha/admin or v3 at
|
||||
https://g.co/recaptcha/v3.
|
||||
|
||||
Then follow the [integration guide on the developer
|
||||
site](https://developers.google.com/recaptcha/intro) to add the reCAPTCHA
|
||||
functionality into your frontend.
|
||||
|
||||
This library comes in when you need to verify the user's response. On the PHP
|
||||
side you need the response from the reCAPTCHA service and secret key from your
|
||||
credentials. Instantiate the `ReCaptcha` class with your secret key, specify any
|
||||
additional validation rules, and then call `verify()` with the reCAPTCHA
|
||||
response (usually in `$_POST['g-recaptcha-response']` or the response from
|
||||
`grecaptcha.execute()` in JS which is in `$gRecaptchaResponse` in the example)
|
||||
and user's IP address. For example:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$recaptcha = new \ReCaptcha\ReCaptcha($secret);
|
||||
$resp = $recaptcha->setExpectedHostname('recaptcha-demo.appspot.com')
|
||||
->verify($gRecaptchaResponse, $remoteIp);
|
||||
if ($resp->isSuccess()) {
|
||||
// Verified!
|
||||
} else {
|
||||
$errors = $resp->getErrorCodes();
|
||||
}
|
||||
```
|
||||
|
||||
The following methods are available:
|
||||
|
||||
- `setExpectedHostname($hostname)`: ensures the hostname matches. You must do
|
||||
this if you have disabled "Domain/Package Name Validation" for your
|
||||
credentials.
|
||||
- `setExpectedApkPackageName($apkPackageName)`: if you're verifying a response
|
||||
from an Android app. Again, you must do this if you have disabled
|
||||
"Domain/Package Name Validation" for your credentials.
|
||||
- `setExpectedAction($action)`: ensures the action matches for the v3 API.
|
||||
- `setScoreThreshold($threshold)`: set a score threshold for responses from the
|
||||
v3 API
|
||||
- `setChallengeTimeout($timeoutSeconds)`: set a timeout between the user passing
|
||||
the reCAPTCHA and your server processing it.
|
||||
|
||||
Each of the `set`\*`()` methods return the `ReCaptcha` instance so you can chain
|
||||
them together. For example:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$recaptcha = new \ReCaptcha\ReCaptcha($secret);
|
||||
$resp = $recaptcha->setExpectedHostname('recaptcha-demo.appspot.com')
|
||||
->setExpectedAction('homepage')
|
||||
->setScoreThreshold(0.5)
|
||||
->verify($gRecaptchaResponse, $remoteIp);
|
||||
|
||||
if ($resp->isSuccess()) {
|
||||
// Verified!
|
||||
} else {
|
||||
$errors = $resp->getErrorCodes();
|
||||
}
|
||||
```
|
||||
|
||||
You can find the constants for the libraries error codes in the `ReCaptcha`
|
||||
class constants, e.g. `ReCaptcha::E_HOSTNAME_MISMATCH`
|
||||
|
||||
For more details on usage and structure, see [ARCHITECTURE](ARCHITECTURE.md).
|
||||
|
||||
### Examples
|
||||
|
||||
You can see examples of each reCAPTCHA type in [examples/](examples/). You can
|
||||
run the examples locally by using the Composer script:
|
||||
|
||||
```sh
|
||||
composer run-script serve-examples
|
||||
```
|
||||
|
||||
This makes use of the in-built PHP dev server to host the examples at
|
||||
http://localhost:8080/
|
||||
|
||||
These are also hosted on Google AppEngine Flexible environment at
|
||||
https://recaptcha-demo.appspot.com/. This is configured by
|
||||
[`app.yaml`](./app.yaml) which you can also use to [deploy to your own AppEngine
|
||||
project](https://cloud.google.com/appengine/docs/flexible/php/download).
|
||||
|
||||
## Contributing
|
||||
|
||||
No one ever has enough engineers, so we're very happy to accept contributions
|
||||
via Pull Requests. For details, see [CONTRIBUTING](CONTRIBUTING.md)
|
||||
@@ -0,0 +1,8 @@
|
||||
runtime: php
|
||||
env: flex
|
||||
|
||||
skip_files:
|
||||
- tests
|
||||
|
||||
runtime_config:
|
||||
document_root: examples
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "google/recaptcha",
|
||||
"description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.",
|
||||
"type": "library",
|
||||
"keywords": ["recaptcha", "captcha", "spam", "abuse"],
|
||||
"homepage": "https://www.google.com/recaptcha/",
|
||||
"license": "BSD-3-Clause",
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/forum/#!forum/recaptcha",
|
||||
"source": "https://github.com/google/recaptcha"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10",
|
||||
"friendsofphp/php-cs-fixer": "^3.14",
|
||||
"php-coveralls/php-coveralls": "^2.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ReCaptcha\\": "src/ReCaptcha"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.3.x-dev"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer -vvv fix --using-cache=no --dry-run .",
|
||||
"lint-fix": "PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer -vvv fix --using-cache=no .",
|
||||
"test": "XDEBUG_MODE=coverage vendor/bin/phpunit",
|
||||
"serve-examples": "@php -S localhost:8080 -t examples"
|
||||
},
|
||||
"config": {
|
||||
"process-timeout": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
/**
|
||||
* This is a PHP library that handles calling reCAPTCHA.
|
||||
*
|
||||
* BSD 3-Clause License
|
||||
* @copyright (c) 2019, Google Inc.
|
||||
* @link https://www.google.com/recaptcha
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder 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 HOLDER 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.
|
||||
*/
|
||||
|
||||
namespace ReCaptcha;
|
||||
|
||||
/**
|
||||
* reCAPTCHA client.
|
||||
*/
|
||||
class ReCaptcha
|
||||
{
|
||||
/**
|
||||
* Version of this client library.
|
||||
* @const string
|
||||
*/
|
||||
public const VERSION = 'php_1.3.1';
|
||||
|
||||
/**
|
||||
* URL for reCAPTCHA siteverify API
|
||||
* @const string
|
||||
*/
|
||||
public const SITE_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
|
||||
|
||||
/**
|
||||
* Invalid JSON received
|
||||
* @const string
|
||||
*/
|
||||
public const E_INVALID_JSON = 'invalid-json';
|
||||
|
||||
/**
|
||||
* Could not connect to service
|
||||
* @const string
|
||||
*/
|
||||
public const E_CONNECTION_FAILED = 'connection-failed';
|
||||
|
||||
/**
|
||||
* Did not receive a 200 from the service
|
||||
* @const string
|
||||
*/
|
||||
public const E_BAD_RESPONSE = 'bad-response';
|
||||
|
||||
/**
|
||||
* Not a success, but no error codes received!
|
||||
* @const string
|
||||
*/
|
||||
public const E_UNKNOWN_ERROR = 'unknown-error';
|
||||
|
||||
/**
|
||||
* ReCAPTCHA response not provided
|
||||
* @const string
|
||||
*/
|
||||
public const E_MISSING_INPUT_RESPONSE = 'missing-input-response';
|
||||
|
||||
/**
|
||||
* Expected hostname did not match
|
||||
* @const string
|
||||
*/
|
||||
public const E_HOSTNAME_MISMATCH = 'hostname-mismatch';
|
||||
|
||||
/**
|
||||
* Expected APK package name did not match
|
||||
* @const string
|
||||
*/
|
||||
public const E_APK_PACKAGE_NAME_MISMATCH = 'apk_package_name-mismatch';
|
||||
|
||||
/**
|
||||
* Expected action did not match
|
||||
* @const string
|
||||
*/
|
||||
public const E_ACTION_MISMATCH = 'action-mismatch';
|
||||
|
||||
/**
|
||||
* Score threshold not met
|
||||
* @const string
|
||||
*/
|
||||
public const E_SCORE_THRESHOLD_NOT_MET = 'score-threshold-not-met';
|
||||
|
||||
/**
|
||||
* Challenge timeout
|
||||
* @const string
|
||||
*/
|
||||
public const E_CHALLENGE_TIMEOUT = 'challenge-timeout';
|
||||
|
||||
/**
|
||||
* Shared secret for the site.
|
||||
* @var string
|
||||
*/
|
||||
private $secret;
|
||||
|
||||
/**
|
||||
* Method used to communicate with service. Defaults to POST request.
|
||||
* @var RequestMethod
|
||||
*/
|
||||
private $requestMethod;
|
||||
|
||||
private $hostname;
|
||||
private $apkPackageName;
|
||||
private $action;
|
||||
private $threshold;
|
||||
private $timeoutSeconds;
|
||||
|
||||
/**
|
||||
* Create a configured instance to use the reCAPTCHA service.
|
||||
*
|
||||
* @param string $secret The shared key between your site and reCAPTCHA.
|
||||
* @param RequestMethod $requestMethod method used to send the request. Defaults to POST.
|
||||
* @throws \RuntimeException if $secret is invalid
|
||||
*/
|
||||
public function __construct($secret, ?RequestMethod $requestMethod = null)
|
||||
{
|
||||
if (empty($secret)) {
|
||||
throw new \RuntimeException('No secret provided');
|
||||
}
|
||||
|
||||
if (!is_string($secret)) {
|
||||
throw new \RuntimeException('The provided secret must be a string');
|
||||
}
|
||||
|
||||
$this->secret = $secret;
|
||||
$this->requestMethod = (is_null($requestMethod)) ? new RequestMethod\Post() : $requestMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the reCAPTCHA siteverify API to verify whether the user passes
|
||||
* CAPTCHA test and additionally runs any specified additional checks
|
||||
*
|
||||
* @param string $response The user response token provided by reCAPTCHA, verifying the user on your site.
|
||||
* @param string $remoteIp The end user's IP address.
|
||||
* @return Response Response from the service.
|
||||
*/
|
||||
public function verify($response, $remoteIp = null)
|
||||
{
|
||||
// Discard empty solution submissions
|
||||
if (empty($response)) {
|
||||
$recaptchaResponse = new Response(false, array(self::E_MISSING_INPUT_RESPONSE));
|
||||
return $recaptchaResponse;
|
||||
}
|
||||
|
||||
$params = new RequestParameters($this->secret, $response, $remoteIp, self::VERSION);
|
||||
$rawResponse = $this->requestMethod->submit($params);
|
||||
$initialResponse = Response::fromJson($rawResponse);
|
||||
$validationErrors = array();
|
||||
|
||||
if (isset($this->hostname) && strcasecmp($this->hostname, $initialResponse->getHostname()) !== 0) {
|
||||
$validationErrors[] = self::E_HOSTNAME_MISMATCH;
|
||||
}
|
||||
|
||||
if (isset($this->apkPackageName) && strcasecmp($this->apkPackageName, $initialResponse->getApkPackageName()) !== 0) {
|
||||
$validationErrors[] = self::E_APK_PACKAGE_NAME_MISMATCH;
|
||||
}
|
||||
|
||||
if (isset($this->action) && strcasecmp($this->action, $initialResponse->getAction()) !== 0) {
|
||||
$validationErrors[] = self::E_ACTION_MISMATCH;
|
||||
}
|
||||
|
||||
if (isset($this->threshold) && $this->threshold > $initialResponse->getScore()) {
|
||||
$validationErrors[] = self::E_SCORE_THRESHOLD_NOT_MET;
|
||||
}
|
||||
|
||||
if (isset($this->timeoutSeconds)) {
|
||||
$challengeTs = strtotime($initialResponse->getChallengeTs());
|
||||
|
||||
if ($challengeTs > 0 && time() - $challengeTs > $this->timeoutSeconds) {
|
||||
$validationErrors[] = self::E_CHALLENGE_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($validationErrors)) {
|
||||
return $initialResponse;
|
||||
}
|
||||
|
||||
return new Response(
|
||||
false,
|
||||
array_merge($initialResponse->getErrorCodes(), $validationErrors),
|
||||
$initialResponse->getHostname(),
|
||||
$initialResponse->getChallengeTs(),
|
||||
$initialResponse->getApkPackageName(),
|
||||
$initialResponse->getScore(),
|
||||
$initialResponse->getAction()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a hostname to match against in verify()
|
||||
* This should be without a protocol or trailing slash, e.g. www.google.com
|
||||
*
|
||||
* @param string $hostname Expected hostname
|
||||
* @return ReCaptcha Current instance for fluent interface
|
||||
*/
|
||||
public function setExpectedHostname($hostname)
|
||||
{
|
||||
$this->hostname = $hostname;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an APK package name to match against in verify()
|
||||
*
|
||||
* @param string $apkPackageName Expected APK package name
|
||||
* @return ReCaptcha Current instance for fluent interface
|
||||
*/
|
||||
public function setExpectedApkPackageName($apkPackageName)
|
||||
{
|
||||
$this->apkPackageName = $apkPackageName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an action to match against in verify()
|
||||
* This should be set per page.
|
||||
*
|
||||
* @param string $action Expected action
|
||||
* @return ReCaptcha Current instance for fluent interface
|
||||
*/
|
||||
public function setExpectedAction($action)
|
||||
{
|
||||
$this->action = $action;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a threshold to meet or exceed in verify()
|
||||
* Threshold should be a float between 0 and 1 which will be tested as response >= threshold.
|
||||
*
|
||||
* @param float $threshold Expected threshold
|
||||
* @return ReCaptcha Current instance for fluent interface
|
||||
*/
|
||||
public function setScoreThreshold($threshold)
|
||||
{
|
||||
$this->threshold = floatval($threshold);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a timeout in seconds to test against the challenge timestamp in verify()
|
||||
*
|
||||
* @param int $timeoutSeconds Maximum time (seconds) elapsed since the challenge timestamp
|
||||
* @return ReCaptcha Current instance for fluent interface
|
||||
*/
|
||||
public function setChallengeTimeout($timeoutSeconds)
|
||||
{
|
||||
$this->timeoutSeconds = $timeoutSeconds;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* This is a PHP library that handles calling reCAPTCHA.
|
||||
*
|
||||
* BSD 3-Clause License
|
||||
* @copyright (c) 2019, Google Inc.
|
||||
* @link https://www.google.com/recaptcha
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder 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 HOLDER 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.
|
||||
*/
|
||||
|
||||
namespace ReCaptcha;
|
||||
|
||||
/**
|
||||
* Method used to send the request to the service.
|
||||
*/
|
||||
interface RequestMethod
|
||||
{
|
||||
/**
|
||||
* Submit the request with the specified parameters.
|
||||
*
|
||||
* @param RequestParameters $params Request parameters
|
||||
* @return string Body of the reCAPTCHA response
|
||||
*/
|
||||
public function submit(RequestParameters $params);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* This is a PHP library that handles calling reCAPTCHA.
|
||||
*
|
||||
* BSD 3-Clause License
|
||||
* @copyright (c) 2019, Google Inc.
|
||||
* @link https://www.google.com/recaptcha
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder 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 HOLDER 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.
|
||||
*/
|
||||
|
||||
namespace ReCaptcha\RequestMethod;
|
||||
|
||||
/**
|
||||
* Convenience wrapper around the cURL functions to allow mocking.
|
||||
*/
|
||||
class Curl
|
||||
{
|
||||
/**
|
||||
* @see http://php.net/curl_init
|
||||
* @param string $url
|
||||
* @return resource cURL handle
|
||||
*/
|
||||
public function init($url = null)
|
||||
{
|
||||
return curl_init($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see http://php.net/curl_setopt_array
|
||||
* @param resource $ch
|
||||
* @param array $options
|
||||
* @return bool
|
||||
*/
|
||||
public function setoptArray($ch, array $options)
|
||||
{
|
||||
return curl_setopt_array($ch, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see http://php.net/curl_exec
|
||||
* @param resource $ch
|
||||
* @return mixed
|
||||
*/
|
||||
public function exec($ch)
|
||||
{
|
||||
return curl_exec($ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see http://php.net/curl_close
|
||||
* @param resource $ch
|
||||
*/
|
||||
public function close($ch)
|
||||
{
|
||||
curl_close($ch);
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* This is a PHP library that handles calling reCAPTCHA.
|
||||
*
|
||||
* BSD 3-Clause License
|
||||
* @copyright (c) 2019, Google Inc.
|
||||
* @link https://www.google.com/recaptcha
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder 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 HOLDER 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.
|
||||
*/
|
||||
|
||||
namespace ReCaptcha\RequestMethod;
|
||||
|
||||
use ReCaptcha\ReCaptcha;
|
||||
use ReCaptcha\RequestMethod;
|
||||
use ReCaptcha\RequestParameters;
|
||||
|
||||
/**
|
||||
* Sends cURL request to the reCAPTCHA service.
|
||||
* Note: this requires the cURL extension to be enabled in PHP
|
||||
* @see http://php.net/manual/en/book.curl.php
|
||||
*/
|
||||
class CurlPost implements RequestMethod
|
||||
{
|
||||
/**
|
||||
* Curl connection to the reCAPTCHA service
|
||||
* @var Curl
|
||||
*/
|
||||
private $curl;
|
||||
|
||||
/**
|
||||
* URL for reCAPTCHA siteverify API
|
||||
* @var string
|
||||
*/
|
||||
private $siteVerifyUrl;
|
||||
|
||||
/**
|
||||
* Only needed if you want to override the defaults
|
||||
*
|
||||
* @param Curl $curl Curl resource
|
||||
* @param string $siteVerifyUrl URL for reCAPTCHA siteverify API
|
||||
*/
|
||||
public function __construct(?Curl $curl = null, $siteVerifyUrl = null)
|
||||
{
|
||||
$this->curl = (is_null($curl)) ? new Curl() : $curl;
|
||||
$this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the cURL request with the specified parameters.
|
||||
*
|
||||
* @param RequestParameters $params Request parameters
|
||||
* @return string Body of the reCAPTCHA response
|
||||
*/
|
||||
public function submit(RequestParameters $params)
|
||||
{
|
||||
$handle = $this->curl->init($this->siteVerifyUrl);
|
||||
|
||||
$options = array(
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $params->toQueryString(),
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Content-Type: application/x-www-form-urlencoded'
|
||||
),
|
||||
CURLINFO_HEADER_OUT => false,
|
||||
CURLOPT_HEADER => false,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true
|
||||
);
|
||||
$this->curl->setoptArray($handle, $options);
|
||||
|
||||
$response = $this->curl->exec($handle);
|
||||
$this->curl->close($handle);
|
||||
|
||||
if ($response !== false) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* This is a PHP library that handles calling reCAPTCHA.
|
||||
*
|
||||
* BSD 3-Clause License
|
||||
* @copyright (c) 2019, Google Inc.
|
||||
* @link https://www.google.com/recaptcha
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder 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 HOLDER 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.
|
||||
*/
|
||||
|
||||
namespace ReCaptcha\RequestMethod;
|
||||
|
||||
use ReCaptcha\ReCaptcha;
|
||||
use ReCaptcha\RequestMethod;
|
||||
use ReCaptcha\RequestParameters;
|
||||
|
||||
/**
|
||||
* Sends POST requests to the reCAPTCHA service.
|
||||
*/
|
||||
class Post implements RequestMethod
|
||||
{
|
||||
/**
|
||||
* URL for reCAPTCHA siteverify API
|
||||
* @var string
|
||||
*/
|
||||
private $siteVerifyUrl;
|
||||
|
||||
/**
|
||||
* Only needed if you want to override the defaults
|
||||
*
|
||||
* @param string $siteVerifyUrl URL for reCAPTCHA siteverify API
|
||||
*/
|
||||
public function __construct($siteVerifyUrl = null)
|
||||
{
|
||||
$this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the POST request with the specified parameters.
|
||||
*
|
||||
* @param RequestParameters $params Request parameters
|
||||
* @return string Body of the reCAPTCHA response
|
||||
*/
|
||||
public function submit(RequestParameters $params)
|
||||
{
|
||||
$options = array(
|
||||
'http' => array(
|
||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||
'method' => 'POST',
|
||||
'content' => $params->toQueryString(),
|
||||
// Force the peer to validate (not needed in 5.6.0+, but still works)
|
||||
'verify_peer' => true,
|
||||
),
|
||||
);
|
||||
$context = stream_context_create($options);
|
||||
$response = file_get_contents($this->siteVerifyUrl, false, $context);
|
||||
|
||||
if ($response !== false) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
/**
|
||||
* This is a PHP library that handles calling reCAPTCHA.
|
||||
*
|
||||
* BSD 3-Clause License
|
||||
* @copyright (c) 2019, Google Inc.
|
||||
* @link https://www.google.com/recaptcha
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder 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 HOLDER 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.
|
||||
*/
|
||||
|
||||
namespace ReCaptcha\RequestMethod;
|
||||
|
||||
/**
|
||||
* Convenience wrapper around native socket and file functions to allow for
|
||||
* mocking.
|
||||
*/
|
||||
class Socket
|
||||
{
|
||||
private $handle = null;
|
||||
|
||||
/**
|
||||
* fsockopen
|
||||
*
|
||||
* @see http://php.net/fsockopen
|
||||
* @param string $hostname
|
||||
* @param int $port
|
||||
* @param int $errno
|
||||
* @param string $errstr
|
||||
* @param float $timeout
|
||||
* @return resource
|
||||
*/
|
||||
public function fsockopen($hostname, $port = -1, &$errno = 0, &$errstr = '', $timeout = null)
|
||||
{
|
||||
$this->handle = fsockopen($hostname, $port, $errno, $errstr, (is_null($timeout) ? ini_get("default_socket_timeout") : $timeout));
|
||||
|
||||
if ($this->handle != false && $errno === 0 && $errstr === '') {
|
||||
return $this->handle;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* fwrite
|
||||
*
|
||||
* @see http://php.net/fwrite
|
||||
* @param string $string
|
||||
* @param int $length
|
||||
* @return int | bool
|
||||
*/
|
||||
public function fwrite($string, $length = null)
|
||||
{
|
||||
return fwrite($this->handle, $string, (is_null($length) ? strlen($string) : $length));
|
||||
}
|
||||
|
||||
/**
|
||||
* fgets
|
||||
*
|
||||
* @see http://php.net/fgets
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function fgets($length = null)
|
||||
{
|
||||
return fgets($this->handle, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* feof
|
||||
*
|
||||
* @see http://php.net/feof
|
||||
* @return bool
|
||||
*/
|
||||
public function feof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* fclose
|
||||
*
|
||||
* @see http://php.net/fclose
|
||||
* @return bool
|
||||
*/
|
||||
public function fclose()
|
||||
{
|
||||
return fclose($this->handle);
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/**
|
||||
* This is a PHP library that handles calling reCAPTCHA.
|
||||
*
|
||||
* BSD 3-Clause License
|
||||
* @copyright (c) 2019, Google Inc.
|
||||
* @link https://www.google.com/recaptcha
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder 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 HOLDER 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.
|
||||
*/
|
||||
|
||||
namespace ReCaptcha\RequestMethod;
|
||||
|
||||
use ReCaptcha\ReCaptcha;
|
||||
use ReCaptcha\RequestMethod;
|
||||
use ReCaptcha\RequestParameters;
|
||||
|
||||
/**
|
||||
* Sends a POST request to the reCAPTCHA service, but makes use of fsockopen()
|
||||
* instead of get_file_contents(). This is to account for people who may be on
|
||||
* servers where allow_url_open is disabled.
|
||||
*/
|
||||
class SocketPost implements RequestMethod
|
||||
{
|
||||
/**
|
||||
* Socket to the reCAPTCHA service
|
||||
* @var Socket
|
||||
*/
|
||||
private $socket;
|
||||
|
||||
private $siteVerifyUrl;
|
||||
|
||||
/**
|
||||
* Only needed if you want to override the defaults
|
||||
*
|
||||
* @param \ReCaptcha\RequestMethod\Socket $socket optional socket, injectable for testing
|
||||
* @param string $siteVerifyUrl URL for reCAPTCHA siteverify API
|
||||
*/
|
||||
public function __construct(?Socket $socket = null, $siteVerifyUrl = null)
|
||||
{
|
||||
$this->socket = (is_null($socket)) ? new Socket() : $socket;
|
||||
$this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the POST request with the specified parameters.
|
||||
*
|
||||
* @param RequestParameters $params Request parameters
|
||||
* @return string Body of the reCAPTCHA response
|
||||
*/
|
||||
public function submit(RequestParameters $params)
|
||||
{
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
$urlParsed = parse_url($this->siteVerifyUrl);
|
||||
|
||||
if (false === $this->socket->fsockopen('ssl://' . $urlParsed['host'], 443, $errno, $errstr, 30)) {
|
||||
return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
|
||||
}
|
||||
|
||||
$content = $params->toQueryString();
|
||||
|
||||
$request = "POST " . $urlParsed['path'] . " HTTP/1.0\r\n";
|
||||
$request .= "Host: " . $urlParsed['host'] . "\r\n";
|
||||
$request .= "Content-Type: application/x-www-form-urlencoded\r\n";
|
||||
$request .= "Content-length: " . strlen($content) . "\r\n";
|
||||
$request .= "Connection: close\r\n\r\n";
|
||||
$request .= $content . "\r\n\r\n";
|
||||
|
||||
$this->socket->fwrite($request);
|
||||
$response = '';
|
||||
|
||||
while (!$this->socket->feof()) {
|
||||
$response .= $this->socket->fgets(4096);
|
||||
}
|
||||
|
||||
$this->socket->fclose();
|
||||
|
||||
if (0 !== strpos($response, 'HTTP/1.0 200 OK')) {
|
||||
return '{"success": false, "error-codes": ["'.ReCaptcha::E_BAD_RESPONSE.'"]}';
|
||||
}
|
||||
|
||||
$parts = preg_split("#\n\s*\n#Uis", $response);
|
||||
|
||||
return $parts[1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/**
|
||||
* This is a PHP library that handles calling reCAPTCHA.
|
||||
*
|
||||
* BSD 3-Clause License
|
||||
* @copyright (c) 2019, Google Inc.
|
||||
* @link https://www.google.com/recaptcha
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder 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 HOLDER 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.
|
||||
*/
|
||||
|
||||
namespace ReCaptcha;
|
||||
|
||||
/**
|
||||
* Stores and formats the parameters for the request to the reCAPTCHA service.
|
||||
*/
|
||||
class RequestParameters
|
||||
{
|
||||
/**
|
||||
* The shared key between your site and reCAPTCHA.
|
||||
* @var string
|
||||
*/
|
||||
private $secret;
|
||||
|
||||
/**
|
||||
* The user response token provided by reCAPTCHA, verifying the user on your site.
|
||||
* @var string
|
||||
*/
|
||||
private $response;
|
||||
|
||||
/**
|
||||
* Remote user's IP address.
|
||||
* @var string
|
||||
*/
|
||||
private $remoteIp;
|
||||
|
||||
/**
|
||||
* Client version.
|
||||
* @var string
|
||||
*/
|
||||
private $version;
|
||||
|
||||
/**
|
||||
* Initialise parameters.
|
||||
*
|
||||
* @param string $secret Site secret.
|
||||
* @param string $response Value from g-captcha-response form field.
|
||||
* @param string $remoteIp User's IP address.
|
||||
* @param string $version Version of this client library.
|
||||
*/
|
||||
public function __construct($secret, $response, $remoteIp = null, $version = null)
|
||||
{
|
||||
$this->secret = $secret;
|
||||
$this->response = $response;
|
||||
$this->remoteIp = $remoteIp;
|
||||
$this->version = $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array representation.
|
||||
*
|
||||
* @return array Array formatted parameters.
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$params = array('secret' => $this->secret, 'response' => $this->response);
|
||||
|
||||
if (!is_null($this->remoteIp)) {
|
||||
$params['remoteip'] = $this->remoteIp;
|
||||
}
|
||||
|
||||
if (!is_null($this->version)) {
|
||||
$params['version'] = $this->version;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query string representation for HTTP request.
|
||||
*
|
||||
* @return string Query string formatted parameters.
|
||||
*/
|
||||
public function toQueryString()
|
||||
{
|
||||
return http_build_query($this->toArray(), '', '&');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
/**
|
||||
* This is a PHP library that handles calling reCAPTCHA.
|
||||
*
|
||||
* BSD 3-Clause License
|
||||
* @copyright (c) 2019, Google Inc.
|
||||
* @link https://www.google.com/recaptcha
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder 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 HOLDER 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.
|
||||
*/
|
||||
|
||||
namespace ReCaptcha;
|
||||
|
||||
/**
|
||||
* The response returned from the service.
|
||||
*/
|
||||
class Response
|
||||
{
|
||||
/**
|
||||
* Success or failure.
|
||||
* @var boolean
|
||||
*/
|
||||
private $success = false;
|
||||
|
||||
/**
|
||||
* Error code strings.
|
||||
* @var array
|
||||
*/
|
||||
private $errorCodes = array();
|
||||
|
||||
/**
|
||||
* The hostname of the site where the reCAPTCHA was solved.
|
||||
* @var string
|
||||
*/
|
||||
private $hostname;
|
||||
|
||||
/**
|
||||
* Timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
|
||||
* @var string
|
||||
*/
|
||||
private $challengeTs;
|
||||
|
||||
/**
|
||||
* APK package name
|
||||
* @var string
|
||||
*/
|
||||
private $apkPackageName;
|
||||
|
||||
/**
|
||||
* Score assigned to the request
|
||||
* @var float
|
||||
*/
|
||||
private $score;
|
||||
|
||||
/**
|
||||
* Action as specified by the page
|
||||
* @var string
|
||||
*/
|
||||
private $action;
|
||||
|
||||
/**
|
||||
* Build the response from the expected JSON returned by the service.
|
||||
*
|
||||
* @param string $json
|
||||
* @return \ReCaptcha\Response
|
||||
*/
|
||||
public static function fromJson($json)
|
||||
{
|
||||
$responseData = json_decode($json, true);
|
||||
|
||||
if (!$responseData) {
|
||||
return new Response(false, array(ReCaptcha::E_INVALID_JSON));
|
||||
}
|
||||
|
||||
$hostname = isset($responseData['hostname']) ? $responseData['hostname'] : '';
|
||||
$challengeTs = isset($responseData['challenge_ts']) ? $responseData['challenge_ts'] : '';
|
||||
$apkPackageName = isset($responseData['apk_package_name']) ? $responseData['apk_package_name'] : '';
|
||||
$score = isset($responseData['score']) ? floatval($responseData['score']) : null;
|
||||
$action = isset($responseData['action']) ? $responseData['action'] : '';
|
||||
|
||||
if (isset($responseData['success']) && $responseData['success'] == true) {
|
||||
return new Response(true, array(), $hostname, $challengeTs, $apkPackageName, $score, $action);
|
||||
}
|
||||
|
||||
if (isset($responseData['error-codes']) && is_array($responseData['error-codes'])) {
|
||||
return new Response(false, $responseData['error-codes'], $hostname, $challengeTs, $apkPackageName, $score, $action);
|
||||
}
|
||||
|
||||
return new Response(false, array(ReCaptcha::E_UNKNOWN_ERROR), $hostname, $challengeTs, $apkPackageName, $score, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param boolean $success
|
||||
* @param string $hostname
|
||||
* @param string $challengeTs
|
||||
* @param string $apkPackageName
|
||||
* @param float $score
|
||||
* @param string $action
|
||||
* @param array $errorCodes
|
||||
*/
|
||||
public function __construct($success, array $errorCodes = array(), $hostname = '', $challengeTs = '', $apkPackageName = '', $score = null, $action = '')
|
||||
{
|
||||
$this->success = $success;
|
||||
$this->hostname = $hostname;
|
||||
$this->challengeTs = $challengeTs;
|
||||
$this->apkPackageName = $apkPackageName;
|
||||
$this->score = $score;
|
||||
$this->action = $action;
|
||||
$this->errorCodes = $errorCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is success?
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isSuccess()
|
||||
{
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error codes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getErrorCodes()
|
||||
{
|
||||
return $this->errorCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hostname.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHostname()
|
||||
{
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get challenge timestamp
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getChallengeTs()
|
||||
{
|
||||
return $this->challengeTs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get APK package name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getApkPackageName()
|
||||
{
|
||||
return $this->apkPackageName;
|
||||
}
|
||||
/**
|
||||
* Get score
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getScore()
|
||||
{
|
||||
return $this->score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAction()
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return array(
|
||||
'success' => $this->isSuccess(),
|
||||
'hostname' => $this->getHostname(),
|
||||
'challenge_ts' => $this->getChallengeTs(),
|
||||
'apk_package_name' => $this->getApkPackageName(),
|
||||
'score' => $this->getScore(),
|
||||
'action' => $this->getAction(),
|
||||
'error-codes' => $this->getErrorCodes(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/* An autoloader for ReCaptcha\Foo classes. This should be required()
|
||||
* by the user before attempting to instantiate any of the ReCaptcha
|
||||
* classes.
|
||||
*
|
||||
* BSD 3-Clause License
|
||||
* @copyright (c) 2019, Google Inc.
|
||||
* @link https://www.google.com/recaptcha
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder 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 HOLDER 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.
|
||||
*/
|
||||
|
||||
spl_autoload_register(function ($class) {
|
||||
if (substr($class, 0, 10) !== 'ReCaptcha\\') {
|
||||
/* If the class does not lie under the "ReCaptcha" namespace,
|
||||
* then we can exit immediately.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
/* All of the classes have names like "ReCaptcha\Foo", so we need
|
||||
* to replace the backslashes with frontslashes if we want the
|
||||
* name to map directly to a location in the filesystem.
|
||||
*/
|
||||
$class = str_replace('\\', '/', $class);
|
||||
|
||||
/* First, check under the current directory. It is important that
|
||||
* we look here first, so that we don't waste time searching for
|
||||
* test classes in the common case.
|
||||
*/
|
||||
$path = dirname(__FILE__).'/'.$class.'.php';
|
||||
if (is_readable($path)) {
|
||||
require_once $path;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* If we didn't find what we're looking for already, maybe it's
|
||||
* a test class?
|
||||
*/
|
||||
$path = dirname(__FILE__).'/../tests/'.$class.'.php';
|
||||
if (is_readable($path)) {
|
||||
require_once $path;
|
||||
}
|
||||
});
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Support. While redistributing the Work or
|
||||
Derivative Works thereof, You may choose to offer, and charge a
|
||||
fee for, acceptance of support, warranty, indemnity, or other
|
||||
liability obligations and/or rights consistent with this License.
|
||||
However, in accepting such obligations, You may act only on Your
|
||||
own behalf and on Your sole responsibility, not on behalf of any
|
||||
other Contributor, and only if You agree to indemnify, defend,
|
||||
and hold each Contributor harmless for any liability incurred by,
|
||||
or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or support.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2026 Trilby Media
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,60 @@
|
||||
# cap-php
|
||||
|
||||
PHP port of the [Cap](https://github.com/tiagozip/cap) proof-of-work captcha server.
|
||||
|
||||
Wire-compatible with the official [`@cap.js/widget`](https://www.npmjs.com/package/@cap.js/widget), so the unmodified JS widget can talk to a PHP-backed endpoint.
|
||||
|
||||
- SHA-256 proof-of-work — no tracking, no third-party calls, no API keys
|
||||
- Small (~500 LOC), no runtime dependencies beyond ext-json / ext-hash
|
||||
- Pluggable storage: in-memory, filesystem, or any PSR-16 cache
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
composer require trilbymedia/cap-php
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```php
|
||||
use TrilbyMedia\Cap\Cap;
|
||||
use TrilbyMedia\Cap\Config;
|
||||
use TrilbyMedia\Cap\Storage\FilesystemStorage;
|
||||
|
||||
$storage = new FilesystemStorage('/var/lib/cap');
|
||||
$cap = new Cap(new Config(
|
||||
challengeStorage: $storage,
|
||||
tokenStorage: $storage,
|
||||
));
|
||||
|
||||
// In your /challenge endpoint:
|
||||
$result = $cap->createChallenge();
|
||||
// echo json_encode($result);
|
||||
|
||||
// In your /redeem endpoint (body: {"token":"...","solutions":[...]}):
|
||||
$result = $cap->redeemChallenge($token, $solutions);
|
||||
// echo json_encode($result);
|
||||
|
||||
// When validating a form submission that carried a cap token:
|
||||
if ($cap->validateToken($submittedToken)) {
|
||||
// success
|
||||
}
|
||||
```
|
||||
|
||||
## Defaults
|
||||
|
||||
| Option | Default | Meaning |
|
||||
| -------------------- | -------- | ------------------------------------------- |
|
||||
| challengeCount | 50 | Number of sub-challenges per captcha |
|
||||
| challengeSize | 32 | Salt length (hex chars) |
|
||||
| challengeDifficulty | 4 | Target prefix length (hex chars) |
|
||||
| expiresMs | 600 000 | Challenge TTL (10 min) |
|
||||
| token TTL | 20 min | Validation token TTL (not configurable) |
|
||||
|
||||
## Protocol compatibility
|
||||
|
||||
The PRNG and hashing are bit-exact with upstream `server/index.js` — verified by `tests/fixtures/prng-vectors.json`, a set of vectors generated directly from the upstream JS implementation.
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0, matching upstream.
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "trilbymedia/cap-php",
|
||||
"description": "PHP port of the Cap proof-of-work captcha server. Wire-compatible with @cap.js/widget.",
|
||||
"type": "library",
|
||||
"license": "Apache-2.0",
|
||||
"keywords": ["captcha", "proof-of-work", "cap", "cap.js", "pow", "sha256", "security"],
|
||||
"homepage": "https://github.com/trilbymedia/cap-php",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Trilby Media",
|
||||
"homepage": "https://trilby.media"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"ext-json": "*",
|
||||
"ext-hash": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"psr/simple-cache": "^2.0 || ^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/simple-cache": "To use Psr16Storage with any PSR-16 cache implementation."
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"TrilbyMedia\\Cap\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"TrilbyMedia\\Cap\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phpunit",
|
||||
"test-coverage": "phpunit --coverage-html coverage"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TrilbyMedia\Cap;
|
||||
|
||||
/**
|
||||
* Cap proof-of-work captcha server.
|
||||
*
|
||||
* Wire-compatible with the official @cap.js/widget. The client widget
|
||||
* POSTs to two endpoints you expose:
|
||||
*
|
||||
* POST /challenge → Cap::createChallenge() response
|
||||
* POST /redeem → Cap::redeemChallenge($body['token'], $body['solutions'])
|
||||
*
|
||||
* When the form is submitted, validate the token the widget put in the
|
||||
* form with Cap::validateToken().
|
||||
*/
|
||||
final class Cap
|
||||
{
|
||||
private const CLEANUP_INTERVAL_MS = 300_000; // 5 min
|
||||
|
||||
private int $lastCleanupMs = 0;
|
||||
|
||||
public function __construct(private readonly Config $config) {}
|
||||
|
||||
/**
|
||||
* Generate a new challenge.
|
||||
*
|
||||
* @return array{challenge: array{c:int,s:int,d:int}, token?: string, expires: int}
|
||||
*/
|
||||
public function createChallenge(?ChallengeOptions $opts = null): array
|
||||
{
|
||||
$this->lazyCleanup();
|
||||
|
||||
$challenge = [
|
||||
'c' => $opts?->challengeCount ?? $this->config->challengeCount,
|
||||
's' => $opts?->challengeSize ?? $this->config->challengeSize,
|
||||
'd' => $opts?->challengeDifficulty ?? $this->config->challengeDifficulty,
|
||||
];
|
||||
$expiresMs = $opts?->expiresMs ?? $this->config->expiresMs;
|
||||
$expires = $this->nowMs() + $expiresMs;
|
||||
|
||||
if ($opts?->store === false) {
|
||||
return ['challenge' => $challenge, 'expires' => $expires];
|
||||
}
|
||||
|
||||
$token = $this->randomHex(25);
|
||||
$this->config->challengeStorage->storeChallenge($token, $challenge + ['expires' => $expires]);
|
||||
|
||||
return ['challenge' => $challenge, 'token' => $token, 'expires' => $expires];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify solutions against a stored challenge and issue a verification token.
|
||||
*
|
||||
* @param int[] $solutions
|
||||
* @return array{success: bool, token?: string, expires?: int, message?: string}
|
||||
*/
|
||||
public function redeemChallenge(string $token, array $solutions): array
|
||||
{
|
||||
foreach ($solutions as $s) {
|
||||
if (!is_int($s)) {
|
||||
return ['success' => false, 'message' => 'Invalid body'];
|
||||
}
|
||||
}
|
||||
if ($token === '') {
|
||||
return ['success' => false, 'message' => 'Invalid body'];
|
||||
}
|
||||
|
||||
$this->lazyCleanup();
|
||||
|
||||
$data = $this->config->challengeStorage->readChallenge($token);
|
||||
$this->config->challengeStorage->deleteChallenge($token);
|
||||
|
||||
if ($data === null || ($data['expires'] ?? 0) < $this->nowMs()) {
|
||||
return ['success' => false, 'message' => 'Challenge invalid or expired'];
|
||||
}
|
||||
|
||||
$count = $data['c'];
|
||||
$size = $data['s'];
|
||||
$diff = $data['d'];
|
||||
|
||||
if (count($solutions) < $count) {
|
||||
return ['success' => false, 'message' => 'Invalid solution'];
|
||||
}
|
||||
|
||||
for ($i = 1; $i <= $count; $i++) {
|
||||
$salt = Prng::generate($token . $i, $size);
|
||||
$target = Prng::generate($token . $i . 'd', $diff);
|
||||
$hash = hash('sha256', $salt . (string)$solutions[$i - 1]);
|
||||
if (!str_starts_with($hash, $target)) {
|
||||
return ['success' => false, 'message' => 'Invalid solution'];
|
||||
}
|
||||
}
|
||||
|
||||
$vertoken = $this->randomHex(15);
|
||||
$id = $this->randomHex(8);
|
||||
$expires = $this->nowMs() + $this->config->tokenTtlMs;
|
||||
$key = $id . ':' . hash('sha256', $vertoken);
|
||||
|
||||
$this->config->tokenStorage->storeToken($key, $expires);
|
||||
|
||||
return ['success' => true, 'token' => $id . ':' . $vertoken, 'expires' => $expires];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a verification token returned from redeemChallenge.
|
||||
* By default, the token is consumed (deleted) on successful validation.
|
||||
*/
|
||||
public function validateToken(string $token, bool $keepToken = false): bool
|
||||
{
|
||||
$this->lazyCleanup();
|
||||
|
||||
if ($token === '' || !str_contains($token, ':')) {
|
||||
return false;
|
||||
}
|
||||
$parts = explode(':', $token, 2);
|
||||
if (count($parts) !== 2 || $parts[0] === '' || $parts[1] === '') {
|
||||
return false;
|
||||
}
|
||||
[$id, $vertoken] = $parts;
|
||||
|
||||
$key = $id . ':' . hash('sha256', $vertoken);
|
||||
$expires = $this->config->tokenStorage->readToken($key);
|
||||
|
||||
if ($expires === null || $expires <= $this->nowMs()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$keepToken) {
|
||||
$this->config->tokenStorage->deleteToken($key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually run cleanup of expired challenges and tokens.
|
||||
*/
|
||||
public function cleanup(): void
|
||||
{
|
||||
$this->config->challengeStorage->deleteExpiredChallenges();
|
||||
$this->config->tokenStorage->deleteExpiredTokens();
|
||||
$this->lastCleanupMs = $this->nowMs();
|
||||
}
|
||||
|
||||
private function lazyCleanup(): void
|
||||
{
|
||||
if ($this->config->disableAutoCleanup) {
|
||||
return;
|
||||
}
|
||||
$now = $this->nowMs();
|
||||
if ($now - $this->lastCleanupMs > self::CLEANUP_INTERVAL_MS) {
|
||||
$this->cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private function nowMs(): int
|
||||
{
|
||||
return (int)(microtime(true) * 1000);
|
||||
}
|
||||
|
||||
private function randomHex(int $bytes): string
|
||||
{
|
||||
return bin2hex(random_bytes($bytes));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TrilbyMedia\Cap;
|
||||
|
||||
/**
|
||||
* Per-call overrides for Cap::createChallenge(). Any field left null
|
||||
* inherits the value configured on the Cap instance.
|
||||
*/
|
||||
final class ChallengeOptions
|
||||
{
|
||||
public function __construct(
|
||||
public readonly ?int $challengeCount = null,
|
||||
public readonly ?int $challengeSize = null,
|
||||
public readonly ?int $challengeDifficulty = null,
|
||||
public readonly ?int $expiresMs = null,
|
||||
public readonly ?bool $store = null,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TrilbyMedia\Cap;
|
||||
|
||||
use TrilbyMedia\Cap\Storage\ChallengeStorageInterface;
|
||||
use TrilbyMedia\Cap\Storage\TokenStorageInterface;
|
||||
|
||||
/**
|
||||
* Immutable configuration for a Cap instance.
|
||||
*/
|
||||
final class Config
|
||||
{
|
||||
public function __construct(
|
||||
public readonly ChallengeStorageInterface $challengeStorage,
|
||||
public readonly TokenStorageInterface $tokenStorage,
|
||||
public readonly int $challengeCount = 50,
|
||||
public readonly int $challengeSize = 32,
|
||||
public readonly int $challengeDifficulty = 4,
|
||||
public readonly int $expiresMs = 600_000,
|
||||
public readonly int $tokenTtlMs = 1_200_000,
|
||||
public readonly bool $disableAutoCleanup = false,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TrilbyMedia\Cap;
|
||||
|
||||
/**
|
||||
* Deterministic PRNG that must be bit-exact with the upstream cap.js
|
||||
* implementation in server/index.js. Used to regenerate the salt/target
|
||||
* pairs for each sub-challenge from the challenge token.
|
||||
*
|
||||
* JS uses 32-bit unsigned/int32 semantics. In PHP (64-bit) we emulate
|
||||
* that with explicit `& 0xFFFFFFFF` masks after every arithmetic op.
|
||||
*/
|
||||
final class Prng
|
||||
{
|
||||
private const UINT32_MASK = 0xFFFFFFFF;
|
||||
private const FNV_OFFSET = 0x811C9DC5; // 2166136261
|
||||
|
||||
public static function generate(string $seed, int $length): string
|
||||
{
|
||||
if ($length <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$state = self::fnv1a($seed);
|
||||
$result = '';
|
||||
|
||||
while (strlen($result) < $length) {
|
||||
$state = self::next($state);
|
||||
$result .= str_pad(dechex($state), 8, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
return substr($result, 0, $length);
|
||||
}
|
||||
|
||||
private static function fnv1a(string $str): int
|
||||
{
|
||||
$hash = self::FNV_OFFSET;
|
||||
$len = strlen($str);
|
||||
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$hash = ($hash ^ ord($str[$i])) & self::UINT32_MASK;
|
||||
|
||||
$s1 = ($hash << 1) & self::UINT32_MASK;
|
||||
$s4 = ($hash << 4) & self::UINT32_MASK;
|
||||
$s7 = ($hash << 7) & self::UINT32_MASK;
|
||||
$s8 = ($hash << 8) & self::UINT32_MASK;
|
||||
$s24 = ($hash << 24) & self::UINT32_MASK;
|
||||
|
||||
$hash = ($hash + $s1 + $s4 + $s7 + $s8 + $s24) & self::UINT32_MASK;
|
||||
}
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
private static function next(int $state): int
|
||||
{
|
||||
$state = ($state ^ (($state << 13) & self::UINT32_MASK)) & self::UINT32_MASK;
|
||||
$state = ($state ^ ($state >> 17)) & self::UINT32_MASK;
|
||||
$state = ($state ^ (($state << 5) & self::UINT32_MASK)) & self::UINT32_MASK;
|
||||
|
||||
return $state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TrilbyMedia\Cap\Storage;
|
||||
|
||||
/**
|
||||
* In-memory storage. Useful for tests and single-request flows.
|
||||
* Not persistent across requests; do not use in production.
|
||||
*/
|
||||
final class ArrayStorage implements ChallengeStorageInterface, TokenStorageInterface
|
||||
{
|
||||
/** @var array<string, array{c:int,s:int,d:int,expires:int}> */
|
||||
private array $challenges = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
private array $tokens = [];
|
||||
|
||||
public function storeChallenge(string $token, array $challenge): void
|
||||
{
|
||||
$this->challenges[$token] = $challenge;
|
||||
}
|
||||
|
||||
public function readChallenge(string $token): ?array
|
||||
{
|
||||
return $this->challenges[$token] ?? null;
|
||||
}
|
||||
|
||||
public function deleteChallenge(string $token): void
|
||||
{
|
||||
unset($this->challenges[$token]);
|
||||
}
|
||||
|
||||
public function deleteExpiredChallenges(): void
|
||||
{
|
||||
$now = (int)(microtime(true) * 1000);
|
||||
foreach ($this->challenges as $k => $v) {
|
||||
if ($v['expires'] < $now) {
|
||||
unset($this->challenges[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function storeToken(string $key, int $expiresMs): void
|
||||
{
|
||||
$this->tokens[$key] = $expiresMs;
|
||||
}
|
||||
|
||||
public function readToken(string $key): ?int
|
||||
{
|
||||
return $this->tokens[$key] ?? null;
|
||||
}
|
||||
|
||||
public function deleteToken(string $key): void
|
||||
{
|
||||
unset($this->tokens[$key]);
|
||||
}
|
||||
|
||||
public function deleteExpiredTokens(): void
|
||||
{
|
||||
$now = (int)(microtime(true) * 1000);
|
||||
foreach ($this->tokens as $k => $v) {
|
||||
if ($v < $now) {
|
||||
unset($this->tokens[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TrilbyMedia\Cap\Storage;
|
||||
|
||||
/**
|
||||
* Storage for outstanding challenges (token → {c, s, d, expires}).
|
||||
* Challenges are typically short-lived (10 min default).
|
||||
*/
|
||||
interface ChallengeStorageInterface
|
||||
{
|
||||
/**
|
||||
* @param array{c:int,s:int,d:int,expires:int} $challenge
|
||||
*/
|
||||
public function storeChallenge(string $token, array $challenge): void;
|
||||
|
||||
/**
|
||||
* @return array{c:int,s:int,d:int,expires:int}|null
|
||||
*/
|
||||
public function readChallenge(string $token): ?array;
|
||||
|
||||
public function deleteChallenge(string $token): void;
|
||||
|
||||
public function deleteExpiredChallenges(): void;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TrilbyMedia\Cap\Storage;
|
||||
|
||||
/**
|
||||
* File-backed storage. Challenges and tokens each live in their own JSON file.
|
||||
* Simple and dependency-free; good default for small deployments.
|
||||
*
|
||||
* Not optimized for concurrency — use Psr16Storage with a real cache
|
||||
* (APCu, Redis, etc.) for production workloads.
|
||||
*/
|
||||
final class FilesystemStorage implements ChallengeStorageInterface, TokenStorageInterface
|
||||
{
|
||||
private string $challengesFile;
|
||||
private string $tokensFile;
|
||||
|
||||
public function __construct(string $directory)
|
||||
{
|
||||
if (!is_dir($directory) && !@mkdir($directory, 0775, true) && !is_dir($directory)) {
|
||||
throw new \RuntimeException("Cannot create storage directory: {$directory}");
|
||||
}
|
||||
$this->challengesFile = rtrim($directory, '/') . '/challenges.json';
|
||||
$this->tokensFile = rtrim($directory, '/') . '/tokens.json';
|
||||
}
|
||||
|
||||
public function storeChallenge(string $token, array $challenge): void
|
||||
{
|
||||
$all = $this->read($this->challengesFile);
|
||||
$all[$token] = $challenge;
|
||||
$this->write($this->challengesFile, $all);
|
||||
}
|
||||
|
||||
public function readChallenge(string $token): ?array
|
||||
{
|
||||
$all = $this->read($this->challengesFile);
|
||||
/** @var array{c:int,s:int,d:int,expires:int}|null */
|
||||
return $all[$token] ?? null;
|
||||
}
|
||||
|
||||
public function deleteChallenge(string $token): void
|
||||
{
|
||||
$all = $this->read($this->challengesFile);
|
||||
if (isset($all[$token])) {
|
||||
unset($all[$token]);
|
||||
$this->write($this->challengesFile, $all);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteExpiredChallenges(): void
|
||||
{
|
||||
$now = (int)(microtime(true) * 1000);
|
||||
$all = $this->read($this->challengesFile);
|
||||
$changed = false;
|
||||
foreach ($all as $k => $v) {
|
||||
if (($v['expires'] ?? 0) < $now) {
|
||||
unset($all[$k]);
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
$this->write($this->challengesFile, $all);
|
||||
}
|
||||
}
|
||||
|
||||
public function storeToken(string $key, int $expiresMs): void
|
||||
{
|
||||
$all = $this->read($this->tokensFile);
|
||||
$all[$key] = $expiresMs;
|
||||
$this->write($this->tokensFile, $all);
|
||||
}
|
||||
|
||||
public function readToken(string $key): ?int
|
||||
{
|
||||
$all = $this->read($this->tokensFile);
|
||||
return isset($all[$key]) ? (int)$all[$key] : null;
|
||||
}
|
||||
|
||||
public function deleteToken(string $key): void
|
||||
{
|
||||
$all = $this->read($this->tokensFile);
|
||||
if (isset($all[$key])) {
|
||||
unset($all[$key]);
|
||||
$this->write($this->tokensFile, $all);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteExpiredTokens(): void
|
||||
{
|
||||
$now = (int)(microtime(true) * 1000);
|
||||
$all = $this->read($this->tokensFile);
|
||||
$changed = false;
|
||||
foreach ($all as $k => $v) {
|
||||
if ((int)$v < $now) {
|
||||
unset($all[$k]);
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
$this->write($this->tokensFile, $all);
|
||||
}
|
||||
}
|
||||
|
||||
private function read(string $file): array
|
||||
{
|
||||
if (!is_file($file)) {
|
||||
return [];
|
||||
}
|
||||
$contents = @file_get_contents($file);
|
||||
if ($contents === false || $contents === '') {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($contents, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
private function write(string $file, array $data): void
|
||||
{
|
||||
$tmp = $file . '.tmp' . bin2hex(random_bytes(4));
|
||||
file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
|
||||
rename($tmp, $file);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TrilbyMedia\Cap\Storage;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
/**
|
||||
* Backs Cap storage with any PSR-16 cache (APCu, Redis, Memcached,
|
||||
* Grav cache, etc.). Recommended for production.
|
||||
*
|
||||
* Note: PSR-16 has no "list all keys" primitive, so deleteExpired*()
|
||||
* is a no-op — cache backends should evict via their own TTL.
|
||||
*/
|
||||
final class Psr16Storage implements ChallengeStorageInterface, TokenStorageInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CacheInterface $cache,
|
||||
private string $challengePrefix = 'cap_c_',
|
||||
private string $tokenPrefix = 'cap_t_',
|
||||
) {}
|
||||
|
||||
public function storeChallenge(string $token, array $challenge): void
|
||||
{
|
||||
$ttl = max(1, (int)ceil(($challenge['expires'] - (int)(microtime(true) * 1000)) / 1000));
|
||||
$this->cache->set($this->key($this->challengePrefix, $token), $challenge, $ttl);
|
||||
}
|
||||
|
||||
public function readChallenge(string $token): ?array
|
||||
{
|
||||
$val = $this->cache->get($this->key($this->challengePrefix, $token));
|
||||
return is_array($val) ? $val : null;
|
||||
}
|
||||
|
||||
public function deleteChallenge(string $token): void
|
||||
{
|
||||
$this->cache->delete($this->key($this->challengePrefix, $token));
|
||||
}
|
||||
|
||||
public function deleteExpiredChallenges(): void
|
||||
{
|
||||
// Cache backend handles expiry via TTL.
|
||||
}
|
||||
|
||||
public function storeToken(string $key, int $expiresMs): void
|
||||
{
|
||||
$ttl = max(1, (int)ceil(($expiresMs - (int)(microtime(true) * 1000)) / 1000));
|
||||
$this->cache->set($this->key($this->tokenPrefix, $key), $expiresMs, $ttl);
|
||||
}
|
||||
|
||||
public function readToken(string $key): ?int
|
||||
{
|
||||
$val = $this->cache->get($this->key($this->tokenPrefix, $key));
|
||||
return is_int($val) ? $val : null;
|
||||
}
|
||||
|
||||
public function deleteToken(string $key): void
|
||||
{
|
||||
$this->cache->delete($this->key($this->tokenPrefix, $key));
|
||||
}
|
||||
|
||||
public function deleteExpiredTokens(): void
|
||||
{
|
||||
// Cache backend handles expiry via TTL.
|
||||
}
|
||||
|
||||
private function key(string $prefix, string $raw): string
|
||||
{
|
||||
// PSR-16 caps keys at 64 chars and reserves some characters; we hash
|
||||
// for safety across implementations, then truncate so prefix+hash
|
||||
// never exceeds the limit. 232+ bits of entropy is well beyond what
|
||||
// we need for a cache key and well clear of birthday-bound concerns.
|
||||
$room = max(8, 64 - strlen($prefix));
|
||||
return $prefix . substr(hash('sha256', $raw), 0, $room);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TrilbyMedia\Cap\Storage;
|
||||
|
||||
/**
|
||||
* Storage for redeemed verification tokens (key → expiresMs).
|
||||
* Key is formatted "{id}:{sha256(vertoken)}".
|
||||
*/
|
||||
interface TokenStorageInterface
|
||||
{
|
||||
public function storeToken(string $key, int $expiresMs): void;
|
||||
|
||||
public function readToken(string $key): ?int;
|
||||
|
||||
public function deleteToken(string $key): void;
|
||||
|
||||
public function deleteExpiredTokens(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user