PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.7.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.7.0
5.8.0 5.8.1 5.7.0 5.6.2 5.6.3 5.6.1 5.6.0 5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 All 41 releases
← All changes | src/FollowUp/FollowUpCoordinator.php +32 -158 trunk → 5.7.0 View file →
@@ -164,9 +164,8 @@
164 164 );
165 165 $actions = array( new FollowUpAction( 'plan', FollowUpAction::KIND_MARKER, 'Plan', false ) );
166 166 $this->repository->plan( $optIn->get_id(), $adapter->getIntegration(), $actions, array(), FollowUpAction::fingerprint( $actions ), $this->now() );
167 167 $this->completeImmediately( $optIn, 'plan', FollowUpResult::failedPermanent( 'plan_failed' ) );
168 - $this->planGlobals( $optIn );
169 168 return true;
170 169 }
171 170
172 171 $skipReasons = array();
@@ -202,70 +201,12 @@
202 201 'skipped' => count( $skipReasons ),
203 202 )
204 203 );
205 204
206 - $this->planGlobals( $optIn );
207 -
208 205 return true;
209 206 }
210 207
211 208 /**
212 - * Add the actions of every global adapter (webhooks …) to the plan.
213 - *
214 - * A global adapter that fails or plans nothing leaves no trace and
215 - * never disturbs the form's own plan. Actions outside the adapter's
216 - * id prefix are dropped.
217 - */
218 - private function planGlobals( OptIn $optIn ): void {
219 - foreach ( $this->registry->globals() as $integration => $adapter ) {
220 - try {
221 - $actions = $adapter->planActions( $optIn );
222 - } catch ( \Throwable $e ) {
223 - $this->logger->error(
224 - 'Follow-up planning of a global adapter failed',
225 - array(
226 - 'plugin' => 'double-opt-in',
227 - 'optin_id' => $optIn->get_id(),
228 - 'integration' => $integration,
229 - 'exception' => get_class( $e ),
230 - )
231 - );
232 - continue;
233 - }
234 -
235 - $prefix = $integration . ':';
236 - $own = array();
237 - $skip = array();
238 - foreach ( $actions as $action ) {
239 - if ( ! $action instanceof FollowUpAction || strpos( $action->getId(), $prefix ) !== 0 ) {
240 - continue;
241 - }
242 - $own[] = $action;
243 - if ( $action->getSkipReason() !== '' ) {
244 - $skip[ $action->getId() ] = $action->getSkipReason();
245 - }
246 - }
247 -
248 - if ( count( $own ) !== count( $actions ) ) {
249 - $this->logger->warning(
250 - 'Follow-up actions outside the adapter prefix were dropped',
251 - array(
252 - 'plugin' => 'double-opt-in',
253 - 'optin_id' => $optIn->get_id(),
254 - 'integration' => $integration,
255 - 'dropped' => count( $actions ) - count( $own ),
256 - )
257 - );
258 - }
259 - if ( empty( $own ) ) {
260 - continue;
261 - }
262 -
263 - $this->repository->plan( $optIn->get_id(), $integration, $own, $skip, FollowUpAction::fingerprint( $own ), $this->now() );
264 - }
265 - }
266 -
267 - /**
268 209 * Execute the eligible actions of an opt-in.
269 210 *
270 211 * @param string $trigger One of FollowUpAttempt::TRIGGER_*.
271 212 * @param array{include_unknown?: bool, action_ids?: string[]} $options
@@ -329,8 +270,13 @@
329 270 // Another request won every claim — it owns this attempt.
330 271 return $this->repository->findByOptIn( $optInId );
331 272 }
332 273
274 + $actions = array();
275 + foreach ( $claimed as $record ) {
276 + $actions[] = $record->toAction();
277 + }
278 +
333 279 $this->logger->info(
334 280 'Follow-up attempt started',
335 281 array(
336 282 'plugin' => 'double-opt-in',
@@ -343,53 +289,22 @@
343 289 );
344 290
345 291 $startedAt = microtime( true );
346 292 $results = array();
347 - $fallbacks = array();
348 -
349 - // Each adapter executes its own rows: the form adapter first (its
350 - // replay may need the request to itself), then the global ones.
351 - foreach ( $this->groupByAdapter( $claimed, $adapter ) as $integration => $group ) {
352 - $handler = $this->handlerFor( $integration, $adapter );
353 - if ( $handler === null ) {
354 - // The add-on that planned these is gone. Recorded honestly;
355 - // a manual retry runs them once it is back.
356 - foreach ( array_keys( $group ) as $actionId ) {
357 - $results[ $actionId ] = FollowUpResult::failedPermanent( 'adapter_missing' );
358 - }
359 - continue;
360 - }
361 -
362 - $actions = array();
363 - foreach ( $group as $record ) {
364 - $actions[] = $record->toAction();
365 - }
366 -
367 - $own = array();
368 - try {
369 - $own = $handler->execute( $optIn, $actions, $attempt );
370 - } catch ( \Throwable $e ) {
371 - foreach ( array_keys( $group ) as $actionId ) {
372 - $fallbacks[ $actionId ] = 'adapter_exception';
373 - }
374 - $this->logger->error(
375 - 'Follow-up adapter threw',
376 - array(
377 - 'plugin' => 'double-opt-in',
378 - 'optin_id' => $optInId,
379 - 'attempt_id' => $attempt->getId(),
380 - 'integration' => $integration,
381 - 'exception' => get_class( $e ),
382 - )
383 - );
384 - }
385 -
386 - // An adapter reports for its own actions only.
387 - foreach ( array_keys( $group ) as $actionId ) {
388 - if ( is_array( $own ) && isset( $own[ $actionId ] ) ) {
389 - $results[ $actionId ] = $own[ $actionId ];
390 - }
391 - }
293 + $fallback = 'action_outcome_unknown';
294 + try {
295 + $results = $adapter->execute( $optIn, $actions, $attempt );
296 + } catch ( \Throwable $e ) {
297 + $fallback = 'adapter_exception';
298 + $this->logger->error(
299 + 'Follow-up adapter threw',
300 + array(
301 + 'plugin' => 'double-opt-in',
302 + 'optin_id' => $optInId,
303 + 'attempt_id' => $attempt->getId(),
304 + 'exception' => get_class( $e ),
305 + )
306 + );
392 307 }
393 308 $durationMs = (int) round( ( microtime( true ) - $startedAt ) * 1000 );
394 309
395 310 $finishedAt = $this->now();
@@ -397,9 +312,9 @@
397 312 $nextRetry = 0;
398 313 foreach ( $claimed as $actionId => $record ) {
399 314 $result = $results[ $actionId ] ?? null;
400 315 if ( ! $result instanceof FollowUpResult ) {
401 - $result = FollowUpResult::unknown( $fallbacks[ $actionId ] ?? 'action_outcome_unknown' );
316 + $result = FollowUpResult::unknown( $fallback );
402 317 }
403 318
404 319 $attemptNumber = $record->attempts + 1;
405 320 // Position in the automatic-retry budget. A manual retry starts
@@ -435,34 +350,23 @@
435 350 }
436 351
437 352 $records = $this->repository->findByOptIn( $optInId );
438 353 $statuses = array();
439 - $byOwner = array( $adapter->getIntegration() => array() );
440 354 foreach ( $records as $record ) {
441 - $statuses[ $record->actionId ] = $record->status;
442 - $byOwner[ $record->integration ][ $record->actionId ] = $record->status;
355 + $statuses[ $record->actionId ] = $record->status;
443 356 }
444 357
445 - // Each adapter settles its own rows — the form adapter's upload
446 - // cleanup must not wait for a webhook.
447 - foreach ( $byOwner as $integration => $own ) {
448 - $handler = $this->handlerFor( (string) $integration, $adapter );
449 - if ( $handler === null ) {
450 - continue;
451 - }
452 - try {
453 - $handler->onSettled( $optIn, $own );
454 - } catch ( \Throwable $e ) {
455 - $this->logger->error(
456 - 'Follow-up onSettled threw',
457 - array(
458 - 'plugin' => 'double-opt-in',
459 - 'optin_id' => $optInId,
460 - 'integration' => $integration,
461 - 'exception' => get_class( $e ),
462 - )
463 - );
464 - }
358 + try {
359 + $adapter->onSettled( $optIn, $statuses );
360 + } catch ( \Throwable $e ) {
361 + $this->logger->error(
362 + 'Follow-up onSettled threw',
363 + array(
364 + 'plugin' => 'double-opt-in',
365 + 'optin_id' => $optInId,
366 + 'exception' => get_class( $e ),
367 + )
368 + );
465 369 }
466 370
467 371 $this->auditAttempt( $optIn, $adapter, $attempt, $statuses, $report, $durationMs );
468 372
@@ -571,38 +475,8 @@
571 475 public function forget( int $optInId ): void {
572 476 if ( $optInId > 0 ) {
573 477 $this->repository->deleteByOptIn( $optInId );
574 478 }
575 - }
576 -
577 - /**
578 - * Claimed rows by the integration that planned them, the form
579 - * adapter's group first.
580 - *
581 - * @param array<string, FollowUpRecord> $claimed Keyed by action id.
582 - *
583 - * @return array<string, array<string, FollowUpRecord>>
584 - */
585 - private function groupByAdapter( array $claimed, FollowUpAdapterInterface $formAdapter ): array {
586 - $groups = array( $formAdapter->getIntegration() => array() );
587 - foreach ( $claimed as $actionId => $record ) {
588 - $groups[ $record->integration ][ $actionId ] = $record;
589 - }
590 -
591 - return array_filter( $groups );
592 - }
593 -
594 - /**
595 - * The adapter that owns rows of an integration: the opt-in's form
596 - * adapter or a global one. Null when the add-on that planned them is
597 - * no longer there.
598 - */
599 - private function handlerFor( string $integration, FollowUpAdapterInterface $formAdapter ): ?FollowUpAdapterInterface {
600 - if ( $integration === $formAdapter->getIntegration() ) {
601 - return $formAdapter;
602 - }
603 -
604 - return $this->registry->globals()[ $integration ] ?? null;
605 479 }
606 480
607 481 /**
608 482 * Backoff in seconds before automatic retry N (1-based position of