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

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