Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions includes/entries.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,9 @@ function alltfo_prepare_entry( $entry ) {
/**
* Changes an entry's status.
*
* Marking as spam or not-spam also tells Akismet, when it is installed, which is
* what keeps the service learning about this particular site.
* Marking as spam or not-spam also tells Akismet -- but only when the entry's
* form has Akismet switched on. A form that never opted in sends nothing, on
* submission or on correction; see `alltfo_akismet_submit_correction()`.
*
* @since 0.1.0
*
Expand Down
40 changes: 36 additions & 4 deletions includes/spam.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function alltfo_screen_for_spam( $schema, $values, $request ) {
);
}

if ( ! $verdict['spam'] && ! empty( $settings['akismet'] ) && alltfo_akismet_available() ) {
if ( ! $verdict['spam'] && alltfo_form_uses_akismet( $schema ) && alltfo_akismet_available() ) {
if ( alltfo_akismet_says_spam( $schema, $values ) ) {
$verdict = array(
'spam' => true,
Expand Down Expand Up @@ -461,29 +461,59 @@ function alltfo_akismet_says_spam( $schema, $values ) {
return isset( $response[1] ) && 'true' === trim( $response[1] );
}

/**
* Whether a form has opted in to Akismet.
*
* The single gate every Akismet request passes through. It is the form's own
* `spam.akismet` switch -- off by default -- and nothing else: not whether the
* Akismet plugin happens to be installed, and not a site-wide setting. Data
* leaves the site for this form only because the site owner said so for this
* form.
*
* @since 0.1.0
*
* @param array $schema The form schema.
* @return bool
*/
function alltfo_form_uses_akismet( $schema ) {
return ! empty( $schema['settings']['spam']['akismet'] );
}

/**
* Tells Akismet it got one wrong.
*
* Called when somebody marks an entry as spam or not-spam in the Entries window,
* which is what keeps the service learning about this particular site rather
* than only about the internet in general.
*
* The correction is subject to the same opt-in as the check that preceded it.
* An entry belongs to a form, and unless that form has Akismet switched on the
* entry's contents, IP address and user agent never leave the site -- not on
* submission, and not when somebody clicks "spam" or "not spam" afterwards. The
* form's switch is consulted first, before Akismet is so much as looked for.
*
* @since 0.1.0
*
* @param int $entry_id The entry.
* @param string $verdict `spam` or `ham`.
* @return void
* @return bool Whether a correction was sent.
*/
function alltfo_akismet_submit_correction( $entry_id, $verdict ) {
$form_id = (int) get_post_meta( $entry_id, ALLTFO_META_FORM, true );

if ( ! $form_id || ! alltfo_form_uses_akismet( alltfo_get_form_schema( $form_id ) ) ) {
return false;
}

if ( ! alltfo_akismet_available() ) {
return;
return false;
}

$context = json_decode( (string) get_post_meta( $entry_id, ALLTFO_META_CONTEXT, true ), true );
$values = json_decode( (string) get_post_meta( $entry_id, ALLTFO_META_VALUES, true ), true );

if ( ! is_array( $context ) || ! is_array( $values ) ) {
return;
return false;
}

$request = array(
Expand All @@ -495,4 +525,6 @@ function alltfo_akismet_submit_correction( $entry_id, $verdict ) {
);

Akismet::http_post( build_query( $request ), 'spam' === $verdict ? 'submit-spam' : 'submit-ham' );

return true;
}
6 changes: 4 additions & 2 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,10 @@ Two optional features send data to a destination **you** choose:
configured on your site. Only when you switch it on for a form is each
submission to that form (the answers, plus the sender's IP and user agent)
sent to Akismet's servers for a spam verdict, with spam/ham corrections you
make in the entries screen reported back. Without the Akismet plugin, or
with the per-form switch off, nothing is sent. See the
make in the entries screen reported back. Corrections obey the same per-form
switch: marking an entry spam or not-spam on a form that has Akismet off
sends nothing. Without the Akismet plugin, or with the per-form switch off,
nothing is ever sent. See the
[Akismet Terms of Service](https://akismet.com/tos/) and the
[Automattic Privacy Policy](https://automattic.com/privacy/).
* **Webhooks** — a form can POST each entry as JSON to a URL you enter,
Expand Down
86 changes: 86 additions & 0 deletions tests/phpunit/stubs/class-akismet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php
/**
* A recording stand-in for the Akismet plugin.
*
* Defines the two static methods `includes/spam.php` touches — `get_api_key()`
* and `http_post()` — so every request the plugin would make to Akismet can be
* counted without the Akismet plugin installed and without a byte leaving the
* test run. The tests reset it between cases.
*
* Guarded so a test environment that does have Akismet loaded is left alone.
*
* @package AllTerrain_Forms
*/

if ( ! class_exists( 'Akismet' ) ) {
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Stands in for Akismet's own class, so it has to carry Akismet's name.

/**
* The recorder.
*/
class Akismet {

/**
* What `get_api_key()` answers. Empty means "not configured".
*
* @var string
*/
public static $api_key = '';

/**
* What `http_post()` answers as the response body.
*
* @var string
*/
public static $body = 'false';

/**
* Every http_post call, as [path, parsed request].
*
* @var array<int, array{path: string, request: array}>
*/
public static $calls = array();

/**
* Back to a blank slate: no key, nothing recorded.
*
* @return void
*/
public static function reset() {
self::$api_key = '';
self::$body = 'false';
self::$calls = array();
}

/**
* Mirrors `Akismet::get_api_key()`.
*
* @return string
*/
public static function get_api_key() {
return self::$api_key;
}

/**
* Mirrors `Akismet::http_post()`, recording instead of sending.
*
* @param string $request The URL-encoded request body.
* @param string $path `comment-check`, `submit-spam` or `submit-ham`.
* @param string|null $ip Unused; part of the real signature.
* @return array { 0: array headers, 1: string body }
*/
public static function http_post( $request, $path, $ip = null ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Mirrors the real signature.
$parsed = array();
parse_str( (string) $request, $parsed );

self::$calls[] = array(
'path' => $path,
'request' => $parsed,
);

return array( array(), self::$body );
}
}

// phpcs:enable
}
Loading
Loading