PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.34
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.34
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 6.5.4 All 140 releases
formidable / classes / models / FrmFormState.php

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

266 lines 6.3 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 ( ! 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 ( ! 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 echo '<input name="frm_state" type="hidden" value="' . esc_attr( $this->get_state_string() ) . '" />';
173 }
174
175 /**
176 * @return string
177 */
178 private function get_state_string() {
179 if ( ! self::open_ssl_is_installed() ) {
180 return '';
181 }
182
183 $secret = self::get_encryption_secret();
184 $json_encoded = json_encode( $this->compressed_state() );
185 return openssl_encrypt( $json_encoded, 'AES-128-ECB', $secret );
186 }
187
188 /**
189 * Returns true if open SSL is installed.
190 *
191 * @since 6.12
192 *
193 * @return bool
194 */
195 private static function open_ssl_is_installed() {
196 return function_exists( 'openssl_encrypt' );
197 }
198
199 /**
200 * Return state but with shorter keys to use for the state string.
201 *
202 * @return array
203 */
204 private function compressed_state() {
205 $compressed = array();
206
207 foreach ( $this->state as $key => $value ) {
208 $compressed[ self::compressed_key( $key ) ] = $value;
209 }
210
211 return $compressed;
212 }
213
214 /**
215 * Get the first character of a key to make the option take less space.
216 * "title" => "t".
217 * "description" => "d".
218 *
219 * @param string $key
220 *
221 * @return string
222 */
223 private static function compressed_key( $key ) {
224 return $key[0];
225 }
226
227 /**
228 * Keys are truncated to a single character to make the state string smaller.
229 * Pro supports additional keys include "i" for include_fields and "g" for get params.
230 * To avoid conflicts, we should not add "i" or "g" in Lite for another state property.
231 *
232 * @param string $key
233 *
234 * @return string The full key name if one is found. If nothing is found, the $key param is passed back.
235 */
236 private static function decompressed_key( $key ) {
237 switch ( $key ) {
238 case 'd':
239 return 'description';
240 case 't':
241 return 'title';
242 case 'h':
243 return 'honeypot_field_id';
244 }
245 return $key;
246 }
247
248 /**
249 * @return string
250 */
251 private static function get_encryption_secret() {
252 $secret_key = get_option( 'frm_form_state_key' );
253
254 // If we already have the secret, send it back.
255 if ( false !== $secret_key ) {
256 return base64_decode( $secret_key ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
257 }
258
259 // We don't have a secret, so let's generate one.
260 $secret_key = is_callable( 'sodium_crypto_secretbox_keygen' ) ? sodium_crypto_secretbox_keygen() : wp_generate_password( 32, true, true );
261 update_option( 'frm_form_state_key', base64_encode( $secret_key ), false ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
262
263 return $secret_key;
264 }
265 }
266