PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.28
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.28
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / models / FrmFormState.php

FrmFormState.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.28, at classes/models/FrmFormState.php

268 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 if ( ! defined( 'ABSPATH' ) ) {
4 die( 'You are not allowed to call this page directly.' );
5 }
6
7 /**
8 * Track form state in an encrypted form field.
9 * The state just holds some basic info, like if a [formidable] shortcode loaded
10 * with a title=1 or description=1 option.
11 *
12 * @since 6.2
13 */
14 class FrmFormState {
15
16 /**
17 * @var FrmFormState|null
18 */
19 private static $instance;
20
21 /**
22 * @var array
23 */
24 private $state;
25
26 private function __construct() {
27 $this->state = array();
28 }
29
30 /**
31 * @param string $key
32 * @param mixed $value
33 *
34 * @return void
35 */
36 public static function set_initial_value( $key, $value ) {
37 if ( is_callable( 'FrmProFormState::set_initial_value' ) ) {
38 // Let Pro handle state.
39 return;
40 }
41
42 self::maybe_initialize();
43 self::$instance->set( $key, $value );
44 }
45
46 /**
47 * @return bool true if just initialized.
48 */
49 private static function maybe_initialize() {
50 if ( empty( self::$instance ) ) {
51 self::$instance = new self();
52 return true;
53 }
54 return false;
55 }
56
57 /**
58 * @param string $key
59 * @param mixed $value
60 *
61 * @return void
62 */
63 public function set( $key, $value ) {
64 $this->state[ $key ] = $value;
65 }
66
67 /**
68 * @param string $key
69 * @param mixed $default
70 *
71 * @return mixed
72 */
73 public static function get_from_request( $key, $default ) {
74 if ( self::maybe_initialize() ) {
75 self::get_state_from_request();
76 }
77 return self::$instance->get( $key, $default );
78 }
79
80 /**
81 * @param string $key
82 * @param mixed $default
83 *
84 * @return mixed
85 */
86 public function get( $key, $default ) {
87 return $this->state[ $key ] ?? $default;
88 }
89
90 /**
91 * Render a basic version of the state field from Pro.
92 * This is required only when submitting with AJAX.
93 * It is used to track the value of a title=1|0 or description=1|0 option in a [formidable] shortcode.
94 *
95 * @param stdClass $form
96 *
97 * @return void
98 */
99 public static function maybe_render_state_field( $form ) {
100 if ( is_callable( 'FrmProFormState::maybe_render_state_field' ) ) {
101 // Let Pro handle state when Pro is available.
102 // This way we can also avoid duplicate state fields if Pro isn't up to date.
103 return;
104 }
105
106 if ( empty( self::$instance ) && ! self::get_state_from_request() ) {
107 return;
108 }
109
110 $honeypot_field_id = self::$instance->get( 'honeypot_field_id', 0 );
111
112 if ( empty( $form->options['ajax_submit'] ) && ! $honeypot_field_id ) {
113 // This is only required for AJAX submit, or when the honeypot field is on the page.
114 return;
115 }
116
117 $state_title = ! empty( self::$instance->state['title'] ) ? 1 : 0;
118 $state_description = ! empty( self::$instance->state['description'] ) ? 1 : 0;
119 $settings_title = ! empty( $form->options['show_title'] ) ? 1 : 0;
120 $settings_description = ! empty( $form->options['show_description'] ) ? 1 : 0;
121
122 if ( $state_title === $settings_title && $state_description === $settings_description && ! $honeypot_field_id ) {
123 // Avoid state field if it matches form settings and there is no honeypot.
124 return;
125 }
126
127 self::$instance->render_state_field();
128 }
129
130 /**
131 * @return bool true if there is valid state data in the request.
132 */
133 private static function get_state_from_request() {
134 $encrypted_state = FrmAppHelper::get_post_param( 'frm_state', '', 'sanitize_text_field' );
135
136 if ( ! $encrypted_state ) {
137 return false;
138 }
139
140 $secret = self::get_encryption_secret();
141 $decrypted_state = openssl_decrypt( $encrypted_state, 'AES-128-ECB', $secret );
142
143 if ( false === $decrypted_state ) {
144 return false;
145 }
146
147 $decoded_state = json_decode( $decrypted_state, true );
148
149 if ( ! is_array( $decoded_state ) ) {
150 return false;
151 }
152
153 foreach ( $decoded_state as $key => $value ) {
154 self::set_initial_value( self::decompressed_key( $key ), $value );
155 }
156
157 return true;
158 }
159
160 /**
161 * @return void
162 */
163 public function render_state_field() {
164 if ( ! self::open_ssl_is_installed() ) {
165 return;
166 }
167
168 if ( ! $this->state && ! self::get_state_from_request() ) {
169 return;
170 }
171
172 $state_string = $this->get_state_string();
173 echo '<input name="frm_state" type="hidden" value="' . esc_attr( $state_string ) . '" />';
174 }
175
176 /**
177 * @return string
178 */
179 private function get_state_string() {
180 if ( ! self::open_ssl_is_installed() ) {
181 return '';
182 }
183
184 $secret = self::get_encryption_secret();
185 $compressed_state = $this->compressed_state();
186 $json_encoded = json_encode( $compressed_state );
187 return openssl_encrypt( $json_encoded, 'AES-128-ECB', $secret );
188 }
189
190 /**
191 * Returns true if open SSL is installed.
192 *
193 * @since 6.12
194 *
195 * @return bool
196 */
197 private static function open_ssl_is_installed() {
198 return function_exists( 'openssl_encrypt' );
199 }
200
201 /**
202 * Return state but with shorter keys to use for the state string.
203 *
204 * @return array
205 */
206 private function compressed_state() {
207 $compressed = array();
208
209 foreach ( $this->state as $key => $value ) {
210 $compressed[ self::compressed_key( $key ) ] = $value;
211 }
212
213 return $compressed;
214 }
215
216 /**
217 * Get the first character of a key to make the option take less space.
218 * "title" => "t".
219 * "description" => "d".
220 *
221 * @param string $key
222 *
223 * @return string
224 */
225 private static function compressed_key( $key ) {
226 return $key[0];
227 }
228
229 /**
230 * Keys are truncated to a single character to make the state string smaller.
231 * Pro supports additional keys include "i" for include_fields and "g" for get params.
232 * To avoid conflicts, we should not add "i" or "g" in Lite for another state property.
233 *
234 * @param string $key
235 *
236 * @return string The full key name if one is found. If nothing is found, the $key param is passed back.
237 */
238 private static function decompressed_key( $key ) {
239 switch ( $key ) {
240 case 'd':
241 return 'description';
242 case 't':
243 return 'title';
244 case 'h':
245 return 'honeypot_field_id';
246 }
247 return $key;
248 }
249
250 /**
251 * @return string
252 */
253 private static function get_encryption_secret() {
254 $secret_key = get_option( 'frm_form_state_key' );
255
256 // If we already have the secret, send it back.
257 if ( false !== $secret_key ) {
258 return base64_decode( $secret_key ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
259 }
260
261 // We don't have a secret, so let's generate one.
262 $secret_key = is_callable( 'sodium_crypto_secretbox_keygen' ) ? sodium_crypto_secretbox_keygen() : wp_generate_password( 32, true, true );
263 update_option( 'frm_form_state_key', base64_encode( $secret_key ), false ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
264
265 return $secret_key;
266 }
267 }
268