Lue dans les fichiers PHP du paquet au commit fa677c524359 : ses espaces de noms, classes, interfaces et énumérations, leurs constantes, propriétés et méthodes publiques avec leur PHPDoc, et les options que prennent ses fabriques. Rien n’en est exécuté.
ChallengeInterfaceinterfaceIs this token valid? A captcha: the page shows a widget, the visitor's browser earns a token and posts it with the form, the site asks the gateway about it.
2 methods
ChallengeIssuerInterfaceinterfaceA captcha whose challenge the site itself issues - ALTCHA: the page asks the site for a challenge, solves it, posts the solution. Nobody else is involved, so somebody on the site's side has to serve it: a route (the Symfony bridge has one), or the widget, which carries one inline when no address is configured.
1 method
ClassifierInterfaceinterfaceIs this content spam? What a visitor wrote - a comment, a message, a sign-up - and who wrote it, read by a classifier.
2 methods
ConfigclassA gateway's configuration while its factory builds it: the options given, the factory's defaults under them, and the keys that must be filled.
6 methods
GatewayFactoryclassThe Omnibus way, at its simplest: a gateway's factory fills a Config - its name ("omnishield.factory_name"), its title ("omnishield.factory_title"), the options it needs ("omnishield.required_options") and the defaults of the others - then builds the gateway from it.
3 methods
GatewayFactoryInterfaceinterfaceBuilds a gateway from its options (a site key, a secret, a threshold...).
2 methods
GatewayInterfaceinterfaceOne guard, configured: a captcha, a spam classifier, a list of known abusers. It answers one of three questions - ChallengeInterface (is this token valid?), ClassifierInterface (is this content spam?), ReputationInterface (are this address, this e-mail, this name known for abuse?) - and its Capabilities say which, before anything is asked.
3 methods
LocalizableInterfaceinterfaceA captcha whose widget shows texts the site can give it - in the visitor's language, or in words of its own ("I'm not a robot", "Verifying...", "Verified").
3 methods
RegistryclassThe application's gateways by name, each built once from its factory and options:
11 methods
ReputationInterfaceinterfaceAre this IP address, this e-mail, this name known for abuse? A list, local or someone else's, asked about whoever submits a form.
1 method
WidgetHtmlclassA widget's markup, printed by the page's printer when it reaches the page, not before: a form's view built and thrown away (a view made to collect a page's assets, a field taken out of the view), or built before another, would otherwise take its provider's script with it, and the widget the page shows would never run. Printed once, then the same markup however many times it is read.
3 methods · 1 property
WidgetPrinterclassPrints widgets for one page: a provider's script once, however many forms the page holds - with the first widget printed, so a form's field asks for its markup with later(), printed when the page shows it. One printer per response - reset() between two, in a worker that serves many.
3 methods
OmnishieldBundleclassOmnishield in a Symfony application: the gateway packages installed (omnishield/altcha, turnstile, recaptcha, akismet, stopforumspam, disposable) registered, the gateways built from configuration and injectable by their name, and what a form needs around a captcha:
5 methods
WidgetLocalizerclassA captcha's widget in the visitor's language: for a gateway that takes texts (LocalizableInterface), the request's locale - or the language the gateway was given - and the texts of the bridge's translation domain "omnishield" - "<gateway>.<text>", French, English, German and Japanese shipped, an application's own catalogue over them -, the gateway's own `strings` option winning over both. A text the catalogue does not hold is left to the widget. A regional locale ("fr_BE") without texts of its own is told to the widget as its language ("fr"), the texts being the language's.
2 methods · 1 constant
Omnishield\Bridge\Symfony\Controller
ChallengeControllerclassA fresh challenge for a captcha the site issues itself (ALTCHA): what the widget fetches when its gateway's challenge_url points here. Never cached.
2 methods
Omnishield\Bridge\Symfony\EventListener
ScriptListenerclassThe scripts a gateway package ships, served by the site itself - the ALTCHA widget (omnishield/altcha's public/altcha.min.js) at /omnishield/altcha/3.3.0/altcha.min.js - so that the page reaches nobody: no CDN sees the visitor. Answered before routing, with no route to import and no asset pipeline (neither Webpack Encore nor AssetMapper); its address carries the version, so it is cached for a year.
2 methods
Omnishield\Bridge\Symfony\Form
ChallengeTypeclassA captcha in a form: a field mapped to nothing that renders the gateway's widget and carries the PassesChallenge constraint.
5 methods
Omnishield\Bridge\Symfony\Validator
PassesChallengeclassThe value is a token the captcha accepts: posted, valid, unused, for this action and host when they are given.
1 method · 7 properties · 3 constants
PassesChallengeValidatorclassAsks the captcha about the token: the visitor's address from the current request (getClientIp(): set trusted_proxies behind a proxy), the action and the host from the constraint. A provider that does not answer refuses the value unless told to accept it; a key the provider refuses is an error of the site's, not the visitor's: it is thrown.
2 methods
OmnishieldExtensionclassA captcha's widget in a template, for a form that is not a Symfony form:
4 methods
InvalidConfigExceptionclassA gateway misconfigured: an option missing, an unknown factory, a submission without what the provider needs.
InvalidKeyExceptionclassThe provider refused the site's key or secret: the configuration is wrong (a key mistyped, revoked, for another site), whatever the visitor did.
NotSupportedExceptionclassThe gateway does not do that: a captcha asked whether content is spam, a list that takes no report. Its Capabilities say so beforehand.
2 methods
OmnishieldExceptioninterfaceEvery exception Omnishield throws.
ProviderExceptionclassThe provider answered with an error rather than a verdict: a request it could not read, an alert on the account. Not a refusal - a token that does not hold is a Verdict that did not pass - and not a silence either (UnreachableException).
1 method · 2 properties
UnreachableExceptionclassThe provider did not answer: no connection, a timeout, a server error, a quota exceeded. Nothing was decided about the token, the content or the identity - whether to let the form through is the application's call.
AnswerclassA provider's answer, read whole: its status, headers (lower-case names) and body. send() is the one place the family's gateways call through: no connection, a timeout, a server error or a quota exceeded is an UnreachableException - the provider said nothing about the request - and anything else comes back for the gateway to read. The request's secrets stay out of what it throws: the HTTP client's message with the key's parameters masked (Redaction::text(), api_key=***), its exception wrapped as a copy (Redaction::copy()), no trace keeping the arguments where the options stood.
4 methods · 3 properties
RedactionclassWhat a failed call may carry of its credentials, taken out before the failure leaves the package: an HTTP client's exception quotes the URL called (a key in its query, a password in its user part), and every exception's trace keeps the arguments of the calls under it (the request options, headers and query with the key) unless PHP runs with zend.exception_ignore_args.
5 methods · 1 constant
AttemptclassA token to check, and what it should match: the visitor's address (given to providers that take it), the action the form was for, the host the page was served from.
3 methods · 4 properties
CapabilitiesclassWhat a gateway is, said before it is asked anything: which of the three questions it answers, through whom, with what.
3 methods · 9 properties
ClassificationclassWhat a classifier said of a submission: ham, spam or flagrant spam, and why.
4 methods · 3 properties
IdentityclassWho submits a form, as a list reads them: an address, an e-mail, a name. Any may be missing.
3 methods · 3 properties
LabelenumWhat a classifier made of a submission.
3 cases
ReputationclassWhat a list knows of an identity: known for abuse or not, how often, how surely, how recently - and which part of it matched.
4 methods · 6 properties · 5 constants
SubmissionclassWhat a visitor submitted, as a classifier reads it: the text, who wrote it, from where. Strings and one date - never an account, an entity or a request: the application fills it from its own.
2 methods · 12 properties · 7 constants
VerdictclassWhat a captcha said of a token: passed or not, and why - in the family's words (reasons), and in the provider's own (codes).
4 methods · 7 properties · 8 constants
WidgetclassWhat the page shows for a captcha: a script, an element and its attributes, the field the visitor's browser posts the token in - and, before anything is loaded, who else the browser will talk to.
3 methods · 12 properties
CacheReplayStoreclassSpent tokens in a PSR-6 cache pool (psr/cache, not required by this package): shared between the requests of every process that uses the pool - a filesystem pool on one server, Redis or Memcached on several.
2 methods
InMemoryReplayStoreclassSpent tokens in this process's memory: for tests, a worker that handles every request, a script. Behind PHP-FPM each request starts with an empty store - use a shared one there (CacheReplayStore, your database).
3 methods
ReplayStoreInterfaceinterfaceWhere spent tokens are remembered, until they would have lapsed anyway: a token serves once. A provider that checks tokens on its servers (Turnstile, reCAPTCHA) refuses a second use itself; a challenge the site issues and checks alone (ALTCHA) cannot know it saw a solution before - this store does.
1 method
FixedGatewayclassA gateway that always says the same, for an application's own tests: it passes every token and every submission, or refuses them all - and it answers the three questions, so one configuration stands in for any gateway (when@test: factory: fixed).
11 methods · 2 properties · 2 constants
FixedGatewayFactoryclassThe fixed gateway, by configuration - for an application's test environment:
1 option
GatewayContractTeststraitWhat every gateway does, as PHPUnit tests: a gateway package's test case uses this trait, gives its factory, the options that build a working gateway and a few answers its provider recorded - and the suite checks the family's contract, question by question, for the questions the gateway's Capabilities claim:
9 methods
Omnishield
ChallengeInterface interface
interface ChallengeInterface extends GatewayInterface ChallengeInterface.php:14
Is this token valid? A captcha: the page shows a widget, the visitor's browser earns a token and posts it with the form, the site asks the gateway about it.
ChallengeInterface::widget()
public function widget(?string $action = null): WidgetWhat the page shows: a script, an element and its attributes, the field the token is posted in.
$actionwhat the form is for ("contact", "signup"): the provider signs it into the token where it can, and verify() checks it back
ChallengeInterface::verify()
public function verify(Attempt $attempt): VerdictWhether the token holds - signed, unexpired, unused, for this action and this host when the attempt names them. A refusal is a Verdict that did not pass, never an exception.
- Throws
Exception\UnreachableException when the provider did not answer
- Throws
Exception\InvalidKeyException when it refused the site's secret
ChallengeIssuerInterface interface
interface ChallengeIssuerInterface extends ChallengeInterface ChallengeIssuerInterface.php:12
A captcha whose challenge the site itself issues - ALTCHA: the page asks the site for a challenge, solves it, posts the solution. Nobody else is involved, so somebody on the site's side has to serve it: a route (the Symfony bridge has one), or the widget, which carries one inline when no address is configured.
ChallengeIssuerInterface::issue()
public function issue(?string $action = null): arrayA fresh challenge, as the widget fetches it (JSON). Never cache it: one challenge, one solution, one form.
ClassifierInterface interface
interface ClassifierInterface extends GatewayInterface ClassifierInterface.php:12
Is this content spam? What a visitor wrote - a comment, a message, a sign-up - and who wrote it, read by a classifier.
ClassifierInterface::classify()
public function classify(Submission $submission): ClassificationHam, spam, or spam flagrant enough to drop unseen.
- Throws
Exception\UnreachableException when the provider did not answer
- Throws
Exception\InvalidKeyException when it refused the site's key
ClassifierInterface::report()
public function report(Submission $submission, bool $spam): voidTeach the classifier: $spam true for a spam it let through, false for a ham it held. Send the submission as it was classified.
- Throws
Exception\NotSupportedException when the provider takes no report
- Throws
Exception\UnreachableException
- Throws
Exception\InvalidKeyException
Config class
final class Config extends ArrayObject Config.php:13
A gateway's configuration while its factory builds it: the options given, the factory's defaults under them, and the keys that must be filled.
Config::defaults()
public function defaults(array $defaults): selfSets each key not set yet.
Config::validateNotEmpty()
public function validateNotEmpty(array $keys): voidConfig::get()
public function get(string $key, mixed $default = null): mixedConfig::string()
public function string(string $key): ?stringA string option; null when it is empty.
Config::bool()
public function bool(string $key): boolA boolean option, as an environment variable gives it too: "1", "true", "yes", "on".
Config::list()
public function list(string $key): arrayA list given as one environment variable arrives as a single comma-separated string: split here, at run time.
GatewayFactory class
abstract class GatewayFactory implements GatewayFactoryInterface GatewayFactory.php:16
The Omnibus way, at its simplest: a gateway's factory fills a Config - its name ("omnishield.factory_name"), its title ("omnishield.factory_title"), the options it needs ("omnishield.required_options") and the defaults of the others - then builds the gateway from it.
Nothing here calls anything: a factory whose provider is reached over HTTP takes its client in its own constructor - the application's, a MockHttpClient in a test - and one that keeps something between requests (a spent token) takes its store there too.
GatewayFactory::getName()
public function getName(): stringGatewayFactory::create()
public function create(array $options = []): GatewayInterfaceGatewayFactory::createConfig()
public function createConfig(array $options = []): ConfigGatewayFactoryInterface interface
interface GatewayFactoryInterface GatewayFactoryInterface.php:6
Builds a gateway from its options (a site key, a secret, a threshold...).
GatewayFactoryInterface::getName()
public function getName(): stringThe name gateways are configured with: "altcha", "turnstile".
..
GatewayFactoryInterface::create()
public function create(array $options = []): GatewayInterfaceGatewayInterface interface
interface GatewayInterface GatewayInterface.php:17
One guard, configured: a captcha, a spam classifier, a list of known abusers. It answers one of three questions - ChallengeInterface (is this token valid?), ClassifierInterface (is this content spam?), ReputationInterface (are this address, this e-mail, this name known for abuse?) - and its Capabilities say which, before anything is asked.
A gateway answers. It does not decide what to do with a refusal, nor with a provider that does not answer: the application does.
GatewayInterface::getName()
public function getName(): stringThe factory's name: "altcha", "turnstile", "akismet".
..
GatewayInterface::getTitle()
public function getTitle(): stringGatewayInterface::capabilities()
public function capabilities(): CapabilitiesWhich questions it answers, through whom, with what.
LocalizableInterface interface
interface LocalizableInterface extends ChallengeInterface LocalizableInterface.php:18
A captcha whose widget shows texts the site can give it - in the visitor's language, or in words of its own ("I'm not a robot", "Verifying...", "Verified").
$gateway instanceof LocalizableInterface && $gateway = $gateway->localized('fr', ['label' => 'Je ne suis pas un robot']);
The Symfony bridge does it for every widget it prints: the gateway's own
language if it was given one, else the request's; the texts of its
translation domain "omnishield" (<gateway>.<text>) in that language; the
gateway's own strings option over them.
LocalizableInterface::texts()
public function texts(): array- Returns
the texts its widget shows, by the name the widget gives them
LocalizableInterface::language()
public function language(): ?stringThe language the gateway itself was given (its language option), null to follow the visitor's
LocalizableInterface::localized()
public function localized(string $language, array $strings = []): staticThe same gateway, its widget in that language with these texts - any left out are the widget's own for that language, else its English.
What the gateway itself was given wins: its language over this one,
its texts (its strings option) over these.
$languagea language tag: "fr", "de", "ja", "pt-br"
$stringsthe texts, by name
Registry class
final class Registry Registry.php:20
The application's gateways by name, each built once from its factory and options:
new Registry([new AltchaGatewayFactory($replays), new AkismetGatewayFactory($http), new DisposableGatewayFactory()], [ 'forms' => ['factory' => 'altcha', 'options' => ['hmac_key' => '...']], 'comments' => ['factory' => 'akismet', 'options' => ['api_key' => '...', 'site' => 'https://example.org']], 'emails' => ['factory' => 'disposable'], ]);
$registry->challenge('forms')->verify($attempt);
Registry::__construct()
public function __construct(iterable $factories, readonly array $config)Registry::get()
public function get(string $name): GatewayInterfaceRegistry::has()
public function has(string $name): boolRegistry::challenge()
public function challenge(string $name): ChallengeInterfaceThe gateway, as a captcha.
Registry::classifier()
public function classifier(string $name): ClassifierInterfaceThe gateway, as a spam classifier.
Registry::reputation()
public function reputation(string $name): ReputationInterfaceThe gateway, as a list of known abusers.
Registry::options()
public function options(string $name): array- Returns
the options a gateway is configured with
Registry::create()
public function create(string $name, array $overrides = []): GatewayInterfaceA gateway built afresh, its configured options with $overrides over them - a key typed in a back office, say. Not kept: get() still gives the configured one.
Registry::names()
public function names(): array- Returns
the configured gateways' names
Registry::all()
public function all(): arrayRegistry::factories()
public function factories(): array- Returns
the factories installed: what
factory:may name
ReputationInterface interface
interface ReputationInterface extends GatewayInterface ReputationInterface.php:12
Are this IP address, this e-mail, this name known for abuse? A list, local or someone else's, asked about whoever submits a form.
ReputationInterface::lookup()
public function lookup(Identity $identity): ReputationWhat the list knows of the parts of the identity it reads (its Capabilities say which); the parts it does not read are ignored.
- Throws
Exception\UnreachableException when the provider did not answer
WidgetHtml class
final class WidgetHtml implements Stringable WidgetHtml.php:18
A widget's markup, printed by the page's printer when it reaches the page, not before: a form's view built and thrown away (a view made to collect a page's assets, a field taken out of the view), or built before another, would otherwise take its provider's script with it, and the widget the page shows would never run. Printed once, then the same markup however many times it is read.
$view->vars['omnishield_html'] = $printer->later($widget, $nonce); // {{ omnishield_html|raw }} $html = $html->wrap('<div hidden>', '</div>'); // still printed when shown
public readonly Widget $widget
WidgetHtml::__construct()
public function __construct(readonly WidgetPrinter $printer, public readonly Widget $widget, readonly ?string $nonce = null, readonly string $before = '', readonly string $after = '')WidgetHtml::wrap()
public function wrap(string $before, string $after): selfThe same widget between two pieces of markup, printed when it is shown.
WidgetHtml::__toString()
public function __toString(): stringWidgetPrinter class
final class WidgetPrinter WidgetPrinter.php:13
Prints widgets for one page: a provider's script once, however many forms the page holds - with the first widget printed, so a form's field asks for its markup with later(), printed when the page shows it. One printer per response - reset() between two, in a worker that serves many.
WidgetPrinter::print()
public function print(Widget $widget, ?string $nonce = null): stringWidgetPrinter::later()
public function later(Widget $widget, ?string $nonce = null): WidgetHtmlThe widget's markup, printed when it is shown (cast to a string): a view built and not shown takes no script.
WidgetPrinter::reset()
public function reset(): voidOmnishield\Bridge\Symfony
OmnishieldBundle class
final class OmnishieldBundle extends AbstractBundle OmnishieldBundle.php:76
Omnishield in a Symfony application: the gateway packages installed (omnishield/altcha, turnstile, recaptcha, akismet, stopforumspam, disposable) registered, the gateways built from configuration and injectable by their name, and what a form needs around a captcha:
omnishield: gateways: forms: { factory: altcha, options: { hmac_key: '%env(ALTCHA_HMAC_KEY)%' } } comments: { factory: akismet, options: { api_key: '%env(AKISMET_KEY)%', site: 'https://example.org' } } emails: { factory: disposable } challenge: gateway: forms # the ChallengeType's, the constraint's and omnishield_widget()'s default unreachable: reject # or accept: a provider that does not answer lets the form through serve_scripts: true # ALTCHA's widget served by the site (/omnishield/altcha/3.3.0/altcha.min.js), not a CDN replay: pool: cache.app # where spent tokens are remembered; none: this process's memory
public function __construct(ChallengeInterface $forms, ClassifierInterface $comments, ReputationInterface $emails) }$builder->add('captcha', ChallengeType::class, ['action' => 'contact']);
{{ omnishield_widget('forms', 'contact') }}Nothing is built, nor checked, when the container compiles: a gateway is built the first time it is asked for, and an option left empty only shows then (InvalidConfigException). A factory is autowired, the application's http_client given to those that call a provider when it has one. An application's own factories (a GatewayFactoryInterface) are registered too, autoconfigured.
Each piece is registered only when its component is installed - Form, Validator, Routing and HttpFoundation, Twig: none is required by omnisense/omnishield.
OmnishieldBundle::getPath()
public function getPath(): stringOmnishieldBundle::configure()
public function configure(DefinitionConfigurator $definition): voidOmnishieldBundle::prependExtension()
public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): voidOmnishieldBundle::loadExtension()
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): voidOmnishieldBundle::replayStore()
public static function replayStore(?object $pool = null): ReplayStoreInterfaceThe store of spent tokens: the pool when there is one, this process's memory otherwise.
WidgetLocalizer class
final class WidgetLocalizer WidgetLocalizer.php:22
A captcha's widget in the visitor's language: for a gateway that takes
texts (LocalizableInterface), the request's locale - or the language the
gateway was given - and the texts of the
bridge's translation domain "omnishield" - "<gateway>.<text>", French,
English, German and Japanese shipped, an application's own catalogue
over them -, the gateway's own strings option winning over both. A text
the catalogue does not hold is left to the widget. A regional locale
("fr_BE") without texts of its own is told to the widget as its language
("fr"), the texts being the language's.
const DOMAIN = 'omnishield'
WidgetLocalizer::__construct()
public function __construct(readonly ?TranslatorInterface $translator = null, readonly ?RequestStack $requests = null)WidgetLocalizer::localize()
public function localize(ChallengeInterface $gateway): ChallengeInterfaceOmnishield\Bridge\Symfony\Controller
ChallengeController class
final class ChallengeController ChallengeController.php:23
A fresh challenge for a captcha the site issues itself (ALTCHA): what the widget fetches when its gateway's challenge_url points here. Never cached.
A plain controller - no AbstractController, nothing of FrameworkBundle - imported by the application:
// config/routes/omnishield.php
return static fn (RoutingConfigurator $routes) => $routes->import(ChallengeController::class, 'attribute');omnishield.gateways.forms.options.challenge_url: /omnishield/forms/challengeChallengeController::__construct()
public function __construct(readonly Registry $registry)ChallengeController::__invoke()
public function __invoke(string $gateway, Request $request): JsonResponseOmnishield\Bridge\Symfony\EventListener
ScriptListener class
final class ScriptListener ScriptListener.php:16
The scripts a gateway package ships, served by the site itself - the ALTCHA widget (omnishield/altcha's public/altcha.min.js) at /omnishield/altcha/3.3.0/altcha.min.js - so that the page reaches nobody: no CDN sees the visitor. Answered before routing, with no route to import and no asset pipeline (neither Webpack Encore nor AssetMapper); its address carries the version, so it is cached for a year.
ScriptListener::__construct()
public function __construct(readonly array $scripts)$scriptsthe path asked => the file
ScriptListener::__invoke()
public function __invoke(RequestEvent $event): voidOmnishield\Bridge\Symfony\Form
ChallengeType class
final class ChallengeType extends AbstractType ChallengeType.php:30
A captcha in a form: a field mapped to nothing that renders the gateway's widget and carries the PassesChallenge constraint.
$builder->add('captcha', ChallengeType::class, ['action' => 'contact']); $builder->add('captcha', ChallengeType::class, ['gateway' => 'turnstile', 'hostname' => true]);
A provider's widget posts its token under its own name ("altcha", "cf-turnstile-response"), outside the form's: the field reads it from the request when the form's own data does not hold it.
ChallengeType::__construct()
public function __construct(readonly Registry $registry, readonly WidgetPrinter $printer, readonly ?RequestStack $requests = null, readonly ?string $gateway = null, readonly ?WidgetLocalizer $localizer = null)ChallengeType::buildForm()
public function buildForm(FormBuilderInterface $builder, array $options): voidChallengeType::buildView()
public function buildView(FormView $view, FormInterface $form, array $options): voidChallengeType::configureOptions()
public function configureOptions(OptionsResolver $resolver): voidChallengeType::getBlockPrefix()
public function getBlockPrefix(): stringOmnishield\Bridge\Symfony\Validator
PassesChallenge class
final class PassesChallenge extends Constraint PassesChallenge.php:14
The value is a token the captcha accepts: posted, valid, unused, for this action and host when they are given.
#[PassesChallenge(gateway: 'forms', action: 'contact')] public ?string $captcha = null;
const MISSING_ERROR = 'b8a5d4c2-6f0e-4b1a-9d3c-0e7f1a2b3c41'const FAILED_ERROR = 'b8a5d4c2-6f0e-4b1a-9d3c-0e7f1a2b3c42'const UNREACHABLE_ERROR = 'b8a5d4c2-6f0e-4b1a-9d3c-0e7f1a2b3c43'public string $missingMessage = 'Please confirm that you are not a robot.'public string $message = 'The check that you are not a robot did not pass. Please try again.'public string $unreachableMessage = 'The check that you are not a robot could not be done just now. Please try again in a moment.'public ?string $gatewaythe configured captcha; omnishield.challenge.gateway when null
public ?string $actionthe action the token must have been made for
public string|bool|null $hostnamethe host the widget must have been shown on; true: the request's
public ?bool $unreachabletrue lets the value through when the provider does not answer; null: omnishield.challenge.unreachable
PassesChallenge::__construct()
public function __construct(public ?string $gateway = null, public ?string $action = null, public string|bool|null $hostname = null, public ?bool $unreachable = null, ?string $message = null, ?string $missingMessage = null, ?string $unreachableMessage = null, ?array $groups = null, mixed $payload = null)$gatewaythe configured captcha; omnishield.challenge.gateway when null
$actionthe action the token must have been made for
$hostnamethe host the widget must have been shown on; true: the request's
$unreachabletrue lets the value through when the provider does not answer; null: omnishield.challenge.unreachable
PassesChallengeValidator class
final class PassesChallengeValidator extends ConstraintValidator PassesChallengeValidator.php:22
Asks the captcha about the token: the visitor's address from the current request (getClientIp(): set trusted_proxies behind a proxy), the action and the host from the constraint. A provider that does not answer refuses the value unless told to accept it; a key the provider refuses is an error of the site's, not the visitor's: it is thrown.
PassesChallengeValidator::__construct()
public function __construct(readonly Registry $registry, readonly ?RequestStack $requests = null, readonly ?string $gateway = null, readonly bool $acceptUnreachable = false)PassesChallengeValidator::validate()
public function validate(mixed $value, Constraint $constraint): voidOmnishield\Bridge\Twig
OmnishieldExtension class
final class OmnishieldExtension extends AbstractExtension OmnishieldExtension.php:26
A captcha's widget in a template, for a form that is not a Symfony form:
<form method="post"> ... {{ omnishield_widget() }} {# the default captcha #} {{ omnishield_widget('forms', 'contact') }} {# a captcha, an action #} {{ omnishield_widget('forms', nonce: csp_nonce) }} </form>
{% set widget = omnishield_widget_data('forms') %} {# its parts: widget.field, widget.origins... #}A provider's script is printed once per page, however many widgets.
OmnishieldExtension::__construct()
public function __construct(readonly Registry $registry, readonly WidgetPrinter $printer = new WidgetPrinter(), readonly ?string $gateway = null, readonly ?WidgetLocalizer $localizer = null)OmnishieldExtension::getFunctions()
public function getFunctions(): arrayOmnishieldExtension::widget()
public function widget(?string $gateway = null, ?string $action = null, ?string $nonce = null): stringOmnishieldExtension::data()
public function data(?string $gateway = null, ?string $action = null): WidgetOmnishield\Exception
InvalidConfigException class
final class InvalidConfigException extends LogicException implements OmnishieldException InvalidConfigException.php:6
A gateway misconfigured: an option missing, an unknown factory, a submission without what the provider needs.
InvalidKeyException class
final class InvalidKeyException extends ProviderException InvalidKeyException.php:9
The provider refused the site's key or secret: the configuration is wrong (a key mistyped, revoked, for another site), whatever the visitor did.
NotSupportedException class
final class NotSupportedException extends LogicException implements OmnishieldException NotSupportedException.php:11
The gateway does not do that: a captcha asked whether content is spam, a list that takes no report. Its Capabilities say so beforehand.
NotSupportedException::question()
public static function question(GatewayInterface $gateway, string $name, string $question): selfNotSupportedException::operation()
public static function operation(string $gateway, string $operation, string $why = ''): selfOmnishieldException interface
interface OmnishieldException extends Throwable OmnishieldException.php:6
Every exception Omnishield throws.
ProviderException class
class ProviderException extends RuntimeException implements OmnishieldException ProviderException.php:11
The provider answered with an error rather than a verdict: a request it could not read, an alert on the account. Not a refusal - a token that does not hold is a Verdict that did not pass - and not a silence either (UnreachableException).
public readonly string $providerpublic readonly ?string $providerCodeThe provider's own error code, when it gave one
ProviderException::__construct()
public function __construct(public readonly string $provider, string $message, public readonly ?string $providerCode = null, ?\Throwable $previous = null)UnreachableException class
final class UnreachableException extends ProviderException UnreachableException.php:10
The provider did not answer: no connection, a timeout, a server error, a quota exceeded. Nothing was decided about the token, the content or the identity - whether to let the form through is the application's call.
Omnishield\Http
Answer class
final readonly class Answer Answer.php:21
A provider's answer, read whole: its status, headers (lower-case names) and body. send() is the one place the family's gateways call through: no connection, a timeout, a server error or a quota exceeded is an UnreachableException - the provider said nothing about the request - and anything else comes back for the gateway to read. The request's secrets stay out of what it throws: the HTTP client's message with the key's parameters masked (Redaction::text(), api_key=***), its exception wrapped as a copy (Redaction::copy()), no trace keeping the arguments where the options stood.
public int $statuspublic array $headerspublic string $body
Answer::__construct()
public function __construct(public int $status, public array $headers, public string $body, string $provider)Answer::send()
public static function send(HttpClientInterface $http, string $provider, string $method, string $url, array $options = []): self$optionsthe HTTP client's options: body, json, query, headers, timeout
Answer::header()
public function header(string $name): ?stringAnswer::json()
public function json(): arrayThe body as a JSON object.
Redaction class
final class Redaction Redaction.php:22
What a failed call may carry of its credentials, taken out before the failure leaves the package: an HTTP client's exception quotes the URL called (a key in its query, a password in its user part), and every exception's trace keeps the arguments of the calls under it (the request options, headers and query with the key) unless PHP runs with zend.exception_ignore_args.
copy() gives the previous exception to wrap instead of the client's own: the same class and code, its message with the key's parameters, the user part and every secret value replaced by ***, its trace without arguments, its own previous copied the same way. Properties of the client's class beyond those of \Exception are not copied (a Symfony HttpException's response, a PSR-18 exception's request: their URL holds the key). withoutArguments() takes the arguments out of the trace of the exception thrown in its place.
const MASK = '***'
Redaction::secrets()
public static function secrets(string $url, array $options = []): arrayThe secrets a request carries: the values of its query parameters, headers and body fields named as credentials (key, token, secret, password, signature, auth...), a bearer token, a basic password, the URL's user part.
$optionsthe HTTP client's options
Redaction::sensitive()
public static function sensitive(string $name): boolWhether a parameter, header or field of this name holds a credential.
Redaction::text()
public static function text(string $text, array $secrets = []): stringA text without credentials: in every address, the user part and the parameters named as credentials masked (api_key=***); then every secret given, raw or URL-encoded, masked wherever it stands.
Redaction::copy()
public static function copy(\Throwable $failure, array $secrets = []): \ThrowableA copy of a failure to wrap in its place: its class, code, file and line, its message through text(), its trace without arguments, its previous copied the same way.
Redaction::withoutArguments()
public static function withoutArguments(\Throwable $failure): \ThrowableThe failure given, the arguments out of its trace (and out of its previous ones'), as zend.exception_ignore_args would have left it.
Omnishield\Model
Attempt class
final readonly class Attempt Attempt.php:10
A token to check, and what it should match: the visitor's address (given to providers that take it), the action the form was for, the host the page was served from.
public string $tokenwhat the widget posted; "" when nothing was
public ?string $ipthe visitor's address, as the application sees it (behind its proxies)
public ?string $actionthe action the token must have been made for; null: not checked
public ?string $hostnamethe host the widget must have been shown on; null: not checked
Attempt::__construct()
public function __construct(public string $token, public ?string $ip = null, public ?string $action = null, public ?string $hostname = null)$tokenwhat the widget posted; "" when nothing was
$ipthe visitor's address, as the application sees it (behind its proxies)
$actionthe action the token must have been made for; null: not checked
$hostnamethe host the widget must have been shown on; null: not checked
Attempt::fromPost()
public static function fromPost(array $post, Widget $widget, ?string $ip = null, ?string $hostname = null): selfThe token from a form's posted fields ($_POST, or a framework's request), under the widget's field.
Attempt::isEmpty()
public function isEmpty(): boolCapabilities class
final readonly class Capabilities Capabilities.php:9
What a gateway is, said before it is asked anything: which of the three questions it answers, through whom, with what.
public bool $challengeit answers "is this token valid?" (ChallengeInterface)
public bool $classifierit answers "is this content spam?" (ClassifierInterface)
public bool $reputationit answers "is this identity known for abuse?" (ReputationInterface)
public bool $thirdPartywhether what it checks - a token, a text, an address - leaves the site for someone else's
whether its widget sets or reads cookies in the visitor's browser
public bool $scoreswhether a Verdict carries a score
public bool $actionswhether it checks the action a token was made for
public bool $reportswhether it takes reports back (ClassifierInterface::report(), a list's own report())
public array $readswhat of an Identity a reputation reads: ip, email, name
Capabilities::__construct()
public function __construct(public bool $challenge = false, public bool $classifier = false, public bool $reputation = false, public bool $thirdParty = true, public bool $cookies = false, public bool $scores = false, public bool $actions = false, public bool $reports = false, public array $reads = [])$challengeit answers "is this token valid?" (ChallengeInterface)
$classifierit answers "is this content spam?" (ClassifierInterface)
$reputationit answers "is this identity known for abuse?" (ReputationInterface)
$thirdPartywhether what it checks - a token, a text, an address - leaves the site for someone else's
$cookieswhether its widget sets or reads cookies in the visitor's browser
$scoreswhether a Verdict carries a score
$actionswhether it checks the action a token was made for
$reportswhether it takes reports back (ClassifierInterface::report(), a list's own report())
$readswhat of an Identity a reputation reads: ip, email, name
Capabilities::questions()
public function questions(): array- Returns
the questions it answers, by their contract: challenge, classifier, reputation
Capabilities::toArray()
public function toArray(): arrayClassification class
final readonly class Classification Classification.php:6
What a classifier said of a submission: ham, spam or flagrant spam, and why.
public Label $labelpublic array $reasonswhat the provider gave as reasons or alerts, in its own words
public array $datawhat else it answered (headers, a recheck delay)
Classification::__construct()
public function __construct(public Label $label, public array $reasons = [], public array $data = [])$reasonswhat the provider gave as reasons or alerts, in its own words
$datawhat else it answered (headers, a recheck delay)
Classification::ham()
public static function ham(): selfClassification::isSpam()
public function isSpam(): boolSpam, flagrant or not.
Classification::isFlagrant()
public function isFlagrant(): boolIdentity class
final readonly class Identity Identity.php:6
Who submits a form, as a list reads them: an address, an e-mail, a name. Any may be missing.
public ?string $ippublic ?string $emailpublic ?string $name
Identity::__construct()
public function __construct(public ?string $ip = null, public ?string $email = null, public ?string $name = null)Identity::domain()
public function domain(): ?stringThe e-mail's domain, lower case, without a trailing dot; null without an e-mail.
Identity::isEmpty()
public function isEmpty(): boolLabel enum
enum Label Label.php:6
What a classifier made of a submission.
case HAM = 'ham'Not spam.
case SPAM = 'spam'Spam: hold it for a person to look at.
case FLAGRANT = 'flagrant'Spam beyond doubt: it may be dropped unseen.
Reputation class
final readonly class Reputation Reputation.php:9
What a list knows of an identity: known for abuse or not, how often, how surely, how recently - and which part of it matched.
const IP = 'ip'const EMAIL = 'email'const NAME = 'name'const DISPOSABLE = 'disposable'The e-mail's domain is a disposable one.
const TOR = 'tor'The address is a Tor exit node.
public bool $knownwhether the list holds it for abusive, at the gateway's threshold
public int $frequencyhow many times it was reported, where the list counts
public float $confidencefrom 0 to 100: how surely it is abusive, as the list reckons
public ?\DateTimeImmutable $lastSeenwhen it was last reported
public array $reasonswhich parts matched: the constants above
public array $datawhat else the list answered, part by part
Reputation::__construct()
public function __construct(public bool $known, public int $frequency = 0, public float $confidence = 0.0, public ?\DateTimeImmutable $lastSeen = null, public array $reasons = [], public array $data = [])$knownwhether the list holds it for abusive, at the gateway's threshold
$frequencyhow many times it was reported, where the list counts
$confidencefrom 0 to 100: how surely it is abusive, as the list reckons
$lastSeenwhen it was last reported
$reasonswhich parts matched: the constants above
$datawhat else the list answered, part by part
Reputation::unknown()
public static function unknown(array $data = []): selfReputation::matched()
public function matched(string $part): boolReputation::toArray()
public function toArray(): arraySubmission class
final readonly class Submission Submission.php:10
What a visitor submitted, as a classifier reads it: the text, who wrote it, from where. Strings and one date - never an account, an entity or a request: the application fills it from its own.
const COMMENT = 'comment'const REPLY = 'reply'const FORUM_POST = 'forum-post'const BLOG_POST = 'blog-post'const CONTACT_FORM = 'contact-form'const SIGNUP = 'signup'const MESSAGE = 'message'public string $contentwhat was written
the name given
public ?string $emailthe e-mail given
public ?string $urlthe address the author gave as theirs
public ?string $ipthe visitor's address
public ?string $userAgentthe browser's User-Agent
public ?string $referrerthe page the visitor came from (Referer)
public ?string $permalinkthe page the content was submitted on, or will be shown at
public ?string $sitethe site's home page: "https://example.org"
public ?string $languagethe site's language: "fr", "fr_FR"; several, comma-separated
public string $typewhat it is: the constants above
public ?\DateTimeImmutable $datewhen it was submitted
Submission::__construct()
public function __construct(public string $content, public ?string $author = null, public ?string $email = null, public ?string $url = null, public ?string $ip = null, public ?string $userAgent = null, public ?string $referrer = null, public ?string $permalink = null, public ?string $site = null, public ?string $language = null, public string $type = self::COMMENT, public ?\DateTimeImmutable $date = null)$contentwhat was written
$authorthe name given
$emailthe e-mail given
$urlthe address the author gave as theirs
$ipthe visitor's address
$userAgentthe browser's User-Agent
$referrerthe page the visitor came from (Referer)
$permalinkthe page the content was submitted on, or will be shown at
$sitethe site's home page: "https://example.org"
$languagethe site's language: "fr", "fr_FR"; several, comma-separated
$typewhat it is: the constants above
$datewhen it was submitted
Submission::identity()
public function identity(): IdentityThe submitter as a list reads them.
Verdict class
final readonly class Verdict Verdict.php:9
What a captcha said of a token: passed or not, and why - in the family's words (reasons), and in the provider's own (codes).
const MISSING = 'missing'No token was posted.
const INVALID = 'invalid'The token is not one the provider made for this site: malformed, forged, for another key.
const EXPIRED = 'expired'The token lapsed.
const DUPLICATE = 'duplicate'The token was used already.
const SPENT = 'spent'The token lapsed or was used already: the provider does not say which.
const ACTION = 'action'It was made for another action than the one expected.
const HOSTNAME = 'hostname'It was made on another host than the one expected.
const SCORE = 'score'Its score is under the threshold.
public bool $passedpublic ?float $scorefrom 0.0 (a bot) to 1.0 (a person), where the provider scores
public ?string $actionthe action the token was made for, as the provider read it
public ?string $hostnamethe host the widget was shown on, as the provider read it
public ?\DateTimeImmutable $atwhen the challenge was solved
public array $reasonswhy it did not pass: the constants above
public array $codesthe provider's own error codes, or its own reasons
Verdict::__construct()
public function __construct(public bool $passed, public ?float $score = null, public ?string $action = null, public ?string $hostname = null, public ?\DateTimeImmutable $at = null, public array $reasons = [], public array $codes = [])$scorefrom 0.0 (a bot) to 1.0 (a person), where the provider scores
$actionthe action the token was made for, as the provider read it
$hostnamethe host the widget was shown on, as the provider read it
$atwhen the challenge was solved
$reasonswhy it did not pass: the constants above
$codesthe provider's own error codes, or its own reasons
Verdict::fail()
public static function fail(string $reason, array $codes = []): selfVerdict::failedFor()
public function failedFor(string $reason): boolVerdict::toArray()
public function toArray(): arrayWidget class
final readonly class Widget Widget.php:13
What the page shows for a captcha: a script, an element and its attributes, the field the visitor's browser posts the token in - and, before anything is loaded, who else the browser will talk to.
html() prints it as it is; an application that renders its own markup reads the parts.
public string $fieldthe posted field that carries the token: "altcha", "cf-turnstile-response", "g-recaptcha-response"
public ?string $scriptthe provider's script: an address, absolute or on the site
public array $scriptAttributesthe script element's attributes beyond src: type, async, defer, integrity, crossorigin
public string $tagthe element the script turns into a widget: "altcha-widget", "div"
public array $attributesits attributes: true prints the name alone, false leaves it out
public ?string $contentwhat goes inside the element, as HTML
public ?string $inlinea short script of this package's, after the element (reCAPTCHA's invisible and score modes)
public ?string $siteKeythe provider's public key for the site, when it has one
public ?string $actionthe action it was made for
public bool $thirdPartywhether the check itself goes through someone other than the site
whether the provider sets or reads cookies in the visitor's browser
public array $originswho the visitor's browser talks to besides the site ("https://challenges.cloudflare.com"): name them first, then load
Widget::__construct()
public function __construct(public string $field, public ?string $script = null, public array $scriptAttributes = [], public string $tag = 'div', public array $attributes = [], public ?string $content = null, public ?string $inline = null, public ?string $siteKey = null, public ?string $action = null, public bool $thirdParty = false, public bool $cookies = false, public array $origins = [])$fieldthe posted field that carries the token: "altcha", "cf-turnstile-response", "g-recaptcha-response"
$scriptthe provider's script: an address, absolute or on the site
$scriptAttributesthe script element's attributes beyond src: type, async, defer, integrity, crossorigin
$tagthe element the script turns into a widget: "altcha-widget", "div"
$attributesits attributes: true prints the name alone, false leaves it out
$contentwhat goes inside the element, as HTML
$inlinea short script of this package's, after the element (reCAPTCHA's invisible and score modes)
$siteKeythe provider's public key for the site, when it has one
$actionthe action it was made for
$thirdPartywhether the check itself goes through someone other than the site
$cookieswhether the provider sets or reads cookies in the visitor's browser
$originswho the visitor's browser talks to besides the site ("https://challenges.cloudflare.com"): name them first, then load
Widget::reachesOthers()
public function reachesOthers(): boolWhether the visitor's browser talks to anyone but the site: say so, and wait for consent, before html().
Widget::html()
public function html(?string $nonce = null, bool $script = true): stringThe widget's markup: the script, the element, the inline script.
$noncethe page's Content-Security-Policy nonce, put on both scripts
$scriptfalse leaves the provider's script out - already printed for another form of the page
Omnishield\Replay
CacheReplayStore class
final class CacheReplayStore implements ReplayStoreInterface CacheReplayStore.php:17
Spent tokens in a PSR-6 cache pool (psr/cache, not required by this package): shared between the requests of every process that uses the pool - a filesystem pool on one server, Redis or Memcached on several.
PSR-6 has no "add if absent": between this request's read and its write, another may read the same token as unspent. The window is a few microseconds, for a token solved once; where that matters, implement ReplayStoreInterface on an atomic store (a unique key, SET NX).
CacheReplayStore::__construct()
public function __construct(readonly CacheItemPoolInterface $pool, readonly string $prefix = 'omnishield.spent.')CacheReplayStore::spend()
public function spend(string $id, \DateTimeImmutable $until): boolInMemoryReplayStore class
final class InMemoryReplayStore implements ReplayStoreInterface InMemoryReplayStore.php:10
Spent tokens in this process's memory: for tests, a worker that handles every request, a script. Behind PHP-FPM each request starts with an empty store - use a shared one there (CacheReplayStore, your database).
InMemoryReplayStore::__construct()
public function __construct(readonly ?\Closure $clock = null)InMemoryReplayStore::spend()
public function spend(string $id, \DateTimeImmutable $until): boolInMemoryReplayStore::count()
public function count(): intReplayStoreInterface interface
interface ReplayStoreInterface ReplayStoreInterface.php:16
Where spent tokens are remembered, until they would have lapsed anyway: a token serves once. A provider that checks tokens on its servers (Turnstile, reCAPTCHA) refuses a second use itself; a challenge the site issues and checks alone (ALTCHA) cannot know it saw a solution before - this store does.
spend() must be atomic where requests run side by side: two requests spending the same token at the same moment, only one may be told "first". A database's unique key, Redis's SET NX, a lock around the check do it.
ReplayStoreInterface::spend()
public function spend(string $id, \DateTimeImmutable $until): boolMarks the token spent until $until.
$idan identifier of the token: hash it if it is long or secret
- Returns
true the first time, false when it was spent already
Omnishield\Testing
FixedGateway class
final class FixedGateway implements ChallengeInterface, ClassifierInterface, ReputationInterface FixedGateway.php:29
A gateway that always says the same, for an application's own tests: it passes every token and every submission, or refuses them all - and it answers the three questions, so one configuration stands in for any gateway (when@test: factory: fixed).
Its widget is a hidden field already holding a token: a form submitted by a test client carries it as a browser would. Only a token left empty is refused when it passes - a test can still check that a form without one is turned away.
const FIELD = 'omnishield-token'const TOKEN = 'omnishield-fixed-token'public array $reports = []public readonly bool $pass
FixedGateway::__construct()
public function __construct(public readonly bool $pass = true)FixedGateway::passing()
public static function passing(): selfFixedGateway::failing()
public static function failing(): selfFixedGateway::getName()
public function getName(): stringFixedGateway::getTitle()
public function getTitle(): stringFixedGateway::capabilities()
public function capabilities(): CapabilitiesFixedGateway::widget()
public function widget(?string $action = null): WidgetFixedGateway::verify()
public function verify(Attempt $attempt): VerdictFixedGateway::classify()
public function classify(Submission $submission): ClassificationFixedGateway::report()
public function report(Submission $submission, bool $spam): voidFixedGateway::lookup()
public function lookup(Identity $identity): ReputationFixedGatewayFactory class
final class FixedGatewayFactory extends GatewayFactory FixedGatewayFactory.php:16
The fixed gateway, by configuration - for an application's test environment:
options: pass: true # false: every token, submission and identity is refused
Options
| Option | Default | |
|---|---|---|
pass | true |
GatewayContractTests trait
trait GatewayContractTests GatewayContractTests.php:76
What every gateway does, as PHPUnit tests: a gateway package's test case uses this trait, gives its factory, the options that build a working gateway and a few answers its provider recorded - and the suite checks the family's contract, question by question, for the questions the gateway's Capabilities claim:
- the factory's name is the gateway's; the Registry builds it from {factory, options}; a required option left out is an InvalidConfigException;
- what the Capabilities claim is what the gateway is: each question it answers is its interface, and the Registry refuses the others with a NotSupportedException; a classifier that takes no report says so the same way; a gateway that sends nothing to a third party makes no HTTP request;
- its answers keep their invariants: a Verdict passes when it has no reason, its reasons are the family's, its score lies between 0 and 1 and only where the gateway scores; a Classification's label is a Label; a Reputation's confidence lies between 0 and 100, a known one says which parts matched, among the parts the list reads;
- nothing is asked for nothing: an empty token is a Verdict missing, an empty identity is unknown, without a request;
- the provider's errors are the family's exceptions (third-party gateways): no answer, HTTP 5xx and 429 are an UnreachableException; HTTP 401 and 403 an InvalidKeyException when the gateway sends a key or a secret, a ProviderException otherwise; an answer it cannot read a ProviderException - never a TypeError, a JsonException or a silent refusal;
- the recorded cases are answered as recorded, every recorded answer read.
No test reaches the network: the factory is handed a MockHttpClient.
final class TurnstileContractTest extends TestCase { use GatewayContractTests;
protected function factory(HttpClientInterface $http): GatewayFactoryInterface { return new TurnstileGatewayFactory($http); }
protected function options(): array { return ['site_key' => '1x00000000000000000000AA', 'secret' => '1x0000000000000000000000000000000AA']; }
protected function recordedCases(): iterable
{
yield 'passes' => [new Attempt('XXXX.DUMMY.TOKEN.XXXX'), true, [new MockResponse(file_get_contents(__DIR__.'/Fixtures/siteverify-testing-passes.json'))]];
yield 'spent' => [new Attempt('XXXX.DUMMY.TOKEN.XXXX'), false, [new MockResponse(file_get_contents(__DIR__.'/Fixtures/siteverify-testing-spent.json'))]];
}
}Requires phpunit/phpunit and symfony/http-client (the package's require-dev).
GatewayContractTests::testTheFactoryNamesTheGatewayAndTheRegistryBuildsIt()
public function testTheFactoryNamesTheGatewayAndTheRegistryBuildsIt(): voidGatewayContractTests::testARequiredOptionLeftOutIsAConfigurationError()
public function testARequiredOptionLeftOutIsAConfigurationError(): voidGatewayContractTests::testItIsWhatItsCapabilitiesSay()
public function testItIsWhatItsCapabilitiesSay(): voidGatewayContractTests::testAGatewayThatCallsNobodyMakesNoRequest()
public function testAGatewayThatCallsNobodyMakesNoRequest(): voidGatewayContractTests::testAChallengeShowsAWidgetAndRefusesAMissingTokenWithoutAsking()
public function testAChallengeShowsAWidgetAndRefusesAMissingTokenWithoutAsking(): voidGatewayContractTests::testAClassifierThatTakesNoReportSaysSo()
public function testAClassifierThatTakesNoReportSaysSo(): voidGatewayContractTests::testAnEmptyIdentityIsUnknownWithoutAsking()
public function testAnEmptyIdentityIsUnknownWithoutAsking(): voidGatewayContractTests::testTheRecordedCasesAreAnsweredAsRecorded()
public function testTheRecordedCasesAreAnsweredAsRecorded(): voidGatewayContractTests::testTheProvidersErrorsAreTheFamilysExceptions()
public function testTheProvidersErrorsAreTheFamilysExceptions(): void