PluginProbe ʕ •ᴥ•ʔ
Hustle – Email Marketing, Lead Generation, Optins, Popups / 7.8.9
Hustle – Email Marketing, Lead Generation, Optins, Popups v7.8.9
7.8.14 7.8.14.1 7.8.13 7.8.13.1 trunk 3.0 3.1 3.1.1 3.1.2 3.1.3 3.1.4 4.3.2 4.4.4 4.4.5 4.4.5.1 4.4.5.4 4.6 4.6.1.1 4.6.1.4 4.7.0.2 4.7.0.3 4.7.0.7 4.7.0.9 4.7.1.0 4.7.1.1 4.8.0.0 5.0.0 5.0.1 5.0.1.1 5.0.1.2 5.1 5.1.1 5.1.2 5.1.3 5.1.3.1 5.1.3.2 5.1.4 5.1.5 6.0 6.0.1 6.0.2 6.0.3 6.0.4.2 6.0.5 6.0.6.1 6.0.7 6.0.8.1 6.0.9 7.0.0.1 7.0.2 7.0.3 7.0.4 7.1.0 7.1.1 7.2.0 7.2.1 7.3.0 7.3.1 7.3.3 7.3.5 7.3.6 7.3.7 7.4.0 7.4.1 7.4.11 7.4.13 7.4.13.1 7.4.2 7.4.3 7.4.4 7.4.5 7.4.5.1 7.4.5.2 7.4.6 7.4.7 7.5.0 7.6.0 7.6.1 7.6.3 7.6.4 7.6.6 7.7.0 7.7.1 7.8.0 7.8.1 7.8.10 7.8.10.1 7.8.10.2 7.8.11 7.8.12 7.8.12.1 7.8.2 7.8.3 7.8.4 7.8.5 7.8.6 7.8.7 7.8.8 7.8.9 7.8.9.1 7.8.9.2 7.8.9.3
wordpress-popup / inc / front / hustle-module-front-ajax.php
wordpress-popup / inc / front Last commit date
non-sshare-styles 10 months ago class-hustle-decorator-non-sshare.php 3 years ago class-hustle-decorator-sshare.php 10 months ago class-hustle-decorator_abstract.php 3 years ago class-hustle-module-preview.php 1 year ago hustle-module-front-ajax.php 1 year ago hustle-module-front.php 2 years ago hustle-module-renderer.php 10 months ago hustle-renderer-abstract.php 3 years ago hustle-renderer-sshare.php 3 years ago
hustle-module-front-ajax.php
1186 lines
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Hustle_Module_Front_Ajax
4 *
5 * @package Hustle
6 */
7
8 /**
9 * Class Hustle_Module_Front_Ajax
10 */
11 class Hustle_Module_Front_Ajax {
12
13 /**
14 * Constructor
15 */
16 public function __construct() {
17
18 // When module is viewed.
19 add_action( 'wp_ajax_hustle_module_viewed', array( $this, 'module_viewed' ) );
20 add_action( 'wp_ajax_nopriv_hustle_module_viewed', array( $this, 'module_viewed' ) );
21
22 // When module form is submitted.
23 add_action( 'wp_ajax_hustle_module_form_submit', array( $this, 'submit_form' ) );
24 add_action( 'wp_ajax_nopriv_hustle_module_form_submit', array( $this, 'submit_form' ) );
25
26 // Update the number of shares of each network.
27 add_action( 'wp_ajax_hustle_update_network_shares', array( $this, 'get_networks_native_shares' ) );
28 add_action( 'wp_ajax_nopriv_hustle_update_network_shares', array( $this, 'get_networks_native_shares' ) );
29
30 // When a conversion happens.
31 add_action( 'wp_ajax_hustle_module_converted', array( $this, 'log_module_conversion' ) );
32 add_action( 'wp_ajax_nopriv_hustle_module_converted', array( $this, 'log_module_conversion' ) );
33
34 // Update the stored click counter.
35 add_action( 'wp_ajax_hustle_sshare_click_counted', array( $this, 'update_sshare_click_counter' ) );
36 add_action( 'wp_ajax_nopriv_hustle_sshare_click_counted', array( $this, 'update_sshare_click_counter' ) );
37
38 // Handles unsubscribe form submisisons.
39 add_action( 'wp_ajax_hustle_unsubscribe_form_submission', array( $this, 'unsubscribe_submit_form' ) );
40 add_action( 'wp_ajax_nopriv_hustle_unsubscribe_form_submission', array( $this, 'unsubscribe_submit_form' ) );
41
42 // Check the schedule (avoiding pages static cache).
43 add_action( 'wp_ajax_hustle_module_display_despite_static_cache', array( $this, 'module_display_despite_static_cache' ) );
44 add_action( 'wp_ajax_nopriv_hustle_module_display_despite_static_cache', array( $this, 'module_display_despite_static_cache' ) );
45 }
46
47 /**
48 * Replace value if it's a dinamic one based on submited fields
49 *
50 * @param string $value The current value.
51 * @param array $form_data The submitted form data.
52 * @return string final value
53 */
54 private function maybe_replace_to_field( $value, $form_data ) {
55 if ( ! empty( $value ) && '{' === $value[0] && '}' === substr( $value, -1 ) ) {
56 $field = trim( $value, '{}' );
57 $value = ! empty( $form_data[ $field ] ) ? $form_data[ $field ] : $value;
58 }
59
60 return $value;
61 }
62
63 /**
64 * Replace common placeholders and current field placeholders
65 *
66 * @param int $module_id Module ID.
67 * @param string $text Text.
68 * @param array $form_data Submitted data.
69 * @return string Replaced text
70 */
71 private function replace_placeholders( $module_id, $text, $form_data ) {
72 preg_match_all( '/\{[^}]*\}/', $text, $matches );
73
74 if ( ! empty( $matches[0] ) && is_array( $matches[0] ) ) {
75 $site_placeholders = array(
76 '{site_url}' => site_url(),
77 '{site_title}' => get_bloginfo( 'name' ),
78 );
79 $module = new Hustle_Module_Model( $module_id );
80 $local_list_settings = ! is_wp_error( $module ) ? $module->get_provider_settings( 'local_list' ) : '';
81 if ( ! empty( $local_list_settings['local_list_name'] ) ) {
82 $site_placeholders['{local_list}'] = $local_list_settings['local_list_name'];
83 }
84
85 foreach ( $matches[0] as $placeholder ) {
86 // find common placeholders.
87 if ( key_exists( $placeholder, $site_placeholders ) ) {
88 $value = $site_placeholders[ $placeholder ];
89 } else {
90 // find field placeholders.
91 $value = $this->maybe_replace_to_field( $placeholder, $form_data );
92 }
93
94 if ( $value !== $placeholder ) {
95 // replace if we found something.
96 $text = str_replace( $placeholder, $value, $text );
97 }
98 }
99 }
100
101 return $text;
102 }
103
104 /**
105 * Check the schedule
106 */
107 public function module_display_despite_static_cache() {
108 $module_id = filter_input( INPUT_POST, 'module_id', FILTER_VALIDATE_INT );
109 if ( ! $module_id ) {
110 wp_send_json_error( __( 'Invalid module ID!', 'hustle' ) );
111 }
112 $module = Hustle_Module_Collection::instance()->return_model_from_id( $module_id );
113 if ( is_wp_error( $module ) ) {
114 wp_send_json_error( __( 'Invalid module!', 'hustle' ) );
115 }
116 $is_scheduled = true;
117
118 // Check the schedule. Ssharing modules don't have schedules.
119 if ( Hustle_Module_Model::SOCIAL_SHARING_MODULE !== $module->module_type
120 && ! $module->get_settings()->is_currently_scheduled() ) {
121 $is_scheduled = false;
122 }
123
124 if ( ! $is_scheduled ) {
125 wp_send_json_success( array( 'display' => false ) );
126 }
127
128 $avoid_static_cache = Opt_In_Utils::is_static_cache_enabled();
129 $passed_conditions = true;
130 if ( $avoid_static_cache ) {
131 $sub_type = filter_input( INPUT_POST, 'subType', FILTER_SANITIZE_SPECIAL_CHARS );
132 $module->sub_type = $sub_type;
133 // Check visibility conditions.
134 $passed_conditions = $module->is_condition_allow();
135 }
136
137 wp_send_json_success( array( 'display' => $passed_conditions ) );
138 }
139
140 /**
141 * Send auto email if it sets
142 *
143 * @param Hustle_Module_Model $module Module.
144 */
145 public function send_automated_email( Hustle_Module_Model $module ) {
146 $module_id = $module->module_id;
147 $emails_settings = $module->get_emails()->to_array();
148
149 if ( empty( $emails_settings['automated_email'] ) || '1' !== $emails_settings['automated_email']
150 || empty( $emails_settings['email_time'] ) ) {
151 return;
152 }
153
154 $recipient = ! empty( $emails_settings['recipient'] ) ? $emails_settings['recipient'] : '';
155 $email_subject = ! empty( $emails_settings['email_subject'] ) ? $emails_settings['email_subject'] : '';
156 $email_body = ! empty( $emails_settings['email_body'] ) ? $emails_settings['email_body'] : '';
157
158 $data = filter_input( INPUT_POST, 'data', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
159 parse_str( $data['form'], $form_data );
160
161 $to = apply_filters( 'hustle_automated_email_recipient', $recipient, $module_id, $data, $form_data );
162 $subject = apply_filters( 'hustle_automated_email_email_subject', $email_subject, $module_id, $data, $form_data );
163 $body = apply_filters( 'hustle_automated_email_email_body', $email_body, $module_id, $data, $form_data );
164
165 $to = $this->replace_placeholders( $module_id, $to, $form_data );
166 $body = $this->replace_placeholders( $module_id, $body, $form_data );
167
168 // Replace {hustle_unsubscribe_link} placeholder.
169 if ( false !== strpos( $body, '{hustle_unsubscribe_link}' ) ) {
170 if ( ! empty( $form_data['hustle_module_id'] ) ) {
171 $modules_id = array( $form_data['hustle_module_id'] );
172 $unsubscribe_url = Hustle_Mail::get_unsubscribe_link( $to, $modules_id );
173 } else {
174 $unsubscribe_url = '';
175 }
176 $body = str_replace( '{hustle_unsubscribe_link}', esc_url( $unsubscribe_url ), $body );
177 }
178
179 $subject = $this->replace_placeholders( $module_id, $subject, $form_data );
180
181 // Send the email right away if it's set as 'instant'.
182 if ( 'instant' === $emails_settings['email_time'] ) {
183 Hustle_Mail::send_email( $to, $subject, $body );
184 return;
185 }
186
187 // Adding the 4th parameter time() to prevent cron jobs from being ignored.
188 // According to wp_schedule_single_event docs:
189 // "Attempts to schedule an event after an event of the same name and $args will also be ignored".
190 $args = array( $to, $subject, $body, time() );
191 if ( 'delay' === $emails_settings['email_time'] ) {
192 $time = ! empty( $emails_settings['auto_email_time'] ) ? (int) $emails_settings['auto_email_time'] : '';
193 $unit = ! empty( $emails_settings['auto_email_unit'] ) ? $emails_settings['auto_email_unit'] : '';
194 // get delay rate.
195 switch ( $unit ) {
196 case 'days':
197 $rate = DAY_IN_SECONDS;
198 break;
199 case 'hours':
200 $rate = HOUR_IN_SECONDS;
201 break;
202 case 'minutes':
203 $rate = MINUTE_IN_SECONDS;
204 break;
205 default:
206 $rate = 1;
207 break;
208 }
209 $schedule_time = time() + $time * $rate;
210 } elseif ( 'schedule' === $emails_settings['email_time'] ) {
211 // time settings.
212 $time = ! empty( $emails_settings['time'] ) ? $emails_settings['time'] : '';
213 $day = ! empty( $emails_settings['day'] ) ? $emails_settings['day'] : '';
214 $time = $this->maybe_replace_to_field( $time, $form_data );
215 $day = $this->maybe_replace_to_field( $day, $form_data );
216
217 // delay settings.
218 $delay_time = ! empty( $emails_settings['schedule_auto_email_time'] ) ? (int) $emails_settings['schedule_auto_email_time'] : 0;
219 $delay_unit = ! empty( $emails_settings['schedule_auto_email_unit'] ) ? $emails_settings['schedule_auto_email_unit'] : '';
220
221 // get delay rate.
222 switch ( $delay_unit ) {
223 case 'days':
224 $delay_rate = DAY_IN_SECONDS;
225 break;
226 case 'hours':
227 $delay_rate = HOUR_IN_SECONDS;
228 break;
229 case 'minutes':
230 $delay_rate = MINUTE_IN_SECONDS;
231 break;
232 default:
233 $delay_rate = 1;
234 break;
235 }
236 // schedule time calculation.
237 $delay = $delay_time * $delay_rate;
238 // convert from local time to GMT.
239 $schedule_time = get_gmt_from_date( date( 'Y-m-d H:i:s', strtotime( $day . ' ' . $time ) ), 'U' );// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
240 $schedule_time = $schedule_time + $delay;
241 if ( time() > $schedule_time ) {
242 return;
243 }
244 }
245 wp_schedule_single_event( $schedule_time, 'hustle_send_email', $args );
246 }
247
248 /**
249 * Submit form
250 */
251 public function submit_form() {
252
253 Hustle_Provider_Autoload::initiate_providers();
254
255 if ( ! isset( $_POST['data']['module_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
256 return;
257 }
258 $module_id = sanitize_text_field( wp_unslash( $_POST['data']['module_id'] ) );// phpcs:ignore WordPress.Security.NonceVerification.Missing
259 $module = new Hustle_Module_Model( $module_id );
260
261 if ( is_wp_error( $module ) || ! $module instanceof Hustle_Module_Model || ! property_exists( $module, 'active' ) ) {
262 wp_send_json_error( __( 'Invalid module', 'hustle' ) );
263 }
264
265 if ( ! $module->active ) {
266 wp_send_json_error( __( 'Module is not active', 'hustle' ) );
267 }
268
269 // Action called before full form submit.
270 do_action( 'hustle_form_before_handle_submit', $module_id );
271
272 $response = $this->handle_form( $module_id );
273
274 // Filter submit form response.
275 $response = apply_filters( 'hustle_form_submit_response', $response, $module_id );
276
277 // Action called after full form submit.
278 do_action( 'hustle_form_after_handle_submit', $module_id, $response );
279
280 if ( is_array( $response ) && ! empty( $response ) ) {
281 if ( $response['success'] ) {
282 wp_send_json_success( $response );
283 }
284 }
285
286 wp_send_json_error( $response );
287 }
288
289 /**
290 * Handles the module's form submission process.
291 *
292 * @since 4.0
293 *
294 * @param int $module_id MOdule ID.
295 * @return array
296 */
297 private function handle_form( $module_id ) {
298
299 $data = filter_input( INPUT_POST, 'data', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
300 parse_str( $data['form'], $form_data );
301
302 // Default error message response.
303 $response = array(
304 'message' => __( 'Error saving form', 'hustle' ),
305 'errors' => array(),
306 'success' => false,
307 'behavior' => array(),
308 );
309 $module = new Hustle_Module_Model( $module_id );
310 if ( is_wp_error( $module ) ) {
311 return $response;
312 }
313
314 $fields = $module->get_form_fields();
315 $field_data_array = array();
316 $placeholder_array = array();
317 $submit_errors = array();
318 $required_fields = array();
319 $fields_to_validate = array();
320 $entry = new Hustle_Entry_Model();
321 $entry->entry_type = $module->module_type;
322 $entry->module_id = $module_id;
323
324 if ( ! is_null( $fields ) ) {
325
326 // Verify recaptcha first.
327 if ( isset( $fields['recaptcha'] ) ) {
328
329 $submit_errors = $this->validate_recaptcha( $form_data, $fields['recaptcha'] );
330
331 if ( ! empty( $submit_errors ) ) {
332 // Recaptcha failed. No need to check the other fields.
333 $fields = array();
334 }
335 }
336
337 foreach ( $fields as $field_name => $field_data ) {
338 $ignored_field_types = Hustle_Entry_Model::ignored_fields();
339 if ( in_array( $field_data['type'], $ignored_field_types, true ) ) {
340 continue;
341 }
342
343 if ( 'true' === $field_data['required'] ) {
344 $required_fields[] = $field_name;
345 }
346
347 if ( isset( $field_data['validate'] ) && 'true' === $field_data['validate'] ) {
348 $fields_to_validate[] = $field_name;
349 }
350
351 if ( 'hidden' === $field_data['type'] ) {
352 $form_data = self::update_hidden_value( $field_data, $form_data );
353 }
354
355 if ( isset( $form_data[ $field_name ] ) ) {
356 $value = sanitize_text_field( $form_data[ $field_name ] );
357 $field_data_array[] = array(
358 'name' => $field_name,
359 'value' => $value,
360 );
361 $placeholder = '{' . $field_name . '}';
362 $placeholder_array[ $placeholder ] = $value;
363 }
364 }
365 if ( empty( $submit_errors ) ) {
366 $submit_errors = $this->validate_fields( $form_data, $required_fields, $fields_to_validate, $fields );
367 $response['errors'] = $submit_errors;
368 }
369 }
370
371 if ( ! empty( $field_data_array ) && empty( $submit_errors ) ) {
372
373 // $_POST doesn't contain everything we need to pass. So do this merge instead.
374 $submitted_data = array_merge( $data, $form_data );
375
376 // Do a pre-flight validation with the providers before submitting any data.
377 // As of 4.0 we're only checking whether the user is already subscribed if enabled.
378 $integrations_settings = $module->get_integrations_settings()->to_array();
379
380 $allow_subscribed = '1' === $integrations_settings['allow_subscribed_users'];
381
382 $formatted_submitted_data = Hustle_Provider_Utils::format_submitted_data_for_addon( $submitted_data );
383 // Do provider's validation.
384 $provider_error = $this->attach_addons_on_form_submit( $module_id, $formatted_submitted_data, $allow_subscribed );
385 if ( true !== $provider_error ) {
386 $response['errors'][] = $provider_error;
387 return $response;
388 }
389
390 $user_exists = Hustle_Entry_Model::is_email_subscribed_to_module_id( $module_id, $submitted_data['email'] );
391 if ( $user_exists ) {
392 $entry_id = Hustle_Entry_Model::get_email_subscribed_to_module_id( $module_id, $submitted_data['email'] );
393 $entry = new Hustle_Entry_Model( $entry_id );
394 $entry->entry_type = $module->module_type;
395 $entry->module_id = $module_id;
396 }
397
398 $active_integrations = array();
399
400 if ( isset( $integrations_settings['active_integrations'] ) && ! empty( $integrations_settings['active_integrations'] ) ) {
401 $active_integrations = explode( ',', $integrations_settings['active_integrations'] );
402 }
403
404 if ( in_array( 'local_list', $active_integrations, true ) ) {
405
406 if ( $user_exists || $entry->save() ) {
407
408 /**
409 * Check is tracking allowed
410 */
411 $settings = Hustle_Settings_Admin::get_privacy_settings();
412 $ip_tracking = ! isset( $settings['ip_tracking'] ) || 'on' === $settings['ip_tracking'];
413 if ( $ip_tracking ) {
414 $field_data_array[] = array(
415 'name' => 'hustle_ip',
416 'value' => Opt_In_Geo::get_user_ip(),
417 );
418 }
419
420 $active_integrations = $this->get_module_active_integrations_to_store( $module_id );
421 $field_data_array[] = array(
422 'name' => 'active_integrations',
423 'value' => $active_integrations,
424 );
425
426 // Filter data before saving to db.
427 $field_data_array = apply_filters( 'hustle_form_submit_field_data', $field_data_array, $module_id );
428
429 // Action before saving to db.
430 do_action( 'hustle_form_submit_before_set_fields', $entry, $module_id, $field_data_array );
431
432 $added_data_array = $this->attach_addons_add_entry_fields( $module_id, $module, $formatted_submitted_data, $field_data_array );
433 $added_data_array = array_merge( $field_data_array, $added_data_array );
434
435 $entry->set_fields( $added_data_array );
436
437 }
438 } else {
439
440 $active_integrations = $this->get_module_active_integrations_to_store( $module_id );
441 $field_data_array[] = array(
442 'name' => 'active_integrations',
443 'value' => $active_integrations,
444 );
445
446 // Filter data before saving to db.
447 $field_data_array = apply_filters( 'hustle_form_submit_field_data', $field_data_array, $module_id );
448
449 // Action before saving to db.
450 do_action( 'hustle_form_submit_before_set_fields', $entry, $module_id, $field_data_array );
451
452 $this->attach_addons_add_entry_fields( $module_id, $module, $formatted_submitted_data, $field_data_array );
453
454 }
455
456 $this->send_automated_email( $module );
457
458 $post_id = sanitize_text_field( $form_data['post_id'] );
459 $module_sub_type = isset( $form_data['hustle_sub_type'] ) ? $form_data['hustle_sub_type'] : null;
460 $this->maybe_log_conversion( $module, $post_id, $module_sub_type );
461
462 $emails_settings = $module->get_emails()->to_array();
463 $fields_array = wp_list_pluck( $field_data_array, 'value', 'name' );
464 $success_message = $this->parse_message_with_fields_placeholders( $emails_settings['success_message'], $placeholder_array );
465 $success_message = apply_filters( 'hustle_parsed_success_message', $success_message, $module_id, $module_sub_type, $form_data );
466
467 $redirect_url = $this->parse_message_with_fields_placeholders( $emails_settings['redirect_url'], $placeholder_array );
468
469 /**
470 * Filters the URL to redirect to on success.
471 *
472 * @since 4.4.1
473 *
474 * @param string $redirect_url The URL to redirect to. Will be passed trough esc_url_raw().
475 * @param string $module_id ID of the current module.
476 * @param string $module_sub_type Subtype of the current module.
477 * @param array $form_data The submitted data.
478 */
479 $redirect_url = apply_filters( 'hustle_success_redirect_url', $redirect_url, $module_id, $module_sub_type, $form_data );
480
481 /**
482 * Filters redirect tab on success
483 *
484 * @param string $redirect_tab Redirect tab ( sametab | newtab_thankyou | newtab_hide )
485 * @param string $module_id ID of the current module.
486 * @param string $module_sub_type Subtype of the current module.
487 * @param array $form_data The submitted data.
488 */
489 $redirect_tab = apply_filters( 'hustle_success_redirect_tab', $emails_settings['redirect_tab'], $module_id, $module_sub_type, $form_data );
490
491 $response = array(
492 'message' => do_shortcode( wp_kses_post( $success_message ) ),
493 'success' => true,
494 'errors' => array(),
495 'behavior' => array(
496 'after_submit' => $emails_settings['after_successful_submission'],
497 'url' => esc_url_raw( $redirect_url ), // Using raw here to honor url params.
498 'redirect_tab' => $redirect_tab,
499 ),
500 );
501
502 if ( ! empty( $emails_settings['automated_file'] ) && ! empty( $emails_settings['auto_download_file'] ) ) {
503 $explode = explode( DIRECTORY_SEPARATOR, $emails_settings['auto_download_file'] );
504 $file_name = end( $explode );
505
506 $response['behavior']['file'] = $emails_settings['auto_download_file'];
507 $response['behavior']['file_name'] = $file_name;
508 }
509 }
510
511 if ( ! empty( $submit_errors ) ) {
512 $response = array(
513 'message' => $this->get_invalid_form_message( $fields ),
514 'success' => false,
515 'errors' => $submit_errors,
516 'behavior' => array(),
517 );
518 }
519
520 return $response;
521 }
522
523 /**
524 * Update hidden value because it can be changed by user
525 *
526 * @param array $field_data Field settings.
527 * @param array $form_data Form data.
528 * @return array
529 */
530 private static function update_hidden_value( $field_data, $form_data ) {
531 if ( ! empty( $field_data['name'] ) && ! empty( $field_data['default_value'] )
532 // skip some types because it returns wrong values on this state for them.
533 && ! in_array( $field_data['default_value'], array( 'query_parameter', 'embed_url', 'refer_url' ), true ) ) {
534 $form_data[ $field_data['name'] ] = Hustle_Module_Renderer::get_hidden_value( $field_data );
535 }
536
537 return $form_data;
538 }
539
540 /**
541 * Do recaptcha backend validation.
542 *
543 * @since 4.0
544 * @param array $form_data Form data.
545 * @param array $recaptcha_field Recaptcha field.
546 * @return array
547 * @throws Exception When reCAPTCHA validation failed.
548 */
549 private function validate_recaptcha( $form_data, $recaptcha_field ) {
550
551 $submit_errors = array();
552 $recaptcha_settings = Hustle_Settings_Admin::get_recaptcha_settings();
553 $recaptcha_version = empty( $recaptcha_field['version'] ) ? 'v2_checkbox' : $recaptcha_field['version'];
554 $recaptcha_secret = ! empty( $recaptcha_settings[ $recaptcha_version . '_secret_key' ] ) ? $recaptcha_settings[ $recaptcha_version . '_secret_key' ] : '';
555 $recaptcha_site = ! empty( $recaptcha_settings[ $recaptcha_version . '_site_key' ] ) ? $recaptcha_settings[ $recaptcha_version . '_site_key' ] : '';
556 $token = ! empty( $form_data['recaptcha-response'] ) ? $form_data['recaptcha-response'] : false;
557
558 if ( ! empty( $recaptcha_secret ) && ! empty( $recaptcha_site ) ) {
559
560 try {
561 $validation_message = ! empty( $recaptcha_field['validation_message'] )
562 ? esc_html( $recaptcha_field['validation_message'] ) : '';
563
564 if ( ! $token ) {
565 throw new Exception(
566 ! empty( $validation_message ) ? $validation_message
567 : esc_html__( 'reCAPTCHA must be verified to submit the form.', 'hustle' )
568 );
569 }
570
571 $remote_ip = filter_input( INPUT_SERVER, 'HTTP_X_FORWARDED_FOR', FILTER_SANITIZE_SPECIAL_CHARS );
572 if ( ! $remote_ip ) {
573 $remote_ip = filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_SANITIZE_SPECIAL_CHARS );
574 }
575 $response = wp_remote_get(
576 add_query_arg(
577 array(
578 'secret' => $recaptcha_secret,
579 'response' => $token,
580 'remoteip' => $remote_ip,
581 ),
582 'https://www.google.com/recaptcha/api/siteverify'
583 )
584 );
585
586 if ( is_wp_error( $response ) ) {
587 throw new Exception( $response->get_error_message() );
588 }
589
590 $response_body = ! empty( $response['body'] ) ? json_decode( $response['body'] ) : '';
591
592 if ( empty( $response_body ) || ! $response_body->success ) {
593 throw new Exception(
594 ! empty( $validation_message ) ? $validation_message
595 : esc_html__( 'reCAPTCHA validation failed. Please try again.', 'hustle' )
596 );
597 }
598
599 if ( 'v3_recaptcha' === $recaptcha_version ) {
600 $selected_score = $recaptcha_field['threshold'];
601
602 if ( $selected_score > $response_body->score ) {
603 throw new Exception(
604 ! empty( $validation_message ) ? $validation_message
605 : esc_html__( 'reCAPTCHA validation failed. The score is too low.', 'hustle' )
606 );
607 }
608 }
609 } catch ( Exception $e ) {
610 $submit_errors['recaptcha'] = $e->getMessage();
611 }
612 }
613
614 return $submit_errors;
615 }
616
617 /**
618 * Validate fields.
619 *
620 * @since 4.0
621 * @since 4.0.2 $fields_to_validate parameter added.
622 *
623 * @param array $form_data Form data.
624 * @param array $required_fields Rewuired Fields.
625 * @param array $fields_to_validate Fields to validate.
626 * @param array $fields Fields.
627 * @return array
628 */
629 private function validate_fields( $form_data, $required_fields, $fields_to_validate, $fields ) {
630
631 $submit_errors = array();
632
633 // Check required fields.
634 foreach ( $required_fields as $slug ) {
635
636 if ( ! isset( $form_data[ $slug ] ) || empty( trim( sanitize_text_field( $form_data[ $slug ] ) ) ) ) {
637 $submit_errors[ $slug ] = $fields[ $slug ]['required_error_message'];
638 }
639 }
640
641 // Validate url and email fields.
642 if ( empty( $submit_errors ) ) {
643
644 foreach ( $fields_to_validate as $slug ) {
645
646 // Don't validate empty fields. These are validated under "required" if needed.
647 if ( empty( trim( $form_data[ $slug ] ) ) ) {
648 continue;
649 }
650
651 if ( 'email' === $fields[ $slug ]['type'] && ! is_email( $form_data[ $slug ] ) ||
652 'url' === $fields[ $slug ]['type'] && false === filter_var( $form_data[ $slug ], FILTER_VALIDATE_URL ) ||
653 'datepicker' === $fields[ $slug ]['type'] && false === $this->validate_date( $form_data[ $slug ], $fields[ $slug ]['date_format'] ) ) {
654
655 $submit_errors[ $slug ] = $fields[ $slug ]['validation_message'];
656
657 } elseif ( 'timepicker' === $fields[ $slug ]['type'] ) {
658
659 $time = str_replace( array( ' AM', ' PM' ), '', $form_data[ $slug ] );
660 $format = '12' === $fields[ $slug ]['time_format'] ? 'h:i' : 'H:i';
661 $created_time = DateTime::createFromFormat( $format, $time );
662 if ( ! $created_time || $created_time->format( $format ) !== $time ) {
663 $submit_errors[ $slug ] = $fields[ $slug ]['validation_message'];
664 }
665 }
666 }
667 }
668
669 return apply_filters( 'hustle_module_submit_errors_validated_fields', $submit_errors, $form_data, $required_fields, $fields_to_validate, $fields );
670 }
671
672 /**
673 * Get the generic error message.
674 *
675 * @since 4.0
676 *
677 * @param array $fields Fields.
678 * @return string
679 */
680 private function get_invalid_form_message( $fields ) {
681
682 if ( isset( $fields['submit'] ) && isset( $fields['submit']['error_message'] ) && ! empty( $fields['submit']['error_message'] ) ) {
683 $message = $fields['submit']['error_message'];
684 } else {
685 $message = '';
686 }
687
688 return esc_html( $message );
689 }
690
691 /**
692 * Replace placeholders
693 *
694 * @param string $raw_message Message.
695 * @param array $submitted_data Submitted data.
696 * @return string
697 */
698 private function parse_message_with_fields_placeholders( $raw_message, $submitted_data ) {
699
700 $message = str_replace(
701 array_keys( $submitted_data ),
702 array_values( $submitted_data ),
703 stripslashes( $raw_message )
704 );
705
706 return $message;
707 }
708
709 /**
710 * Log a conversion if tracking is enabled.
711 *
712 * @since 4.0
713 *
714 * @param int $module Module ID.
715 * @param int $page_id Page ID.
716 * @param string|null $module_sub_type Sub type.
717 * @param string|null $cta CTA.
718 */
719 private function maybe_log_conversion( $module, $page_id, $module_sub_type = null, $cta = null ) {
720
721 $type = is_null( $module_sub_type ) ? $module->module_type : $module_sub_type;
722
723 if ( ! $module->is_tracking_enabled( $type ) ) {
724 return false;
725 }
726
727 $tracking = Hustle_Tracking_Model::get_instance();
728 if ( 'social_sharing' === $module->module_type ) {
729 $action = 'conversion';
730 } elseif ( $cta ) {
731 $action = $cta . '_conversion';
732 } else {
733 $action = 'optin_conversion';
734 }
735 $tracking->save_tracking( $module->id, $action, $module->module_type, $page_id, $module_sub_type );
736
737 return true;
738 }
739
740 /**
741 * Get an array with the connected integrations to show in entries.
742 *
743 * @since 4.0
744 *
745 * @param int $module_id Module ID.
746 * @return array
747 */
748 private function get_module_active_integrations_to_store( $module_id ) {
749
750 $active_integrations = array();
751 $connected_addons = Hustle_Provider_Utils::get_addons_instance_connected_with_module( $module_id );
752
753 foreach ( $connected_addons as $addon ) {
754 // Local list is not really an integration to be shown here.
755 if ( 'local_list' === $addon->get_slug() ) {
756 continue;
757 }
758 $active_integrations[ $addon->get_slug() ] = $addon->get_title();
759 }
760
761 return $active_integrations;
762 }
763
764 /**
765 * Executor on form submit for attached addons.
766 *
767 * @see Hustle_Provider_Form_Hooks_Abstract::on_form_submit()
768 * @since 4.0
769 *
770 * @param int $module_id Module ID.
771 * @param array $submitted_data Data submitted by the user.
772 * @param bool $allow_subscribed Allow already subscribed.
773 * @return bool true on success|string error message from addon otherwise
774 */
775 private function attach_addons_on_form_submit( $module_id, $submitted_data, $allow_subscribed ) {
776
777 // Find is_form_connected.
778 $connected_addons = Hustle_Provider_Utils::get_addons_instance_connected_with_module( $module_id );
779
780 $submitted_data = Opt_In_Utils::validate_and_sanitize_fields( $submitted_data );
781
782 foreach ( $connected_addons as $connected_addon ) {
783 try {
784 $slug = $connected_addon->get_slug();
785 $formatted_submitted_data = apply_filters( 'hustle_format_submitted_data', $submitted_data, $slug );
786 $form_hooks = $connected_addon->get_addon_form_hooks( $module_id );
787 if ( $form_hooks instanceof Hustle_Provider_Form_Hooks_Abstract ) {
788 $addon_return = $form_hooks->on_form_submit( $formatted_submitted_data, $allow_subscribed );
789 if ( true !== $addon_return ) {
790 return $form_hooks->get_submit_form_error_message();
791 }
792 }
793 } catch ( Exception $e ) {
794 Opt_In_Utils::maybe_log( $connected_addon->get_slug(), 'failed to attach_addons_on_form_submit', $e->getMessage() );
795 }
796 }
797
798 return true;
799 }
800
801 /**
802 * Executor to add more entry fields for attached addons.
803 *
804 * @see Hustle_Provider_Form_Hooks_Abstract::add_entry_fields()
805 *
806 * @since 4.0
807 *
808 * @param int $module_id Module ID.
809 * @param Hustle_Module_Model $module Module.
810 * @param array $submitted_data Data submitted by the user.
811 * @param array $current_entry_fields Entry fields.
812 * @return array fields to be added to entry
813 */
814 private function attach_addons_add_entry_fields( $module_id, Hustle_Module_Model $module, $submitted_data, $current_entry_fields ) {
815 $additional_fields_data = array();
816 $connected_addons = Hustle_Provider_Utils::get_addons_instance_connected_with_module( $module_id );
817 foreach ( $connected_addons as $connected_addon ) {
818 try {
819 $slug = $connected_addon->get_slug();
820 $formatted_submitted_data = apply_filters( 'hustle_format_submitted_data', $submitted_data, $slug );
821 $form_hooks = $connected_addon->get_addon_form_hooks( $module_id );
822
823 if ( $form_hooks instanceof Hustle_Provider_Form_Hooks_Abstract ) {
824
825 $addon_fields = $form_hooks->add_entry_fields( $formatted_submitted_data );
826 // log errors.
827 if (
828 ! empty( $addon_fields[0] ) && ! empty( $addon_fields[0]['value'] ) &&
829 isset( $addon_fields[0]['value']['is_sent'] ) &&
830 false === $addon_fields[0]['value']['is_sent']
831 ) {
832 $error = ! empty( $addon_fields[0]['value']['description'] ) ?
833 $addon_fields[0]['value']['description']
834 : __( 'Something went wrong.', 'hustle' );
835
836 $error = $connected_addon->get_title() . ' ' . $error;
837
838 Opt_In_Utils::maybe_log( $error );
839 }
840
841 $account_settings = $connected_addon->get_settings_values();
842 if ( ! empty( $connected_addon->selected_global_multi_id ) ) {
843 $addon_fields[0]['value']['account_name'] = isset( $account_settings[ $connected_addon->selected_global_multi_id ]['name'] )
844 ? $account_settings[ $connected_addon->selected_global_multi_id ]['name'] . ' (' . $connected_addon->selected_global_multi_id . ')'
845 : $connected_addon->selected_global_multi_id;
846 }
847
848 // Reformat additional fields.
849 $addon_fields = self::format_addon_additional_fields( $connected_addon, $addon_fields );
850 $additional_fields_data = array_merge( $additional_fields_data, $addon_fields );
851 }
852 } catch ( Exception $e ) {
853 Opt_In_Utils::maybe_log( $connected_addon->get_slug(), 'failed to add_entry_fields', $e->getMessage() );
854 }
855 }
856
857 return $additional_fields_data;
858 }
859
860 /**
861 * Format additional fields from provider.
862 * Format used is `hustle_provider_{$slug}_{$field_name}`
863 *
864 * @since 4.0
865 *
866 * @param Hustle_Provider_Abstract $addon Addon.
867 * @param array $additional_fields Additional fields.
868 * @return array
869 */
870 private static function format_addon_additional_fields( Hustle_Provider_Abstract $addon, $additional_fields ) {
871 // to `name` and `value` basis.
872 $formatted_additional_fields = array();
873 if ( ! is_array( $additional_fields ) ) {
874 return array();
875 }
876
877 foreach ( $additional_fields as $additional_field ) {
878 if ( ! isset( $additional_field['name'] ) || ! isset( $additional_field['value'] ) ) {
879 continue;
880 }
881 $formatted_additional_fields[] = array(
882 'name' => 'hustle_provider_' . $addon->get_slug() . '_' . $additional_field['name'],
883 'value' => $additional_field['value'],
884 );
885 }
886
887 return $formatted_additional_fields;
888 }
889
890 /**
891 * Handles the unsubscribe form submission.
892 *
893 * @since 3.0.5
894 */
895 public function unsubscribe_submit_form() {
896
897 parse_str( $_POST['data'], $submitted_data ); // phpcs:ignore
898 $sanitized_data = Opt_In_Utils::validate_and_sanitize_fields( $submitted_data );
899 $messages = Hustle_Settings_Admin::get_unsubscribe_messages();
900 $email = isset( $sanitized_data['email'] ) ? filter_var( $sanitized_data['email'], FILTER_VALIDATE_EMAIL ) : '';
901 // Check if we got the email address and if it's valid.
902 if ( $email ) {
903 $modules_id = self::get_module_ids( $email, $sanitized_data, $messages );
904
905 // Handle 'choose_list' form step.
906 if ( isset( $sanitized_data['form_step'] ) && 'choose_list' === $sanitized_data['form_step'] ) {
907
908 if ( ! empty( $sanitized_data['skip_confirmation'] ) ) {
909 $sanitized_data['lists_id'] = $modules_id;
910 }
911
912 // If the lists are defined, submit the email with the nonce.
913 if ( ! empty( $sanitized_data['lists_id'] ) && isset( $sanitized_data['current_url'] ) ) {
914
915 // Do the process to send the unsubscription email.
916 $email_processed = Hustle_Mail::handle_unsubscription_user_email( $email, $sanitized_data['lists_id'], $sanitized_data['current_url'] );
917
918 if ( $email_processed ) {
919
920 $html = $messages['email_submitted'];
921 $wrapper = '.hustle-form-body';
922
923 $response = array(
924 'html' => apply_filters( 'hustle_unsubscribe_email_processed_html', $html, $sanitized_data ),
925 'wrapper' => apply_filters( 'hustle_unsubscribe_email_processed_wrapper', $wrapper, $sanitized_data ),
926 );
927 wp_send_json_success( $response );
928
929 }
930 }
931
932 $html = apply_filters( 'hustle_unsubscribe_email_not_processed_html', $messages['email_not_processed'], $sanitized_data );
933 wp_send_json_error( array( 'html' => $html ) );
934
935 } elseif ( isset( $sanitized_data['form_step'] ) && 'enter_email' === $sanitized_data['form_step'] ) {
936 $modules_id = self::get_module_ids( $email, $sanitized_data, $messages );
937
938 $module = new Hustle_Module_Model();
939 $params = array(
940 'ajax_step' => true,
941 'modules_id' => $modules_id,
942 'module' => $module,
943 'email' => $email,
944 'current_url' => $sanitized_data['current_url'],
945 'messages' => $messages,
946 );
947 $renderer = new Hustle_Layout_Helper();
948 $html = $renderer->render( 'general/unsubscribe-form', $params, true );
949 $wrapper = '.hustle-form-body';
950
951 $response = array(
952 'html' => apply_filters( 'hustle_render_unsubscribe_lists_html', $html, $modules_id, $email ),
953 'wrapper' => apply_filters( 'hustle_render_unsubscribe_list_wrapper', $wrapper, $modules_id, $email ),
954 );
955 wp_send_json_success( $response );
956 }
957 } else {
958 // Return an error if the email is missing or is invalid.
959 $html = apply_filters( 'hustle_unsubscribe_invalid_email_address_message', $messages['invalid_email'], $sanitized_data );
960 wp_send_json_error( array( 'html' => $html ) );
961 }
962
963 wp_send_json_success( $sanitized_data );
964 }
965
966 /**
967 * Get module ids
968 *
969 * @param atring $email Email.
970 * @param array $sanitized_data Data.
971 * @param array $messages Message texts.
972 * @return array
973 */
974 private static function get_module_ids( $email, $sanitized_data, $messages ) {
975 $entry = new Hustle_Entry_Model();
976 $modules_id = $entry->get_modules_id_by_email_in_local_list( $email );
977 // The lists are not defined yet. Show the list for the user to select them.
978 // If not showing all, show only the ones defined in the shortcode.
979 if ( ! empty( $sanitized_data['form_module_id'] ) && '-1' !== $sanitized_data['form_module_id'] ) {
980 $form_modules_id = array_map( 'trim', explode( ',', $sanitized_data['form_module_id'] ) );
981 $modules_id = array_intersect( $form_modules_id, $modules_id );
982 }
983
984 // If the email is not in any of the selected lists.
985 if ( empty( $modules_id ) ) {
986
987 $html = $messages['email_not_found'];
988 $wrapper = '.hustle-form-body';
989
990 $response = array(
991 'html' => apply_filters( 'hustle_unsubscribe_email_not_found_html', $html, $modules_id, $email ),
992 'wrapper' => apply_filters( 'hustle_unsubscribe_email_not_found_wrapper', $wrapper, $modules_id, $email ),
993 );
994 wp_send_json_success( $response );
995 }
996
997 return $modules_id;
998 }
999
1000 /**
1001 * Retrieve the number of shares from the network's native APIs.
1002 *
1003 * @since 3.0.3
1004 * @since 4.0 Get the networks' names from frontend.
1005 */
1006 public function get_networks_native_shares() {
1007
1008 // TODO: check the networks are the ones that have APIs, and sanitize.
1009 $networks = filter_input( INPUT_POST, 'networks', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_REQUIRE_ARRAY );
1010
1011 $post_id = filter_input( INPUT_POST, 'postId', FILTER_VALIDATE_INT );
1012
1013 if ( ! $networks || false === $post_id || is_null( $post_id ) ) {
1014 wp_send_json_error();
1015 }
1016
1017 $module_instance = new Hustle_SShare_Model();
1018 $networks_shares = $module_instance->retrieve_networks_shares( $networks, $post_id );
1019
1020 wp_send_json_success(
1021 array(
1022 'networks' => $networks_shares,
1023 'shorten' => array(
1024 'thousand' => esc_html__( 'K', 'hustle' ),
1025 'million' => esc_html__( 'M', 'hustle' ),
1026 ),
1027 )
1028 );
1029 }
1030
1031 /**
1032 * Log module conversion
1033 *
1034 * @return null
1035 */
1036 public function log_module_conversion() {
1037 $data = json_decode( file_get_contents( 'php://input' ) );
1038 $data = $data ? get_object_vars( $data ) : array();
1039
1040 if ( ! is_array( $data ) || empty( $data ) ) {
1041 return;
1042 }
1043 $module_id = $data['module_id'];
1044
1045 if ( empty( $module_id ) ) {
1046 wp_send_json_error( __( 'Invalid Request!', 'hustle' ) . $module_id );
1047 }
1048
1049 $module = Hustle_Module_Collection::instance()->return_model_from_id( $module_id );
1050 if ( is_wp_error( $module ) ) {
1051 wp_send_json_error( __( 'Invalid module!', 'hustle' ) );
1052 }
1053
1054 if ( $module->id ) {
1055
1056 $page_id = $data['page_id'];
1057 $module_sub_type = ( isset( $data['module_sub_type'] ) && ! empty( $data['module_sub_type'] ) ) ? $data['module_sub_type'] : null;
1058
1059 $cta = ! empty( $data['cta'] ) ? $data['cta'] : null;
1060 $res = $this->maybe_log_conversion( $module, $page_id, $module_sub_type, $cta );
1061
1062 } else {
1063 $res = false;
1064 }
1065
1066 if ( ! $res ) {
1067 wp_send_json_error( __( 'Error saving stats', 'hustle' ) );
1068 } else {
1069 wp_send_json_success( __( 'Stats Successfully saved', 'hustle' ) );
1070 }
1071 }
1072
1073 /**
1074 * Module view
1075 *
1076 * @return null
1077 */
1078 public function module_viewed() {
1079 $data = json_decode( file_get_contents( 'php://input' ) );
1080 $data = $data ? get_object_vars( $data ) : array();
1081
1082 if ( ! is_array( $data ) || empty( $data ) ) {
1083 return;
1084 }
1085
1086 $module_id = $data['module_id'];
1087
1088 if ( empty( $module_id ) ) {
1089 wp_send_json_error( __( 'Invalid Request: Module id invalid', 'hustle' ) );
1090 }
1091
1092 $module = new Hustle_Module_Model( $module_id );
1093 if ( is_wp_error( $module ) ) {
1094 wp_send_json_error( __( 'Invalid module!', 'hustle' ) );
1095 }
1096
1097 if ( $module->id ) {
1098
1099 $module_type = $data['module_type'];
1100 $page_id = $data['page_id'];
1101 $module_sub_type = isset( $data['module_sub_type'] ) ? $data['module_sub_type'] : null;
1102
1103 $tracking = Hustle_Tracking_Model::get_instance();
1104 $res = $tracking->save_tracking( $module_id, 'view', $module_type, $page_id, $module_sub_type );
1105
1106 } else {
1107 $res = false;
1108 }
1109
1110 if ( ! $res ) {
1111 wp_send_json_error( __( 'Error saving stats', 'hustle' ) );
1112 } else {
1113 wp_send_json_success( __( 'Stats Successfully saved', 'hustle' ) );
1114 }
1115
1116 }
1117
1118 /**
1119 * Update the click counter after an icon is clicked.
1120 *
1121 * @since 4.0
1122 */
1123 public function update_sshare_click_counter() {
1124
1125 $module_id = filter_input( INPUT_POST, 'moduleId', FILTER_VALIDATE_INT );
1126 $module = Hustle_Module_Collection::instance()->return_model_from_id( $module_id );
1127
1128 if ( ! is_wp_error( $module ) ) {
1129
1130 $network = filter_input( INPUT_POST, 'network', FILTER_SANITIZE_SPECIAL_CHARS );
1131
1132 $content = $module->get_content()->to_array();
1133
1134 if ( isset( $content['social_icons'][ $network ] ) ) {
1135
1136 $content['social_icons'][ $network ]['counter'] = intval( $content['social_icons'][ $network ]['counter'] ) + 1;
1137 $module->update_meta( Hustle_Module_Model::KEY_CONTENT, $content );
1138
1139 wp_send_json_success();
1140 }
1141 }
1142
1143 wp_send_json_error();
1144 }
1145
1146 /**
1147 * Validate date field.
1148 *
1149 * @param string $date date.
1150 * @param string $format date format.
1151 * @since 4.0.2
1152 */
1153 private function validate_date( $date, $format ) {
1154 $format = $this->validate_format( $format );
1155 $d = DateTime::createFromFormat( $format, $date );
1156
1157 return ( $d && $d->format( $format ) === $date );
1158 }
1159
1160 /**
1161 * Changes the date format from JS one to PHP version.
1162 *
1163 * @param string $format date format.
1164 * @since 4.0.2
1165 */
1166 private function validate_format( $format ) {
1167 // Formats based on https://api.jqueryui.com/datepicker/#utility-formatDate.
1168 $format_translate = array(
1169 'dd' => 'd',
1170 'mm' => 'm',
1171 'yy' => 'Y',
1172 'MM' => 'F',
1173 'oo' => 'z', // PHP doesn't have format like this, using the version without leading zeros.
1174 'DD' => 'l',
1175 'd' => 'j',
1176 'o' => 'z',
1177 'm' => 'n',
1178 '@' => 'U',
1179 );
1180
1181 $format = strtr( $format, $format_translate );
1182
1183 return $format;
1184 }
1185 }
1186