PluginProbe ʕ •ᴥ•ʔ
Hustle – Email Marketing, Lead Generation, Optins, Popups / 7.8.14
Hustle – Email Marketing, Lead Generation, Optins, Popups v7.8.14
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 / hustle-module-model.php
wordpress-popup / inc Last commit date
display-conditions 5 months ago front 3 days ago helpers 1 month ago metas 3 months ago multisite 5 months ago palettes 3 years ago provider 1 month ago providers 3 days ago templates 3 years ago update 5 months ago class-hustle-admin-page-abstract.php 1 month ago class-hustle-auth-token.php 5 months ago class-hustle-condition-factory.php 6 years ago class-hustle-cross-sell.php 10 months ago class-hustle-dashboard-admin.php 1 month ago class-hustle-data.php 1 month ago class-hustle-db.php 2 years ago class-hustle-installer.php 4 years ago class-hustle-meta.php 3 years ago class-hustle-module-admin.php 1 month ago class-hustle-module-collection.php 5 months ago class-hustle-module-fields.php 3 months ago class-hustle-module-page-abstract.php 1 month ago class-hustle-module-validator.php 3 months ago class-hustle-notifications.php 3 days ago class-hustle-settings-admin.php 1 month ago class-hustle-wp-dashboard-page.php 5 months ago class-opt-in.php 3 days ago hustle-background-conversion-log.php 3 months ago hustle-deletion.php 3 months ago hustle-embedded-admin.php 3 years ago hustle-entries-admin.php 5 months ago hustle-entry-model.php 1 month ago hustle-general-data-protection.php 5 months ago hustle-init.php 1 month ago hustle-mail.php 5 months ago hustle-migration.php 5 months ago hustle-model.php 3 days ago hustle-module-model.php 1 month ago hustle-module-widget-legacy.php 5 months ago hustle-module-widget.php 5 months ago hustle-modules-common-admin-ajax.php 1 month ago hustle-popup-admin.php 3 years ago hustle-providers-admin.php 1 month ago hustle-providers.php 5 months ago hustle-settings-admin-ajax.php 1 month ago hustle-settings-page.php 3 years ago hustle-slidein-admin.php 3 years ago hustle-sshare-admin.php 3 days ago hustle-sshare-model.php 3 days ago hustle-tracking-model.php 3 months ago interface-hustle-auth-provider.php 5 months ago opt-in-geo.php 5 months ago opt-in-utils.php 3 days ago opt-in-wpmudev-api.php 5 months ago
hustle-module-model.php
1043 lines
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Hustle_Module_Model
4 *
5 * @package Hustle
6 */
7
8 /**
9 * Class Hustle_Module_Model
10 */
11 class Hustle_Module_Model extends Hustle_Model {
12
13 /**
14 * Get the sub-types for embedded modules.
15 *
16 * @since the beggining of time
17 * @since 4.0 "after_content" changed to "inline"
18 * @param bool $with_titles With titles.
19 *
20 * @return array
21 */
22 public static function get_embedded_types( $with_titles = false ) {
23 if ( ! $with_titles ) {
24 return array( 'inline', 'widget', 'shortcode' );
25 } else {
26 return array(
27 'inline' => __( 'Inline', 'hustle' ),
28 'widget' => __( 'Widget', 'hustle' ),
29 'shortcode' => __( 'Shortcode', 'hustle' ),
30 );
31 }
32 }
33
34 /**
35 * Get the sub-types for this module.
36 *
37 * @since 4.0
38 * @param bool $with_titles With titles.
39 *
40 * @return array
41 */
42 public function get_sub_types( $with_titles = false ) {
43 if ( self::EMBEDDED_MODULE === $this->module_type ) {
44 return self::get_embedded_types( $with_titles );
45 }
46
47 return array();
48 }
49
50 /**
51 * Gets the instance of the decorator class for this module type.
52 *
53 * @since 4.3.0
54 *
55 * @return Hustle_Decorator_Non_Sshare
56 */
57 public function get_decorator_instance() {
58 return new Hustle_Decorator_Non_Sshare( $this );
59 }
60
61 /**
62 * Content Model based upon module type.
63 *
64 * @return Class
65 */
66 public function get_content() {
67 $data = $this->get_settings_meta( self::KEY_CONTENT );
68
69 if ( ! Opt_In_Utils::is_free() ) {
70 if ( ! empty( $data['background_image'] ) ) {
71 $data['background_image'] = self::replace_free_to_pro_folder( $data['background_image'] );
72 }
73 if ( ! empty( $data['feature_image'] ) ) {
74 $data['feature_image'] = self::replace_free_to_pro_folder( $data['feature_image'] );
75 }
76 }
77
78 return new Hustle_Meta_Base_Content( $data, $this );
79 }
80
81 /**
82 * It applies for users which was upgraded from free to pro version
83 *
84 * @param string $subject String which includes an old path.
85 * @return string
86 */
87 private static function replace_free_to_pro_folder( $subject ) {
88 $free_plugin_name = 'wordpress-popup/';
89 $pro_plugin_name = 'hustle/';
90 return str_replace( '/plugins/' . $free_plugin_name, '/plugins/' . $pro_plugin_name, $subject );
91 }
92
93 /**
94 * Get the content of the data stored under 'emails' meta.
95 *
96 * @since 4.0
97 *
98 * @return Hustle_Meta_Base_Emails
99 */
100 public function get_emails() {
101 $data = $this->get_settings_meta( self::KEY_EMAILS );
102
103 return new Hustle_Meta_Base_Emails( $data, $this );
104 }
105
106 /**
107 * Get the module's settings for the given provider.
108 *
109 * @since 4.0
110 *
111 * @param string $slug Slug.
112 * @param bool $get_cached Get cached.
113 * @return array
114 */
115 public function get_provider_settings( $slug, $get_cached = true ) {
116 return $this->get_settings_meta( $slug . self::KEY_PROVIDER, array(), $get_cached );
117 }
118
119 /**
120 * Save the module's settings for the given provider.
121 *
122 * @since 4.0
123 *
124 * @param string $slug Slug.
125 * @param array $data Data.
126 * @return array
127 */
128 public function set_provider_settings( $slug, $data ) {
129 return $this->update_meta( $slug . self::KEY_PROVIDER, $data );
130 }
131
132 /**
133 * Get the all-integrations module's settings.
134 * This is not each provider's settings. Instead, these are per module settings
135 * that are applied to all the active providers of this module.
136 *
137 * @since 4.0
138 *
139 * @return \Hustle_Meta_Base_Integrations
140 */
141 public function get_integrations_settings() {
142 $stored = $this->get_settings_meta( self::KEY_INTEGRATIONS_SETTINGS );
143 return new Hustle_Meta_Base_Integrations( $stored, $this );
144 }
145
146 /**
147 * Get design
148 *
149 * @return \Hustle_Meta_Base_Design
150 */
151 public function get_design() {
152 $stored = $this->get_settings_meta( self::KEY_DESIGN );
153 return new Hustle_Meta_Base_Design( $stored, $this );
154 }
155
156 /**
157 * Get the stored settings for the "Display" tab.
158 * Used for Embedded.
159 *
160 * @since 4.0
161 *
162 * @return Hustle_Meta_Base_Display
163 */
164 public function get_display() {
165 return new Hustle_Meta_Base_Display( $this->get_settings_meta( self::KEY_DISPLAY_OPTIONS ), $this );
166 }
167
168 /**
169 * Used when populating data with "get".
170 */
171 public function get_settings() {
172 $saved = $this->get_settings_meta( self::KEY_SETTINGS );
173
174 // We made this an array in 4.1.0. Some sites didn't run the migration for some reason.
175 // This prevents the old string from triggering php errors and warnings.
176 // It'd be even better to find out why that migration didn't run in some sites.
177 if ( ! empty( $saved['triggers']['trigger'] ) && ! is_array( $saved['triggers']['trigger'] ) ) {
178 $saved['triggers']['trigger'] = array( $saved['triggers']['trigger'] );
179 }
180
181 if ( self::POPUP_MODULE === $this->module_type ) {
182 return new Hustle_Popup_Settings( $saved, $this );
183
184 } elseif ( self::EMBEDDED_MODULE === $this->module_type ) {
185 return new Hustle_Meta_Base_Settings( $saved, $this );
186
187 } elseif ( self::SLIDEIN_MODULE === $this->module_type ) {
188 return new Hustle_Slidein_Settings( $saved, $this );
189 }
190
191 return false;
192 }
193
194 /**
195 * Get the stored schedule flags
196 *
197 * @since 4.2.0
198 * @return array
199 */
200 public function get_schedule_flags() {
201 $default = array(
202 'is_currently_scheduled' => '1',
203 'check_schedule_at' => 1,
204 );
205
206 return $this->get_settings_meta( 'schedule_flags', $default );
207 }
208
209 /**
210 * Set the schedule flags.
211 *
212 * @since 4.2.0
213 * @param array $flags Flags.
214 * @return void
215 */
216 public function set_schedule_flags( $flags ) {
217 $this->update_meta( 'schedule_flags', $flags );
218 }
219
220 /**
221 * Get the module's data. Used to display it.
222 *
223 * @since 3.0.7
224 *
225 * @return array
226 */
227 public function get_module_data_to_display() {
228 $settings = array( 'settings' => $this->get_settings()->to_array() );
229 $data = array_merge( $settings, $this->get_data() );
230
231 return $data;
232 }
233
234 /**
235 * Get the form fields of this module, if any.
236 *
237 * @since 4.0
238 *
239 * @return null|array
240 */
241 public function get_form_fields() {
242
243 if ( 'social_sharing' === $this->module_type || 'informational' === $this->module_mode ) {
244 return null;
245 }
246
247 $emails_data = empty( $this->emails ) ? $this->get_emails()->to_array() : (array) $this->emails;
248 /**
249 * Edit module fields
250 *
251 * @since 4.1.1
252 * @param string $form_elements Current module fields.
253 */
254 $form_fields = apply_filters( 'hustle_form_elements', $emails_data['form_elements'] );
255
256 return $form_fields;
257 }
258
259 /**
260 * Create a new module of the provided mode and type.
261 *
262 * @since 4.0
263 *
264 * @param array $data Must contain the Module's 'mode', 'name' and 'type.
265 * @return int|false Module ID if successfully saved. False otherwise.
266 */
267 public function create_new( $data ) {
268 $module_populated = $this->populate_module_from_data( $data );
269 if ( ! $module_populated ) {
270 return false;
271 }
272
273 // Save to modules table.
274 $this->save();
275
276 $data = $this->sanitize_module( $data );
277
278 // Save the new module's meta.
279 $this->store_new_module_meta( $data );
280
281 $this->activate_providers( $data );
282
283 return $this->id;
284 }
285
286 /**
287 * Populates the current model with the given data.
288 *
289 * @since 4.3.4
290 *
291 * @param array $data Data to populate the module with.
292 * @return bool
293 */
294 private function populate_module_from_data( $data ) {
295 // Verify it's a valid module type.
296 if ( ! in_array( $data['module_type'], array( self::POPUP_MODULE, self::SLIDEIN_MODULE, self::EMBEDDED_MODULE ), true ) ) {
297 return false;
298 }
299
300 // Abort if the mode isn't set.
301 if ( ! in_array( $data['module_mode'], array( 'optin', 'informational' ), true ) ) {
302 return false;
303 }
304
305 $this->module_name = sanitize_text_field( $data['module_name'] );
306 $this->module_type = $data['module_type'];
307 $this->active = 0;
308 $this->module_mode = $data['module_mode'];
309
310 return true;
311 }
312
313 /**
314 * Store the metas in the db when creating a new module.
315 *
316 * @since 4.0.0
317 *
318 * @param array $data Module's data to store.
319 */
320 private function store_new_module_meta( $data ) {
321 $def_content = apply_filters( 'hustle_module_get_' . self::KEY_CONTENT . '_defaults', $this->get_content()->to_array(), $this, $data );
322 $content = empty( $data['content'] ) ? $def_content : array_merge( $def_content, $data['content'] );
323
324 $def_emails = apply_filters( 'hustle_module_get_' . self::KEY_EMAILS . '_defaults', $this->get_emails()->to_array(), $this, $data );
325 $emails = empty( $data['emails'] ) ? $def_emails : array_merge( $def_emails, $data['emails'] );
326
327 $def_design = apply_filters( 'hustle_module_get_' . self::KEY_DESIGN . '_defaults', $this->get_design()->to_array(), $this, $data );
328 $design = empty( $data['design'] ) ? $def_design : array_merge( $def_design, $data['design'] );
329 if ( ! empty( $data['base_template'] ) ) {
330 $design['base_template'] = $data['base_template'];
331 }
332
333 $def_integrations_settings = apply_filters( 'hustle_module_get_' . self::KEY_INTEGRATIONS_SETTINGS . '_defaults', $this->get_integrations_settings()->to_array(), $this, $data );
334 $integrations_settings = empty( $data['integrations_settings'] ) ? $def_integrations_settings : array_merge( $def_integrations_settings, $data['integrations_settings'] );
335
336 $def_settings = apply_filters( 'hustle_module_get_' . self::KEY_SETTINGS . '_defaults', $this->get_settings()->to_array(), $this, $data );
337 $settings = empty( $data['settings'] ) ? $def_settings : array_merge( $def_settings, $data['settings'] );
338
339 $def_visibility = apply_filters( 'hustle_module_get_' . self::KEY_VISIBILITY . '_defaults', $this->get_visibility()->to_array(), $this, $data );
340 $visibility = empty( $data['visibility'] ) ? $def_visibility : array_merge( $def_visibility, $data['visibility'] );
341
342 $this->update_meta( self::KEY_CONTENT, $content );
343 $this->update_meta( self::KEY_EMAILS, $emails );
344 $this->update_meta( self::KEY_INTEGRATIONS_SETTINGS, $integrations_settings );
345 $this->update_meta( self::KEY_DESIGN, $design );
346 $this->update_meta( self::KEY_SETTINGS, $settings );
347 $this->update_meta( self::KEY_VISIBILITY, $visibility );
348
349 // Embedded only. Display options.
350 if ( self::EMBEDDED_MODULE === $this->module_type ) {
351 $def_display = apply_filters( 'hustle_module_get_' . self::KEY_DISPLAY_OPTIONS . '_defaults', $this->get_display()->to_array(), $this, $data );
352 $display = empty( $data['display'] ) ? $def_display : array_merge( $def_display, $data['display'] );
353
354 $this->update_meta( self::KEY_DISPLAY_OPTIONS, $display );
355 }
356
357 $this->enable_type_track_mode( $this->module_type, true );
358 }
359
360 /**
361 * Populates an instance of a module for a template preview.
362 * This is only used for displaying a preview. We're using a saved
363 * module nor creating a new one, thus we don't have an ID.
364 *
365 * @since 4.3.4
366 *
367 * @param array $data Module data to be populated.
368 */
369 public function populate_module_for_template( $data ) {
370 $this->module_id = 0;
371
372 $this->populate_module_from_data( $data );
373 }
374
375 /**
376 * Creates and store the nonce used to validate email unsubscriptions.
377 *
378 * @since 3.0.5
379 * @param string $email Email to be unsubscribed.
380 * @param array $lists_id IDs of the modules to which it will be unsubscribed.
381 * @return boolean
382 */
383 public function create_unsubscribe_nonce( $email, array $lists_id ) {
384 // Since we're supporting php 5.2, random_bytes or other strong rng are not available. So using this instead.
385 $nonce = hash_hmac( 'md5', $email, wp_rand() . time() );
386
387 $data = get_option( self::KEY_UNSUBSCRIBE_NONCES, array() );
388
389 // If the email already created a nonce and didn't use it, replace its data.
390 $data[ $email ] = array(
391 'nonce' => $nonce,
392 'lists_id' => $lists_id,
393 'date_created' => time(),
394 );
395
396 $updated = update_option( self::KEY_UNSUBSCRIBE_NONCES, $data );
397 if ( $updated ) {
398 return $nonce;
399 } else {
400 return false;
401 }
402 }
403
404 /**
405 * Does the actual email unsubscription.
406 *
407 * @since 3.0.5
408 * @param string $email Email to be unsubscribed.
409 * @param string $nonce Nonce associated with the email for the unsubscription.
410 * @return boolean
411 */
412 public function unsubscribe_email( $email, $nonce ) {
413 $data = get_option( self::KEY_UNSUBSCRIBE_NONCES, false );
414 if ( ! $data ) {
415 return false;
416 }
417 if ( ! isset( $data[ $email ] ) || ! isset( $data[ $email ]['nonce'] ) || ! isset( $data[ $email ]['lists_id'] ) ) {
418 return false;
419 }
420 $email_data = $data[ $email ];
421 if ( ! hash_equals( (string) $email_data['nonce'], $nonce ) ) {
422 return false;
423 }
424 // Nonce expired. Remove it. Currently giving 1 day of life span.
425 if ( ( time() - (int) $email_data['date_created'] ) > DAY_IN_SECONDS ) {
426 unset( $data[ $email ] );
427 update_option( self::KEY_UNSUBSCRIBE_NONCES, $data );
428 return false;
429 }
430
431 // Proceed to unsubscribe.
432 foreach ( $email_data['lists_id'] as $id ) {
433 $unsubscribed = $this->remove_local_subscription_by_email_and_module_id( $email, $id );
434 }
435
436 // The email was unsubscribed and the nonce was used. Remove it from the saved list.
437 unset( $data[ $email ] );
438 update_option( self::KEY_UNSUBSCRIBE_NONCES, $data );
439
440 return true;
441 }
442
443 /**
444 * Updates the metas specific for Non Social Sharing modules.
445 *
446 * @since 4.3.0
447 * @param array $data Data to save.
448 * @return void
449 */
450 protected function update_module_metas( $data ) {
451 // Meta used in all module types.
452 if ( isset( $data['content'] ) ) {
453 if ( ! empty( $data['content']['feature_image'] ) ) {
454 $data['content']['feature_image_alt'] = $this->update_feature_image_alt( $data['content']['feature_image'], false );
455 }
456 $this->update_meta( self::KEY_CONTENT, $data['content'] );
457 }
458 // Meta used in all module types.
459 if ( isset( $data['visibility'] ) ) {
460 $this->update_meta( self::KEY_VISIBILITY, $data['visibility'] );
461 }
462
463 // Design tab.
464 if ( isset( $data['design'] ) ) {
465 $saved_design = $this->get_design()->to_array();
466 $new_design = array_merge( $saved_design, $data['design'] );
467
468 $this->update_meta( self::KEY_DESIGN, $new_design );
469 }
470
471 // Emails tab.
472 if ( isset( $data['emails'] ) ) {
473 $this->update_meta( self::KEY_EMAILS, $data['emails'] );
474 }
475
476 // Settings tab.
477 if ( isset( $data['settings'] ) ) {
478 // Clear flags to skip cached schedule values.
479 $this->set_schedule_flags( array() );
480 $this->update_meta( self::KEY_SETTINGS, $data['settings'] );
481 }
482
483 // Integrations tab.
484 if ( isset( $data['integrations_settings'] ) ) {
485 $this->update_meta( self::KEY_INTEGRATIONS_SETTINGS, $data['integrations_settings'] );
486 }
487
488 // Embedded only meta.
489 if ( self::EMBEDDED_MODULE === $this->module_type && isset( $data['display'] ) ) {
490 $this->update_meta( self::KEY_DISPLAY_OPTIONS, $data['display'] );
491 }
492
493 // Activate integrations if provided.
494 if ( isset( $data['integrations'] ) ) {
495 $this->activate_providers( $data );
496 }
497
498 $this->maybe_update_custom_fields();
499 }
500
501 /**
502 * Sanitize/Replace the module's data.
503 *
504 * @param array $data Data to sanitize.
505 * @return array Sanitized data.
506 */
507 public function sanitize_module( $data ) {
508 $design_obj = $this->get_design();
509 $default_options = $design_obj->get_defaults();
510 $saved_options = $design_obj->to_array();
511 $new_options = ! empty( $data['design'] ) ? $data['design'] : array();
512 $typography_options = $design_obj->get_typography_defaults( 'desktop' );
513
514 // Check is `Border, Spacing and Shadow` enabled for desktop.
515 $spacing_on = $this->get_newest_value( 'customize_border_shadow_spacing', $new_options, $saved_options );
516 // Check is `Typography` enabled for desktop.
517 $typography_on = $this->get_newest_value( 'customize_typography', $new_options, $saved_options );
518
519 foreach ( $new_options as $option_name => $value ) {
520 if ( $spacing_on ) {
521 $data = $this->replace_empty_spacing_numbers( $data, $option_name, $value, $default_options );
522 }
523 if ( $typography_on ) {
524 $data = $this->replace_empty_typography_numbers( $data, $option_name, $value, $default_options, $typography_options );
525 }
526 }
527
528 if ( ! empty( $data['module']['module_name'] ) ) {
529 $data['module']['module_name'] = sanitize_text_field( $data['module']['module_name'] );
530 }
531
532 if ( ! empty( $data['settings'] ) && ! is_array( $data['settings'] ) ) {
533 $setting_json = true;
534 $data['settings'] = json_decode( $data['settings'], true );
535 }
536
537 array_walk_recursive(
538 $data,
539 function ( &$value, $key ) {
540 $consist_html = apply_filters(
541 'hustle_fields_with_html',
542 array(
543 'main_content',
544 'title',
545 'sub_title',
546 'email_body',
547 'success_message',
548 'emailmessage',
549 'email_message',
550 'gdpr_message',
551 'required_error_message',
552 'v3_recaptcha_badge_replacement',
553 )
554 );
555 if ( in_array( $key, array( 'refs', 'urls' ), true ) ) {
556 // Handle Visibility -> URL textarea.
557 $urls = preg_split( '/\r\n|\r|\n/', $value );
558 $urls = array_map(
559 function ( $v ) {
560 return filter_var( wp_strip_all_tags( $v ), FILTER_SANITIZE_URL );
561 },
562 (array) $urls
563 );
564 $value = implode( "\n", $urls );
565 } elseif ( in_array( $key, $consist_html, true ) ) {
566 $value = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $value ) ) );
567 if ( ! in_array( $key, array( 'main_content', 'emailmessage', 'email_message', 'success_message' ), true ) ) {
568 $value = wp_kses_post( $value );
569 }
570 } elseif ( 'custom_css' === $key ) {
571 $value = sanitize_textarea_field( $value );
572 } elseif ( ! is_int( $value ) ) {
573 $value = sanitize_text_field( $value );
574 }
575 }
576 );
577
578 if ( ! empty( $setting_json ) ) {
579 $data['settings'] = wp_json_encode( $data['settings'] );
580 }
581
582 if ( isset( $data['emails']['form_elements'] ) ) {
583 $data['emails']['form_elements'] = $this->sanitize_form_elements( $data['emails']['form_elements'] );
584 }
585
586 return $data;
587 }
588
589 /**
590 * Validates the module's data.
591 *
592 * @since 4.0.3
593 *
594 * @param array $data Data to validate.
595 * @return array
596 */
597 public function validate_module( $data ) {
598 $errors = array();
599
600 // Name validation.
601 if ( empty( $data['module']['module_name'] ) ) {
602 $errors['error']['name_error'] = __( 'This field is required', 'hustle' );
603
604 return $errors;
605 }
606
607 $validator = $this->create_validator();
608
609 $field_errors = array();
610 foreach ( Hustle_Module_Fields::FIELDS as $section => $fields ) {
611 if ( ! isset( $data[ $section ] ) ) {
612 continue;
613 }
614
615 $result = $validator->validate( $fields, $data[ $section ] );
616 if ( ! $result['is_valid'] ) {
617 $field_errors[ $section ] = $result['errors'];
618 }
619 }
620
621 if ( ! empty( $field_errors ) ) {
622 $errors['error']['field_errors'] = $field_errors;
623 return $errors;
624 }
625
626 return true;
627 }
628
629 /**
630 * Create the validator instance.
631 *
632 * @return Hustle_Module_Fields_Validator
633 */
634 protected function create_validator() {
635 return new Hustle_Module_Fields_Validator();
636 }
637
638 /**
639 * Get newest value.
640 *
641 * @param string $option_name Option name.
642 * @param array $new_options New options.
643 * @param array $saved_options Old options.
644 * @return bool
645 */
646 private function get_newest_value( $option_name, $new_options, $saved_options ) {
647 if ( isset( $new_options[ $option_name ] ) ) {
648 $value = $new_options[ $option_name ];
649 } elseif ( isset( $saved_options[ $option_name ] ) ) {
650 $value = $saved_options[ $option_name ];
651 } else {
652 $value = '';
653 }
654
655 return $value;
656 }
657
658 /**
659 * Check if it's spacing option and value isn't set
660 *
661 * @param string $key Option name.
662 * @param string $value Option value.
663 * @return bool
664 */
665 private function is_empty_spacing_number( $key, $value ) {
666 $needles = array( '_margin_', '_padding_', '_shadow_', '_radius_', '_border_' );
667 return $this->similar_in_array( $key, $needles ) && '' === $value;
668 }
669
670 /**
671 * Check if it's typography option and value isn't set
672 *
673 * @param string $key Option name.
674 * @param string $value Option value.
675 * @param array $typography_options Typography options.
676 * @return bool
677 */
678 private function is_empty_typography_number( $key, $value, $typography_options ) {
679 return '' === $value && isset( $typography_options[ $key ] )
680 && ( is_float( $typography_options[ $key ] ) || is_int( $typography_options[ $key ] ) );
681 }
682
683 /**
684 * If it's an empty number option from Destop section - replace it to relevant default value.
685 *
686 * @param string $data Data for sanitize.
687 * @param string $option_name Option name.
688 * @param string $option_value Option value.
689 * @param array $default_options Default options.
690 * @return string
691 */
692 private function replace_empty_spacing_numbers( $data, $option_name, $option_value, $default_options ) {
693 if ( $this->is_empty_spacing_number( $option_name, $option_value ) && '_mobile' !== substr( $option_name, -7 ) ) {
694 $data['design'][ $option_name ] = isset( $default_options[ $option_name ] ) ? $default_options[ $option_name ] : 0;
695 }
696
697 return $data;
698 }
699
700 /**
701 * If it's an empty number option from Typography section - replace it to relevant default value.
702 *
703 * @param string $data Data for sanitize.
704 * @param string $option_name Option name.
705 * @param string $option_value Option value.
706 * @param array $default_options Default options.
707 * @param array $typography_options Typography options.
708 * @return string
709 */
710 private function replace_empty_typography_numbers( $data, $option_name, $option_value, $default_options, $typography_options ) {
711 if ( $this->is_empty_typography_number( $option_name, $option_value, $typography_options ) && '_mobile' !== substr( $option_name, -7 ) ) {
712 $data['design'][ $option_name ] = isset( $default_options[ $option_name ] ) ? $default_options[ $option_name ] : 0;
713 }
714
715 return $data;
716 }
717
718 /**
719 * Checks if a value contains a part of any array item
720 *
721 * @param array $haystack The value.
722 * @param string $needles The array of pices.
723 * @return boolean
724 */
725 private function similar_in_array( $haystack, $needles ) {
726 foreach ( $needles as $needle ) {
727 if ( false !== strpos( $haystack, $needle ) ) {
728 return true;
729 }
730 }
731
732 return false;
733 }
734
735 /**
736 * Get renderer
737 *
738 * @return \Hustle_Module_Renderer
739 */
740 public function get_renderer() {
741 return new Hustle_Module_Renderer();
742 }
743
744 /**
745 * Sanitize the form fields name replacing spaces by underscores.
746 * This way the data is handled properly along hustle.
747 *
748 * @since 4.0
749 * @param string $name Name.
750 * @return string
751 */
752 private function sanitize_form_field_name( $name ) {
753 $sanitized_name = apply_filters( 'hustle_sanitize_form_field_name', str_replace( ' ', '_', trim( $name ) ), $name );
754
755 return sanitize_text_field( $sanitized_name );
756 }
757
758 /**
759 * Sanitize form elements
760 *
761 * @param array $form_elements Form elements.
762 * @return type
763 */
764 public function sanitize_form_elements( $form_elements ) {
765 // Sanitize GDPR message.
766 if ( isset( $form_elements['gdpr']['gdpr_message'] ) ) {
767 $allowed_html = array(
768 'a' => array(
769 'href' => true,
770 'title' => true,
771 'target' => true,
772 'alt' => true,
773 ),
774 'b' => array(),
775 'strong' => array(),
776 'i' => array(),
777 'em' => array(),
778 'del' => array(),
779 );
780 $form_elements['gdpr']['gdpr_message'] = wp_kses( wp_unslash( $form_elements['gdpr']['gdpr_message'] ), $allowed_html );
781 }
782
783 $sanitized_fields = array();
784
785 // Loop through each form field.
786 foreach ( $form_elements as $field_data ) {
787 $name = $this->sanitize_form_field_name( $field_data['name'] );
788
789 // After sanitize if name become empty then create array key from label.
790 if ( ! $name ) {
791 $name = $this->sanitize_form_field_name( $field_data['label'] );
792
793 // If still key is empty then go to next iteration.
794 if ( ! $name ) {
795 continue;
796 }
797 }
798
799 // Check field name already exists or not. If exists then create a new name.
800 $name = $this->get_unique_field_name( $sanitized_fields, $name );
801
802 // Sanitize necessary fields.
803 $field_data['name'] = $name;
804 $field_data['label'] = sanitize_text_field( $field_data['label'] );
805 $field_data['placeholder'] = sanitize_text_field( $field_data['placeholder'] );
806
807 // Add new item with field data.
808 $sanitized_fields[ $name ] = $field_data;
809 }
810
811 return $sanitized_fields;
812 }
813
814 /**
815 * Update Custom Fields for Sendgrid New Campaigns
816 */
817 private function maybe_update_custom_fields() {
818 $connected_addons = Hustle_Provider_Utils::get_addons_instance_connected_with_module( $this->module_id );
819
820 foreach ( $connected_addons as $addon ) {
821
822 // Change logic only for sendgrid for now.
823 if ( 'sendgrid' !== $addon->get_slug() ) {
824 continue;
825 }
826 $global_multi_id = $addon->selected_global_multi_id;
827 $new_campaigns = $addon->get_setting( 'new_campaigns', '', $global_multi_id );
828
829 // only if it's the New Sendgrid Campaigns.
830 if ( 'new_campaigns' !== $new_campaigns ) {
831 continue;
832 }
833 $emails = $this->get_emails()->to_array();
834 $custom_fields = array();
835
836 $api_key = $addon->get_setting( 'api_key', '', $global_multi_id );
837 $api = $addon::api( $api_key, $new_campaigns );
838
839 foreach ( $emails['form_elements'] as $element ) {
840 if ( empty( $element['type'] ) || in_array( $element['type'], array( 'submit', 'recaptcha', 'turnstile' ), true ) ) {
841 continue;
842 }
843 $custom_fields[] = array(
844 'type' => 'text',
845 'name' => $element['name'],
846 );
847 }
848
849 if ( ! empty( $custom_fields ) ) {
850 $api->add_custom_fields( $custom_fields );
851 }
852 }
853 }
854
855 /**
856 * Gets the selected Google fonts for the active elements in the module.
857 * Used for non-ssharing modules only.
858 *
859 * @since 4.3.0
860 *
861 * @return array
862 */
863 public function get_google_fonts() {
864 $fonts = array();
865
866 if ( '1' === $this->design->use_vanilla ) {
867 return $fonts;
868 }
869
870 $elements = array(
871 'title' => '' !== $this->content->title,
872 'subtitle' => '' !== $this->content->sub_title,
873 'main_content_paragraph' => '' !== $this->content->main_content,
874 'main_content_heading_one' => '' !== $this->content->main_content,
875 'main_content_heading_two' => '' !== $this->content->main_content,
876 'main_content_heading_three' => '' !== $this->content->main_content,
877 'main_content_heading_four' => '' !== $this->content->main_content,
878 'main_content_heading_five' => '' !== $this->content->main_content,
879 'main_content_heading_six' => '' !== $this->content->main_content,
880 'cta' => '0' !== $this->content->show_cta,
881 'never_see_link' => '0' !== $this->content->show_never_see_link,
882 );
883
884 // Only list the font of the elements that are shown, and aren't using a 'custom' font.
885 foreach ( $elements as $element_name => $is_shown ) {
886 if ( ! $is_shown ) {
887 continue;
888 }
889
890 $font = $this->design->{ $element_name . '_font_family' };
891 if ( 'custom' !== $font ) {
892 $font_weight = $this->design->{ $element_name . '_font_weight' };
893 if ( ! isset( $fonts[ $font ] ) ) {
894 $fonts[ $font ] = array();
895 }
896 if ( ! in_array( $font_weight, $fonts[ $font ], true ) ) {
897 $fonts[ $font ][] = $font_weight;
898 }
899 }
900 }
901
902 // We're done here for informational modules.
903 if ( self::OPTIN_MODE !== $this->module_mode ) {
904 return $fonts;
905 }
906
907 $has_mailchimp = ! empty( $this->get_provider_settings( 'mailchimp' ) );
908
909 $form_fields = $this->get_form_fields();
910 $has_success_message = 'show_success' === $this->emails->after_successful_submission;
911
912 $elements_optin = array(
913 'form_extras' => $has_mailchimp,
914 'input' => true,
915 'select' => $has_mailchimp,
916 'checkbox' => $has_mailchimp,
917 'dropdown' => $has_mailchimp,
918 'gdpr' => ! empty( $form_fields['gdpr'] ),
919 'recaptcha' => ! empty( $form_fields['recaptcha'] ),
920 'submit_button' => true,
921 'success_message_paragraph' => $has_success_message,
922 'success_message_heading_one' => $has_success_message,
923 'success_message_heading_two' => $has_success_message,
924 'success_message_heading_three' => $has_success_message,
925 'success_message_heading_four' => $has_success_message,
926 'success_message_heading_five' => $has_success_message,
927 'success_message_heading_six' => $has_success_message,
928 'error_message' => true,
929 );
930
931 foreach ( $elements_optin as $element_name => $is_shown ) {
932 if ( ! $is_shown ) {
933 continue;
934 }
935
936 $font = $this->design->{ $element_name . '_font_family' };
937 if ( 'custom' !== $font ) {
938 $font_weight = $this->design->{ $element_name . '_font_weight' };
939 if ( ! isset( $fonts[ $font ] ) ) {
940 $fonts[ $font ] = array();
941 }
942 if ( ! in_array( $font_weight, $fonts[ $font ], true ) ) {
943 $fonts[ $font ][] = $font_weight;
944 }
945 }
946 }
947 return $fonts;
948 }
949
950 /**
951 * Find the unique name of field without overriding others.
952 *
953 * @since 4.3.3
954 *
955 * @param array $sanitized_fields Array for fields that are previously sanitized.
956 * @param string $field_name field name which will be compare.
957 *
958 * @return array
959 */
960 private function get_unique_field_name( $sanitized_fields, $field_name ) {
961 $new_name = $field_name;
962 $i = 0;
963
964 while ( array_key_exists( $new_name, $sanitized_fields ) ) {
965 ++$i;
966 $new_name = $field_name . '-' . $i;
967 }
968
969 return $new_name;
970 }
971
972 /**
973 * Disconnect an integration from the module by its slug.
974 *
975 * @since 7.8.13
976 *
977 * @param string $slug Integration slug.
978 * @return void
979 */
980 public function disconnect_integration( $slug ) {
981 $settings = $this->get_integrations_settings()->to_array();
982
983 if (
984 isset( $settings['active_integrations'] ) &&
985 is_string( $settings['active_integrations'] )
986 ) {
987 $integrations = explode( ',', $settings['active_integrations'] );
988 $integrations = array_map( 'trim', $integrations );
989
990 if ( in_array( $slug, $integrations, true ) ) {
991 // Remove the slug from the active integrations list.
992 $integrations = array_diff( $integrations, array( $slug ) );
993 $integrations_count = count( $integrations );
994
995 $settings['active_integrations'] = implode( ',', $integrations );
996 }
997 }
998
999 if ( isset( $integrations_count ) ) {
1000 $settings['active_integrations_count'] = $integrations_count;
1001 }
1002
1003 $this->update_meta( self::KEY_INTEGRATIONS_SETTINGS, $settings );
1004
1005 // Delete the integration-specific settings meta.
1006 $integration_meta_key = $slug . self::KEY_PROVIDER;
1007 $this->update_meta( $integration_meta_key, array() );
1008 }
1009
1010 /**
1011 * Connect an integration to the module by its slug.
1012 *
1013 * @since 7.8.13
1014 *
1015 * @param string $slug Integration slug.
1016 * @return void
1017 */
1018 public function connect_integration( $slug ) {
1019 $settings = $this->get_integrations_settings()->to_array();
1020
1021 if ( isset( $settings['active_integrations'] ) && is_string( $settings['active_integrations'] ) ) {
1022 $integrations = explode( ',', $settings['active_integrations'] );
1023 $integrations = array_map( 'trim', $integrations );
1024
1025 if ( ! in_array( $slug, $integrations, true ) ) {
1026 $integrations[] = $slug;
1027 $integrations_count = count( $integrations );
1028
1029 $settings['active_integrations'] = implode( ',', $integrations );
1030 }
1031 } else {
1032 $settings['active_integrations'] = $slug;
1033 $integrations_count = 1;
1034 }
1035
1036 if ( isset( $integrations_count ) ) {
1037 $settings['active_integrations_count'] = $integrations_count;
1038 }
1039
1040 $this->update_meta( self::KEY_INTEGRATIONS_SETTINGS, $settings );
1041 }
1042 }
1043