PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.6.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.6.0
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 3.0.62 3.0.70 3.0.71 3.0.72 All 35 releases
double-opt-in / src / Admin / FollowUpRestController.php

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

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