PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.26.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.26.1
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.26.1, at classes/models/FrmFormState.php

267 lines 6.5 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
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 return true;
157 }
158
159 /**
160 * @return void
161 */
162 public function render_state_field() {
163 if ( ! self::open_ssl_is_installed() ) {
164 return;
165 }
166
167 if ( ! $this->state && ! self::get_state_from_request() ) {
168 return;
169 }
170
171 $state_string = $this->get_state_string();
172 echo '<input name="frm_state" type="hidden" value="' . esc_attr( $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 $compressed_state = $this->compressed_state();
185 $json_encoded = json_encode( $compressed_state );
186 $encrypted = openssl_encrypt( $json_encoded, 'AES-128-ECB', $secret );
187 return $encrypted;
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 return $compressed;
213 }
214
215 /**
216 * Get the first character of a key to make the option take less space.
217 * "title" => "t".
218 * "description" => "d".
219 *
220 * @param string $key
221 *
222 * @return string
223 */
224 private static function compressed_key( $key ) {
225 return $key[0];
226 }
227
228 /**
229 * Keys are truncated to a single character to make the state string smaller.
230 * Pro supports additional keys include "i" for include_fields and "g" for get params.
231 * To avoid conflicts, we should not add "i" or "g" in Lite for another state property.
232 *
233 * @param string $key
234 *
235 * @return string The full key name if one is found. If nothing is found, the $key param is passed back.
236 */
237 private static function decompressed_key( $key ) {
238 switch ( $key ) {
239 case 'd':
240 return 'description';
241 case 't':
242 return 'title';
243 case 'h':
244 return 'honeypot_field_id';
245 }
246 return $key;
247 }
248
249 /**
250 * @return string
251 */
252 private static function get_encryption_secret() {
253 $secret_key = get_option( 'frm_form_state_key' );
254
255 // If we already have the secret, send it back.
256 if ( false !== $secret_key ) {
257 return base64_decode( $secret_key ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
258 }
259
260 // We don't have a secret, so let's generate one.
261 $secret_key = is_callable( 'sodium_crypto_secretbox_keygen' ) ? sodium_crypto_secretbox_keygen() : wp_generate_password( 32, true, true );
262 update_option( 'frm_form_state_key', base64_encode( $secret_key ), 'no' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
263
264 return $secret_key;
265 }
266 }
267