PluginProbe ʕ •ᴥ•ʔ
Hustle – Email Marketing, Lead Generation, Optins, Popups / 7.8.14.2
Hustle – Email Marketing, Lead Generation, Optins, Popups v7.8.14.2
7.8.14.2 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 1 week ago front 1 week ago helpers 1 week ago metas 1 week ago multisite 1 week ago palettes 1 week ago provider 1 week ago providers 1 week ago templates 1 week ago update 1 week ago class-hustle-admin-page-abstract.php 1 week ago class-hustle-auth-token.php 1 week ago class-hustle-condition-factory.php 1 week ago class-hustle-cross-sell.php 1 week ago class-hustle-dashboard-admin.php 1 week ago class-hustle-data.php 1 week ago class-hustle-db.php 1 week ago class-hustle-installer.php 1 week ago class-hustle-meta.php 1 week ago class-hustle-module-admin.php 1 week ago class-hustle-module-collection.php 1 week ago class-hustle-module-fields.php 1 week ago class-hustle-module-page-abstract.php 1 week ago class-hustle-module-validator.php 1 week ago class-hustle-notifications.php 1 week ago class-hustle-settings-admin.php 1 week ago class-hustle-wp-dashboard-page.php 1 week ago class-opt-in.php 1 week ago hustle-background-conversion-log.php 1 week ago hustle-deletion.php 1 week ago hustle-embedded-admin.php 1 week ago hustle-entries-admin.php 1 week ago hustle-entry-model.php 1 week ago hustle-general-data-protection.php 1 week ago hustle-init.php 1 week ago hustle-mail.php 1 week ago hustle-migration.php 1 week ago hustle-model.php 1 week ago hustle-module-model.php 1 week ago hustle-module-widget-legacy.php 1 week ago hustle-module-widget.php 1 week ago hustle-modules-common-admin-ajax.php 1 week ago hustle-popup-admin.php 1 week ago hustle-providers-admin.php 1 week ago hustle-providers.php 1 week ago hustle-settings-admin-ajax.php 1 week ago hustle-settings-page.php 1 week ago hustle-slidein-admin.php 1 week ago hustle-sshare-admin.php 1 week ago hustle-sshare-model.php 1 week ago hustle-tracking-model.php 1 week ago interface-hustle-auth-provider.php 1 week ago opt-in-geo.php 1 week ago opt-in-utils.php 1 week ago opt-in-wpmudev-api.php 1 week ago
hustle-module-model.php
1044 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 'notification_email_body',
554 )
555 );
556 if ( in_array( $key, array( 'refs', 'urls' ), true ) ) {
557 // Handle Visibility -> URL textarea.
558 $urls = preg_split( '/\r\n|\r|\n/', $value );
559 $urls = array_map(
560 function ( $v ) {
561 return filter_var( wp_strip_all_tags( $v ), FILTER_SANITIZE_URL );
562 },
563 (array) $urls
564 );
565 $value = implode( "\n", $urls );
566 } elseif ( in_array( $key, $consist_html, true ) ) {
567 $value = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $value ) ) );
568 if ( ! in_array( $key, array( 'main_content', 'emailmessage', 'email_message', 'success_message' ), true ) ) {
569 $value = wp_kses_post( $value );
570 }
571 } elseif ( 'custom_css' === $key ) {
572 $value = sanitize_textarea_field( $value );
573 } elseif ( ! is_int( $value ) ) {
574 $value = sanitize_text_field( $value );
575 }
576 }
577 );
578
579 if ( ! empty( $setting_json ) ) {
580 $data['settings'] = wp_json_encode( $data['settings'] );
581 }
582
583 if ( isset( $data['emails']['form_elements'] ) ) {
584 $data['emails']['form_elements'] = $this->sanitize_form_elements( $data['emails']['form_elements'] );
585 }
586
587 return $data;
588 }
589
590 /**
591 * Validates the module's data.
592 *
593 * @since 4.0.3
594 *
595 * @param array $data Data to validate.
596 * @return array
597 */
598 public function validate_module( $data ) {
599 $errors = array();
600
601 // Name validation.
602 if ( empty( $data['module']['module_name'] ) ) {
603 $errors['error']['name_error'] = __( 'This field is required', 'hustle' );
604
605 return $errors;
606 }
607
608 $validator = $this->create_validator();
609
610 $field_errors = array();
611 foreach ( Hustle_Module_Fields::FIELDS as $section => $fields ) {
612 if ( ! isset( $data[ $section ] ) ) {
613 continue;
614 }
615
616 $result = $validator->validate( $fields, $data[ $section ] );
617 if ( ! $result['is_valid'] ) {
618 $field_errors[ $section ] = $result['errors'];
619 }
620 }
621
622 if ( ! empty( $field_errors ) ) {
623 $errors['error']['field_errors'] = $field_errors;
624 return $errors;
625 }
626
627 return true;
628 }
629
630 /**
631 * Create the validator instance.
632 *
633 * @return Hustle_Module_Fields_Validator
634 */
635 protected function create_validator() {
636 return new Hustle_Module_Fields_Validator();
637 }
638
639 /**
640 * Get newest value.
641 *
642 * @param string $option_name Option name.
643 * @param array $new_options New options.
644 * @param array $saved_options Old options.
645 * @return bool
646 */
647 private function get_newest_value( $option_name, $new_options, $saved_options ) {
648 if ( isset( $new_options[ $option_name ] ) ) {
649 $value = $new_options[ $option_name ];
650 } elseif ( isset( $saved_options[ $option_name ] ) ) {
651 $value = $saved_options[ $option_name ];
652 } else {
653 $value = '';
654 }
655
656 return $value;
657 }
658
659 /**
660 * Check if it's spacing option and value isn't set
661 *
662 * @param string $key Option name.
663 * @param string $value Option value.
664 * @return bool
665 */
666 private function is_empty_spacing_number( $key, $value ) {
667 $needles = array( '_margin_', '_padding_', '_shadow_', '_radius_', '_border_' );
668 return $this->similar_in_array( $key, $needles ) && '' === $value;
669 }
670
671 /**
672 * Check if it's typography option and value isn't set
673 *
674 * @param string $key Option name.
675 * @param string $value Option value.
676 * @param array $typography_options Typography options.
677 * @return bool
678 */
679 private function is_empty_typography_number( $key, $value, $typography_options ) {
680 return '' === $value && isset( $typography_options[ $key ] )
681 && ( is_float( $typography_options[ $key ] ) || is_int( $typography_options[ $key ] ) );
682 }
683
684 /**
685 * If it's an empty number option from Destop section - replace it to relevant default value.
686 *
687 * @param string $data Data for sanitize.
688 * @param string $option_name Option name.
689 * @param string $option_value Option value.
690 * @param array $default_options Default options.
691 * @return string
692 */
693 private function replace_empty_spacing_numbers( $data, $option_name, $option_value, $default_options ) {
694 if ( $this->is_empty_spacing_number( $option_name, $option_value ) && '_mobile' !== substr( $option_name, -7 ) ) {
695 $data['design'][ $option_name ] = isset( $default_options[ $option_name ] ) ? $default_options[ $option_name ] : 0;
696 }
697
698 return $data;
699 }
700
701 /**
702 * If it's an empty number option from Typography section - replace it to relevant default value.
703 *
704 * @param string $data Data for sanitize.
705 * @param string $option_name Option name.
706 * @param string $option_value Option value.
707 * @param array $default_options Default options.
708 * @param array $typography_options Typography options.
709 * @return string
710 */
711 private function replace_empty_typography_numbers( $data, $option_name, $option_value, $default_options, $typography_options ) {
712 if ( $this->is_empty_typography_number( $option_name, $option_value, $typography_options ) && '_mobile' !== substr( $option_name, -7 ) ) {
713 $data['design'][ $option_name ] = isset( $default_options[ $option_name ] ) ? $default_options[ $option_name ] : 0;
714 }
715
716 return $data;
717 }
718
719 /**
720 * Checks if a value contains a part of any array item
721 *
722 * @param array $haystack The value.
723 * @param string $needles The array of pices.
724 * @return boolean
725 */
726 private function similar_in_array( $haystack, $needles ) {
727 foreach ( $needles as $needle ) {
728 if ( false !== strpos( $haystack, $needle ) ) {
729 return true;
730 }
731 }
732
733 return false;
734 }
735
736 /**
737 * Get renderer
738 *
739 * @return \Hustle_Module_Renderer
740 */
741 public function get_renderer() {
742 return new Hustle_Module_Renderer();
743 }
744
745 /**
746 * Sanitize the form fields name replacing spaces by underscores.
747 * This way the data is handled properly along hustle.
748 *
749 * @since 4.0
750 * @param string $name Name.
751 * @return string
752 */
753 private function sanitize_form_field_name( $name ) {
754 $sanitized_name = apply_filters( 'hustle_sanitize_form_field_name', str_replace( ' ', '_', trim( $name ) ), $name );
755
756 return sanitize_text_field( $sanitized_name );
757 }
758
759 /**
760 * Sanitize form elements
761 *
762 * @param array $form_elements Form elements.
763 * @return type
764 */
765 public function sanitize_form_elements( $form_elements ) {
766 // Sanitize GDPR message.
767 if ( isset( $form_elements['gdpr']['gdpr_message'] ) ) {
768 $allowed_html = array(
769 'a' => array(
770 'href' => true,
771 'title' => true,
772 'target' => true,
773 'alt' => true,
774 ),
775 'b' => array(),
776 'strong' => array(),
777 'i' => array(),
778 'em' => array(),
779 'del' => array(),
780 );
781 $form_elements['gdpr']['gdpr_message'] = wp_kses( wp_unslash( $form_elements['gdpr']['gdpr_message'] ), $allowed_html );
782 }
783
784 $sanitized_fields = array();
785
786 // Loop through each form field.
787 foreach ( $form_elements as $field_data ) {
788 $name = $this->sanitize_form_field_name( $field_data['name'] );
789
790 // After sanitize if name become empty then create array key from label.
791 if ( ! $name ) {
792 $name = $this->sanitize_form_field_name( $field_data['label'] );
793
794 // If still key is empty then go to next iteration.
795 if ( ! $name ) {
796 continue;
797 }
798 }
799
800 // Check field name already exists or not. If exists then create a new name.
801 $name = $this->get_unique_field_name( $sanitized_fields, $name );
802
803 // Sanitize necessary fields.
804 $field_data['name'] = $name;
805 $field_data['label'] = sanitize_text_field( $field_data['label'] );
806 $field_data['placeholder'] = sanitize_text_field( $field_data['placeholder'] );
807
808 // Add new item with field data.
809 $sanitized_fields[ $name ] = $field_data;
810 }
811
812 return $sanitized_fields;
813 }
814
815 /**
816 * Update Custom Fields for Sendgrid New Campaigns
817 */
818 private function maybe_update_custom_fields() {
819 $connected_addons = Hustle_Provider_Utils::get_addons_instance_connected_with_module( $this->module_id );
820
821 foreach ( $connected_addons as $addon ) {
822
823 // Change logic only for sendgrid for now.
824 if ( 'sendgrid' !== $addon->get_slug() ) {
825 continue;
826 }
827 $global_multi_id = $addon->selected_global_multi_id;
828 $new_campaigns = $addon->get_setting( 'new_campaigns', '', $global_multi_id );
829
830 // only if it's the New Sendgrid Campaigns.
831 if ( 'new_campaigns' !== $new_campaigns ) {
832 continue;
833 }
834 $emails = $this->get_emails()->to_array();
835 $custom_fields = array();
836
837 $api_key = $addon->get_setting( 'api_key', '', $global_multi_id );
838 $api = $addon::api( $api_key, $new_campaigns );
839
840 foreach ( $emails['form_elements'] as $element ) {
841 if ( empty( $element['type'] ) || in_array( $element['type'], array( 'submit', 'recaptcha', 'turnstile' ), true ) ) {
842 continue;
843 }
844 $custom_fields[] = array(
845 'type' => 'text',
846 'name' => $element['name'],
847 );
848 }
849
850 if ( ! empty( $custom_fields ) ) {
851 $api->add_custom_fields( $custom_fields );
852 }
853 }
854 }
855
856 /**
857 * Gets the selected Google fonts for the active elements in the module.
858 * Used for non-ssharing modules only.
859 *
860 * @since 4.3.0
861 *
862 * @return array
863 */
864 public function get_google_fonts() {
865 $fonts = array();
866
867 if ( '1' === $this->design->use_vanilla ) {
868 return $fonts;
869 }
870
871 $elements = array(
872 'title' => '' !== $this->content->title,
873 'subtitle' => '' !== $this->content->sub_title,
874 'main_content_paragraph' => '' !== $this->content->main_content,
875 'main_content_heading_one' => '' !== $this->content->main_content,
876 'main_content_heading_two' => '' !== $this->content->main_content,
877 'main_content_heading_three' => '' !== $this->content->main_content,
878 'main_content_heading_four' => '' !== $this->content->main_content,
879 'main_content_heading_five' => '' !== $this->content->main_content,
880 'main_content_heading_six' => '' !== $this->content->main_content,
881 'cta' => '0' !== $this->content->show_cta,
882 'never_see_link' => '0' !== $this->content->show_never_see_link,
883 );
884
885 // Only list the font of the elements that are shown, and aren't using a 'custom' font.
886 foreach ( $elements as $element_name => $is_shown ) {
887 if ( ! $is_shown ) {
888 continue;
889 }
890
891 $font = $this->design->{ $element_name . '_font_family' };
892 if ( 'custom' !== $font ) {
893 $font_weight = $this->design->{ $element_name . '_font_weight' };
894 if ( ! isset( $fonts[ $font ] ) ) {
895 $fonts[ $font ] = array();
896 }
897 if ( ! in_array( $font_weight, $fonts[ $font ], true ) ) {
898 $fonts[ $font ][] = $font_weight;
899 }
900 }
901 }
902
903 // We're done here for informational modules.
904 if ( self::OPTIN_MODE !== $this->module_mode ) {
905 return $fonts;
906 }
907
908 $has_mailchimp = ! empty( $this->get_provider_settings( 'mailchimp' ) );
909
910 $form_fields = $this->get_form_fields();
911 $has_success_message = 'show_success' === $this->emails->after_successful_submission;
912
913 $elements_optin = array(
914 'form_extras' => $has_mailchimp,
915 'input' => true,
916 'select' => $has_mailchimp,
917 'checkbox' => $has_mailchimp,
918 'dropdown' => $has_mailchimp,
919 'gdpr' => ! empty( $form_fields['gdpr'] ),
920 'recaptcha' => ! empty( $form_fields['recaptcha'] ),
921 'submit_button' => true,
922 'success_message_paragraph' => $has_success_message,
923 'success_message_heading_one' => $has_success_message,
924 'success_message_heading_two' => $has_success_message,
925 'success_message_heading_three' => $has_success_message,
926 'success_message_heading_four' => $has_success_message,
927 'success_message_heading_five' => $has_success_message,
928 'success_message_heading_six' => $has_success_message,
929 'error_message' => true,
930 );
931
932 foreach ( $elements_optin as $element_name => $is_shown ) {
933 if ( ! $is_shown ) {
934 continue;
935 }
936
937 $font = $this->design->{ $element_name . '_font_family' };
938 if ( 'custom' !== $font ) {
939 $font_weight = $this->design->{ $element_name . '_font_weight' };
940 if ( ! isset( $fonts[ $font ] ) ) {
941 $fonts[ $font ] = array();
942 }
943 if ( ! in_array( $font_weight, $fonts[ $font ], true ) ) {
944 $fonts[ $font ][] = $font_weight;
945 }
946 }
947 }
948 return $fonts;
949 }
950
951 /**
952 * Find the unique name of field without overriding others.
953 *
954 * @since 4.3.3
955 *
956 * @param array $sanitized_fields Array for fields that are previously sanitized.
957 * @param string $field_name field name which will be compare.
958 *
959 * @return array
960 */
961 private function get_unique_field_name( $sanitized_fields, $field_name ) {
962 $new_name = $field_name;
963 $i = 0;
964
965 while ( array_key_exists( $new_name, $sanitized_fields ) ) {
966 ++$i;
967 $new_name = $field_name . '-' . $i;
968 }
969
970 return $new_name;
971 }
972
973 /**
974 * Disconnect an integration from the module by its slug.
975 *
976 * @since 7.8.13
977 *
978 * @param string $slug Integration slug.
979 * @return void
980 */
981 public function disconnect_integration( $slug ) {
982 $settings = $this->get_integrations_settings()->to_array();
983
984 if (
985 isset( $settings['active_integrations'] ) &&
986 is_string( $settings['active_integrations'] )
987 ) {
988 $integrations = explode( ',', $settings['active_integrations'] );
989 $integrations = array_map( 'trim', $integrations );
990
991 if ( in_array( $slug, $integrations, true ) ) {
992 // Remove the slug from the active integrations list.
993 $integrations = array_diff( $integrations, array( $slug ) );
994 $integrations_count = count( $integrations );
995
996 $settings['active_integrations'] = implode( ',', $integrations );
997 }
998 }
999
1000 if ( isset( $integrations_count ) ) {
1001 $settings['active_integrations_count'] = $integrations_count;
1002 }
1003
1004 $this->update_meta( self::KEY_INTEGRATIONS_SETTINGS, $settings );
1005
1006 // Delete the integration-specific settings meta.
1007 $integration_meta_key = $slug . self::KEY_PROVIDER;
1008 $this->update_meta( $integration_meta_key, array() );
1009 }
1010
1011 /**
1012 * Connect an integration to the module by its slug.
1013 *
1014 * @since 7.8.13
1015 *
1016 * @param string $slug Integration slug.
1017 * @return void
1018 */
1019 public function connect_integration( $slug ) {
1020 $settings = $this->get_integrations_settings()->to_array();
1021
1022 if ( isset( $settings['active_integrations'] ) && is_string( $settings['active_integrations'] ) ) {
1023 $integrations = explode( ',', $settings['active_integrations'] );
1024 $integrations = array_map( 'trim', $integrations );
1025
1026 if ( ! in_array( $slug, $integrations, true ) ) {
1027 $integrations[] = $slug;
1028 $integrations_count = count( $integrations );
1029
1030 $settings['active_integrations'] = implode( ',', $integrations );
1031 }
1032 } else {
1033 $settings['active_integrations'] = $slug;
1034 $integrations_count = 1;
1035 }
1036
1037 if ( isset( $integrations_count ) ) {
1038 $settings['active_integrations_count'] = $integrations_count;
1039 }
1040
1041 $this->update_meta( self::KEY_INTEGRATIONS_SETTINGS, $settings );
1042 }
1043 }
1044