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.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 3.0.60 3.0.61 All 39 releases
double-opt-in / src / Admin / FollowUpRestController.php

FollowUpRestController.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.7.0, at src/Admin/FollowUpRestController.php

220 lines 6.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST routes for follow-up status and manual retry.
4 *
5 * @package Forge12\DoubleOptIn\Admin
6 * @since 5.6.0
7 */
8
9 declare( strict_types=1 );
10
11 namespace Forge12\DoubleOptIn\Admin;
12
13 use Forge12\DoubleOptIn\FollowUp\FollowUpAttempt;
14 use Forge12\DoubleOptIn\FollowUp\FollowUpCoordinator;
15 use Forge12\DoubleOptIn\Setup\FormPluginDetector;
16 use forge12\contactform7\CF7DoubleOptIn\OptIn;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 /**
23 * `GET f12-doi/v1/optins/{id}/follow-ups` status + per-action rows
24 * `POST f12-doi/v1/optins/{id}/follow-ups/retry` run failed actions again
25 *
26 * Both require `manage_options`. CSRF protection is WordPress' REST
27 * cookie authentication: without a valid `X-WP-Nonce` (`wp_rest`) the
28 * request runs as user 0 and fails the capability check.
29 */
30 class FollowUpRestController {
31
32 public const API_NAMESPACE = 'f12-doi/v1';
33
34 /** @var FollowUpCoordinator */
35 private $coordinator;
36
37 /** @var callable(int):?OptIn */
38 private $loader;
39
40 /**
41 * @param callable|null $loader Loads an opt-in by id (test seam).
42 */
43 public function __construct( FollowUpCoordinator $coordinator, ?callable $loader = null ) {
44 $this->coordinator = $coordinator;
45 $this->loader = $loader ?? static function ( int $id ): ?OptIn {
46 return OptIn::get_by_id( $id );
47 };
48 }
49
50 public function init(): void {
51 add_action( 'rest_api_init', array( $this, 'registerRoutes' ) );
52 }
53
54 public function checkPermission(): bool {
55 return current_user_can( 'manage_options' );
56 }
57
58 public function registerRoutes(): void {
59 $idArg = array(
60 'id' => array(
61 'validate_callback' => static function ( $p ) {
62 return is_numeric( $p );
63 },
64 ),
65 );
66
67 register_rest_route(
68 self::API_NAMESPACE,
69 '/optins/(?P<id>[\d]+)/follow-ups',
70 array(
71 'methods' => \WP_REST_Server::READABLE,
72 'callback' => array( $this, 'getStatus' ),
73 'permission_callback' => array( $this, 'checkPermission' ),
74 'args' => $idArg,
75 )
76 );
77
78 register_rest_route(
79 self::API_NAMESPACE,
80 '/optins/(?P<id>[\d]+)/follow-ups/retry',
81 array(
82 'methods' => \WP_REST_Server::CREATABLE,
83 'callback' => array( $this, 'retry' ),
84 'permission_callback' => array( $this, 'checkPermission' ),
85 'args' => array_merge(
86 $idArg,
87 array(
88 'include_unknown' => array(
89 'type' => 'boolean',
90 'default' => false,
91 ),
92 'action_ids' => array(
93 'type' => 'array',
94 'items' => array( 'type' => 'string' ),
95 'default' => array(),
96 ),
97 )
98 ),
99 )
100 );
101 }
102
103 /**
104 * @param \WP_REST_Request $request
105 *
106 * @return \WP_REST_Response|\WP_Error
107 */
108 public function getStatus( $request ) {
109 $optIn = ( $this->loader )( (int) $request->get_param( 'id' ) );
110 if ( ! $optIn ) {
111 return new \WP_Error( 'not_found', __( 'Opt-In not found.', 'double-opt-in' ), array( 'status' => 404 ) );
112 }
113
114 return $this->respond( $optIn );
115 }
116
117 /**
118 * @param \WP_REST_Request $request
119 *
120 * @return \WP_REST_Response|\WP_Error
121 */
122 public function retry( $request ) {
123 $optIn = ( $this->loader )( (int) $request->get_param( 'id' ) );
124 if ( ! $optIn ) {
125 return new \WP_Error( 'not_found', __( 'Opt-In not found.', 'double-opt-in' ), array( 'status' => 404 ) );
126 }
127
128 if ( ! $optIn->is_confirmed() ) {
129 return new \WP_Error( 'not_confirmed', __( 'Follow-up actions only run for confirmed opt-ins.', 'double-opt-in' ), array( 'status' => 409 ) );
130 }
131
132 if ( $this->coordinator->adapterFor( $optIn ) === null ) {
133 return new \WP_Error( 'integration_unavailable', __( 'The form integration of this opt-in is not active.', 'double-opt-in' ), array( 'status' => 409 ) );
134 }
135
136 $actionIds = array();
137 foreach ( (array) $request->get_param( 'action_ids' ) as $actionId ) {
138 // Same alphabet as FollowUpAction ids; sanitize_key() would
139 // strip the ':' separator.
140 $actionId = strtolower( (string) $actionId );
141 if ( $actionId !== '' && strlen( $actionId ) <= 100 && ! preg_match( '/[^a-z0-9_:.\-]/', $actionId ) ) {
142 $actionIds[] = $actionId;
143 }
144 }
145 $includeUnknown = (bool) $request->get_param( 'include_unknown' );
146
147 $options = array( 'include_unknown' => $includeUnknown );
148 if ( ! empty( $actionIds ) ) {
149 $options['action_ids'] = $actionIds;
150 }
151
152 // The coordinator writes the `follow_up.manual_retry` audit event.
153 $this->coordinator->run( $optIn, FollowUpAttempt::TRIGGER_MANUAL, $options );
154
155 return $this->respond( $optIn );
156 }
157
158 /**
159 * Same envelope as AdminRestController (`{success, data}`).
160 */
161 private function respond( OptIn $optIn ): \WP_REST_Response {
162 return new \WP_REST_Response(
163 array(
164 'success' => true,
165 'data' => $this->payload( $optIn ),
166 ),
167 200
168 );
169 }
170
171 /**
172 * Structural status only — no form values, recipients or tokens.
173 *
174 * @return array<string, mixed>
175 */
176 public function payload( OptIn $optIn ): array {
177 $status = $this->coordinator->statusFor( $optIn->get_id(), $optIn->is_confirmed() );
178 $status['optin_id'] = $optIn->get_id();
179 $status['form_id'] = $optIn->get_cf_form_id();
180 $status['confirmed'] = $optIn->is_confirmed();
181 $status['managed'] = $this->coordinator->adapterFor( $optIn ) !== null;
182 $status['versions'] = self::versions();
183 return $status;
184 }
185
186 /**
187 * Plugin versions for the support diagnosis export.
188 *
189 * @return array<string, string>
190 */
191 private static function versions(): array {
192 $versions = array(
193 'core' => defined( 'FORGE12_OPTIN_VERSION' ) ? (string) FORGE12_OPTIN_VERSION : '',
194 'core_api' => defined( 'F12_DOI_CORE_API_VERSION' ) ? (string) F12_DOI_CORE_API_VERSION : '',
195 'wordpress' => isset( $GLOBALS['wp_version'] ) ? (string) $GLOBALS['wp_version'] : '',
196 'php' => PHP_VERSION,
197 );
198 foreach ( array(
199 'elementor' => 'F12_DOI_ELEMENTOR_VERSION',
200 'avada' => 'F12_DOI_AVADA_VERSION',
201 'wpforms' => 'F12_DOI_WPFORMS_VERSION',
202 'gravity_forms' => 'F12_DOI_GRAVITY_FORMS_VERSION',
203 ) as $key => $constant ) {
204 if ( defined( $constant ) ) {
205 $versions[ 'addon_' . $key ] = (string) constant( $constant );
206 }
207 }
208 if ( defined( 'WPCF7_VERSION' ) ) {
209 $versions['cf7'] = (string) constant( 'WPCF7_VERSION' );
210 }
211 foreach ( array( 'elementor_pro', 'wpforms', 'gravityforms' ) as $key ) {
212 $version = FormPluginDetector::detectVersion( $key );
213 if ( $version !== null && $version !== '' ) {
214 $versions[ $key ] = $version;
215 }
216 }
217 return $versions;
218 }
219 }
220