Skip to content

omnisense/omnishield API reference

Floor
Family
Version
1.x
Conformance suite
runs, no CI read
Verified for real
Its README does not say
Licence
MIT
composer require omnisense/omnishield:1.x-dev

Read from the package's own PHP files at the commit fa677c524359: its namespaces, classes, interfaces and enums, their public constants, properties and methods with their PHPDoc, and the options its factories take. Nothing of it is run.

Omnishield

  • ChallengeInterface interface

    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.

    2 methods

  • ChallengeIssuerInterface interface

    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.

    1 method

  • ClassifierInterface interface

    Is this content spam? What a visitor wrote - a comment, a message, a sign-up - and who wrote it, read by a classifier.

    2 methods

  • Config class

    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.

    6 methods

  • GatewayFactory class

    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.

    3 methods

  • GatewayFactoryInterface interface

    Builds a gateway from its options (a site key, a secret, a threshold...).

    2 methods

  • GatewayInterface interface

    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.

    3 methods

  • LocalizableInterface interface

    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").

    3 methods

  • Registry class

    The application's gateways by name, each built once from its factory and options:

    11 methods

  • ReputationInterface interface

    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.

    1 method

  • WidgetHtml class

    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.

    3 methods · 1 property

  • WidgetPrinter class

    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.

    3 methods

Omnishield\Bridge\Symfony

  • OmnishieldBundle class

    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:

    5 methods

  • WidgetLocalizer class

    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.

    2 methods · 1 constant

Omnishield\Bridge\Symfony\Controller

  • ChallengeController class

    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.

    2 methods

Omnishield\Bridge\Symfony\EventListener

  • ScriptListener class

    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.

    2 methods

Omnishield\Bridge\Symfony\Form

  • ChallengeType class

    A 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

  • PassesChallenge class

    The 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

  • PassesChallengeValidator class

    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.

    2 methods

Omnishield\Bridge\Twig

  • OmnishieldExtension class

    A captcha's widget in a template, for a form that is not a Symfony form:

    4 methods

Omnishield\Exception

  • InvalidConfigException class

    A gateway misconfigured: an option missing, an unknown factory, a submission without what the provider needs.

  • InvalidKeyException class

    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

    The 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

  • OmnishieldException interface

    Every exception Omnishield throws.

  • ProviderException class

    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).

    1 method · 2 properties

  • UnreachableException class

    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

    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.

    4 methods · 3 properties

  • Redaction class

    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.

    5 methods · 1 constant

Omnishield\Model

  • Attempt class

    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.

    3 methods · 4 properties

  • Capabilities class

    What a gateway is, said before it is asked anything: which of the three questions it answers, through whom, with what.

    3 methods · 9 properties

  • Classification class

    What a classifier said of a submission: ham, spam or flagrant spam, and why.

    4 methods · 3 properties

  • Identity class

    Who submits a form, as a list reads them: an address, an e-mail, a name. Any may be missing.

    3 methods · 3 properties

  • Label enum

    What a classifier made of a submission.

    3 cases

  • Reputation class

    What 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

  • Submission class

    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.

    2 methods · 12 properties · 7 constants

  • Verdict class

    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).

    4 methods · 7 properties · 8 constants

  • Widget class

    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.

    3 methods · 12 properties

Omnishield\Replay

  • CacheReplayStore class

    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.

    2 methods

  • InMemoryReplayStore class

    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).

    3 methods

  • ReplayStoreInterface interface

    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.

    1 method

Omnishield\Testing

  • FixedGateway class

    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).

    11 methods · 2 properties · 2 constants

  • FixedGatewayFactory class

    The fixed gateway, by configuration - for an application's test environment:

    1 option

  • GatewayContractTests trait

    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:

    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): Widget

What the page shows: a script, an element and its attributes, the field the token is posted in.

$action

what 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): Verdict

Whether 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): array

A 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): Classification

Ham, 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): void

Teach 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): self

Sets each key not set yet.

Config::validateNotEmpty()

public function validateNotEmpty(array $keys): void

Config::get()

public function get(string $key, mixed $default = null): mixed

Config::string()

public function string(string $key): ?string

A string option; null when it is empty.

Config::bool()

public function bool(string $key): bool

A boolean option, as an environment variable gives it too: "1", "true", "yes", "on".

Config::list()

public function list(string $key): array

A 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(): string

GatewayFactory::create()

public function create(array $options = []): GatewayInterface

GatewayFactory::createConfig()

public function createConfig(array $options = []): Config

GatewayFactoryInterface interface

interface GatewayFactoryInterface GatewayFactoryInterface.php:6

Builds a gateway from its options (a site key, a secret, a threshold...).

GatewayFactoryInterface::getName()

public function getName(): string

The name gateways are configured with: "altcha", "turnstile".

..

GatewayFactoryInterface::create()

public function create(array $options = []): GatewayInterface

GatewayInterface 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(): string

The factory's name: "altcha", "turnstile", "akismet".

..

GatewayInterface::getTitle()

public function getTitle(): string

GatewayInterface::capabilities()

public function capabilities(): Capabilities

Which 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(): ?string

The language the gateway itself was given (its language option), null to follow the visitor's

LocalizableInterface::localized()

public function localized(string $language, array $strings = []): static

The 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.

$language

a language tag: "fr", "de", "ja", "pt-br"

$strings

the 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): GatewayInterface

Registry::has()

public function has(string $name): bool

Registry::challenge()

public function challenge(string $name): ChallengeInterface

The gateway, as a captcha.

Registry::classifier()

public function classifier(string $name): ClassifierInterface

The gateway, as a spam classifier.

Registry::reputation()

public function reputation(string $name): ReputationInterface

The 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 = []): GatewayInterface

A 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(): array

Registry::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): Reputation

What 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): self

The same widget between two pieces of markup, printed when it is shown.

WidgetHtml::__toString()

public function __toString(): string

WidgetPrinter 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): string

WidgetPrinter::later()

public function later(Widget $widget, ?string $nonce = null): WidgetHtml

The 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(): void

Omnishield\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(): string

OmnishieldBundle::configure()

public function configure(DefinitionConfigurator $definition): void

OmnishieldBundle::prependExtension()

public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void

OmnishieldBundle::loadExtension()

public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void

OmnishieldBundle::replayStore()

public static function replayStore(?object $pool = null): ReplayStoreInterface

The 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): ChallengeInterface

Omnishield\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/challenge

ChallengeController::__construct()

public function __construct(readonly Registry $registry)

ChallengeController::__invoke()

public function __invoke(string $gateway, Request $request): JsonResponse

Omnishield\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)
$scripts

the path asked => the file

ScriptListener::__invoke()

public function __invoke(RequestEvent $event): void

Omnishield\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): void

ChallengeType::buildView()

public function buildView(FormView $view, FormInterface $form, array $options): void

ChallengeType::configureOptions()

public function configureOptions(OptionsResolver $resolver): void

ChallengeType::getBlockPrefix()

public function getBlockPrefix(): string

Omnishield\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 $gateway

the configured captcha; omnishield.challenge.gateway when null

public ?string $action

the action the token must have been made for

public string|bool|null $hostname

the host the widget must have been shown on; true: the request's

public ?bool $unreachable

true 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)
$gateway

the configured captcha; omnishield.challenge.gateway when null

$action

the action the token must have been made for

$hostname

the host the widget must have been shown on; true: the request's

$unreachable

true 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): void

Omnishield\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(): array

OmnishieldExtension::widget()

public function widget(?string $gateway = null, ?string $action = null, ?string $nonce = null): string

OmnishieldExtension::data()

public function data(?string $gateway = null, ?string $action = null): Widget

Omnishield\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): self

NotSupportedException::operation()

public static function operation(string $gateway, string $operation, string $why = ''): self

OmnishieldException 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 $provider
public readonly ?string $providerCode

The 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 $status
public array $headers
public 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
$options

the HTTP client's options: body, json, query, headers, timeout

Answer::header()

public function header(string $name): ?string

Answer::json()

public function json(): array

The 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 = []): array

The 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.

$options

the HTTP client's options

Redaction::sensitive()

public static function sensitive(string $name): bool

Whether a parameter, header or field of this name holds a credential.

Redaction::text()

public static function text(string $text, array $secrets = []): string

A 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 = []): \Throwable

A 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): \Throwable

The 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 $token

what the widget posted; "" when nothing was

public ?string $ip

the visitor's address, as the application sees it (behind its proxies)

public ?string $action

the action the token must have been made for; null: not checked

public ?string $hostname

the 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)
$token

what the widget posted; "" when nothing was

$ip

the visitor's address, as the application sees it (behind its proxies)

$action

the action the token must have been made for; null: not checked

$hostname

the 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): self

The token from a form's posted fields ($_POST, or a framework's request), under the widget's field.

Attempt::isEmpty()

public function isEmpty(): bool

Capabilities 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 $challenge

it answers "is this token valid?" (ChallengeInterface)

public bool $classifier

it answers "is this content spam?" (ClassifierInterface)

public bool $reputation

it answers "is this identity known for abuse?" (ReputationInterface)

public bool $thirdParty

whether what it checks - a token, a text, an address - leaves the site for someone else's

public bool $cookies

whether its widget sets or reads cookies in the visitor's browser

public bool $scores

whether a Verdict carries a score

public bool $actions

whether it checks the action a token was made for

public bool $reports

whether it takes reports back (ClassifierInterface::report(), a list's own report())

public array $reads

what 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 = [])
$challenge

it answers "is this token valid?" (ChallengeInterface)

$classifier

it answers "is this content spam?" (ClassifierInterface)

$reputation

it answers "is this identity known for abuse?" (ReputationInterface)

$thirdParty

whether what it checks - a token, a text, an address - leaves the site for someone else's

$cookies

whether its widget sets or reads cookies in the visitor's browser

$scores

whether a Verdict carries a score

$actions

whether it checks the action a token was made for

$reports

whether it takes reports back (ClassifierInterface::report(), a list's own report())

$reads

what 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(): array

Classification class

final readonly class Classification Classification.php:6

What a classifier said of a submission: ham, spam or flagrant spam, and why.

public Label $label
public array $reasons

what the provider gave as reasons or alerts, in its own words

public array $data

what else it answered (headers, a recheck delay)

Classification::__construct()

public function __construct(public Label $label, public array $reasons = [], public array $data = [])
$reasons

what the provider gave as reasons or alerts, in its own words

$data

what else it answered (headers, a recheck delay)

Classification::ham()

public static function ham(): self

Classification::isSpam()

public function isSpam(): bool

Spam, flagrant or not.

Classification::isFlagrant()

public function isFlagrant(): bool

Identity 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 $ip
public ?string $email
public ?string $name

Identity::__construct()

public function __construct(public ?string $ip = null, public ?string $email = null, public ?string $name = null)

Identity::domain()

public function domain(): ?string

The e-mail's domain, lower case, without a trailing dot; null without an e-mail.

Identity::isEmpty()

public function isEmpty(): bool

Label 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 $known

whether the list holds it for abusive, at the gateway's threshold

public int $frequency

how many times it was reported, where the list counts

public float $confidence

from 0 to 100: how surely it is abusive, as the list reckons

public ?\DateTimeImmutable $lastSeen

when it was last reported

public array $reasons

which parts matched: the constants above

public array $data

what 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 = [])
$known

whether the list holds it for abusive, at the gateway's threshold

$frequency

how many times it was reported, where the list counts

$confidence

from 0 to 100: how surely it is abusive, as the list reckons

$lastSeen

when it was last reported

$reasons

which parts matched: the constants above

$data

what else the list answered, part by part

Reputation::unknown()

public static function unknown(array $data = []): self

Reputation::matched()

public function matched(string $part): bool

Reputation::toArray()

public function toArray(): array

Submission 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 $content

what was written

public ?string $author

the name given

public ?string $email

the e-mail given

public ?string $url

the address the author gave as theirs

public ?string $ip

the visitor's address

public ?string $userAgent

the browser's User-Agent

public ?string $referrer

the page the visitor came from (Referer)

the page the content was submitted on, or will be shown at

public ?string $site

the site's home page: "https://example.org"

public ?string $language

the site's language: "fr", "fr_FR"; several, comma-separated

public string $type

what it is: the constants above

public ?\DateTimeImmutable $date

when 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)
$content

what was written

$author

the name given

$email

the e-mail given

$url

the address the author gave as theirs

$ip

the visitor's address

$userAgent

the browser's User-Agent

$referrer

the page the visitor came from (Referer)

$permalink

the page the content was submitted on, or will be shown at

$site

the site's home page: "https://example.org"

$language

the site's language: "fr", "fr_FR"; several, comma-separated

$type

what it is: the constants above

$date

when it was submitted

Submission::identity()

public function identity(): Identity

The 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 $passed
public ?float $score

from 0.0 (a bot) to 1.0 (a person), where the provider scores

public ?string $action

the action the token was made for, as the provider read it

public ?string $hostname

the host the widget was shown on, as the provider read it

public ?\DateTimeImmutable $at

when the challenge was solved

public array $reasons

why it did not pass: the constants above

public array $codes

the 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 = [])
$score

from 0.0 (a bot) to 1.0 (a person), where the provider scores

$action

the action the token was made for, as the provider read it

$hostname

the host the widget was shown on, as the provider read it

$at

when the challenge was solved

$reasons

why it did not pass: the constants above

$codes

the provider's own error codes, or its own reasons

Verdict::fail()

public static function fail(string $reason, array $codes = []): self

Verdict::failedFor()

public function failedFor(string $reason): bool

Verdict::toArray()

public function toArray(): array

Widget 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 $field

the posted field that carries the token: "altcha", "cf-turnstile-response", "g-recaptcha-response"

public ?string $script

the provider's script: an address, absolute or on the site

public array $scriptAttributes

the script element's attributes beyond src: type, async, defer, integrity, crossorigin

public string $tag

the element the script turns into a widget: "altcha-widget", "div"

public array $attributes

its attributes: true prints the name alone, false leaves it out

public ?string $content

what goes inside the element, as HTML

public ?string $inline

a short script of this package's, after the element (reCAPTCHA's invisible and score modes)

public ?string $siteKey

the provider's public key for the site, when it has one

public ?string $action

the action it was made for

public bool $thirdParty

whether the check itself goes through someone other than the site

public bool $cookies

whether the provider sets or reads cookies in the visitor's browser

public array $origins

who 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 = [])
$field

the posted field that carries the token: "altcha", "cf-turnstile-response", "g-recaptcha-response"

$script

the provider's script: an address, absolute or on the site

$scriptAttributes

the script element's attributes beyond src: type, async, defer, integrity, crossorigin

$tag

the element the script turns into a widget: "altcha-widget", "div"

$attributes

its attributes: true prints the name alone, false leaves it out

$content

what goes inside the element, as HTML

$inline

a short script of this package's, after the element (reCAPTCHA's invisible and score modes)

$siteKey

the provider's public key for the site, when it has one

$action

the action it was made for

$thirdParty

whether the check itself goes through someone other than the site

$cookies

whether the provider sets or reads cookies in the visitor's browser

$origins

who the visitor's browser talks to besides the site ("https://challenges.cloudflare.com"): name them first, then load

Widget::reachesOthers()

public function reachesOthers(): bool

Whether 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): string

The widget's markup: the script, the element, the inline script.

$nonce

the page's Content-Security-Policy nonce, put on both scripts

$script

false 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): bool

InMemoryReplayStore 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): bool

InMemoryReplayStore::count()

public function count(): int

ReplayStoreInterface 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): bool

Marks the token spent until $until.

$id

an 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(): self

FixedGateway::failing()

public static function failing(): self

FixedGateway::getName()

public function getName(): string

FixedGateway::getTitle()

public function getTitle(): string

FixedGateway::capabilities()

public function capabilities(): Capabilities

FixedGateway::widget()

public function widget(?string $action = null): Widget

FixedGateway::verify()

public function verify(Attempt $attempt): Verdict

FixedGateway::classify()

public function classify(Submission $submission): Classification

FixedGateway::report()

public function report(Submission $submission, bool $spam): void

FixedGateway::lookup()

public function lookup(Identity $identity): Reputation

FixedGatewayFactory 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

OptionDefault
passtrue

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(): void

GatewayContractTests::testARequiredOptionLeftOutIsAConfigurationError()

public function testARequiredOptionLeftOutIsAConfigurationError(): void

GatewayContractTests::testItIsWhatItsCapabilitiesSay()

public function testItIsWhatItsCapabilitiesSay(): void

GatewayContractTests::testAGatewayThatCallsNobodyMakesNoRequest()

public function testAGatewayThatCallsNobodyMakesNoRequest(): void

GatewayContractTests::testAChallengeShowsAWidgetAndRefusesAMissingTokenWithoutAsking()

public function testAChallengeShowsAWidgetAndRefusesAMissingTokenWithoutAsking(): void

GatewayContractTests::testAClassifierThatTakesNoReportSaysSo()

public function testAClassifierThatTakesNoReportSaysSo(): void

GatewayContractTests::testAnEmptyIdentityIsUnknownWithoutAsking()

public function testAnEmptyIdentityIsUnknownWithoutAsking(): void

GatewayContractTests::testTheRecordedCasesAreAnsweredAsRecorded()

public function testTheRecordedCasesAreAnsweredAsRecorded(): void

GatewayContractTests::testTheProvidersErrorsAreTheFamilysExceptions()

public function testTheProvidersErrorsAreTheFamilysExceptions(): void