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

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