Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/wordpress-popup/inc/class-hustle-admin-page-abstract.php
+++ b/wordpress-popup/inc/class-hustle-admin-page-abstract.php
@@ -208,14 +208,6 @@
true
);
- wp_enqueue_script(
- 'shared-tutorials',
- Opt_In::$plugin_url . 'assets/js/shared-tutorials.min.js',
- '',
- HUSTLE_SUI_VERSION,
- true
- );
-
/**
* Filters the variable to be localized into the js side of Hustle's admin pages.
*
@@ -267,9 +259,6 @@
'social_sharing' => Hustle_Data::SOCIAL_SHARING_LISTING_PAGE,
),
'messages' => array(
- /* translators: Plugin name */
- 'hustleTutorials' => esc_html( sprintf( __( '%s Tutorials', 'hustle' ), Opt_In_Utils::get_plugin_name() ) ),
- 'tutorialsRemoved' => $tutorials_removed,
'something_went_wrong' => esc_html__( 'Something went wrong. Please try again', 'hustle' ), // everywhere.
'something_went_wrong_reload' => '<label class="wpmudev-label--notice"><span>' . esc_html__( 'Something went wrong. Please reload this page and try again.', 'hustle' ) . '</span></label>', // everywhere.
/* translators: "Aweber" between "strong" tags */
--- a/wordpress-popup/inc/class-hustle-module-fields.php
+++ b/wordpress-popup/inc/class-hustle-module-fields.php
@@ -0,0 +1,21 @@
+<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * Hustle_Module_Fields class file.
+ *
+ * @package Hustle
+ * @since 7.8.11
+ */
+class Hustle_Module_Fields {
+ const FIELDS = array(
+ 'emails' => array(
+ 'recipient' => array(
+ 'type' => 'email',
+ 'required_if' => 'automated_email',
+ ),
+ 'email_subject' => array(
+ 'type' => 'string',
+ 'required_if' => 'automated_email',
+ ),
+ ),
+ );
+}
--- a/wordpress-popup/inc/class-hustle-module-page-abstract.php
+++ b/wordpress-popup/inc/class-hustle-module-page-abstract.php
@@ -114,6 +114,16 @@
}
/**
+ * Perform actions during the 'load-{page}' hook.
+ *
+ * @since 7.8.11
+ */
+ public function current_page_loaded() {
+ Hustle_Background_Conversion_Log::get_instance()->process_task();
+ parent::current_page_loaded();
+ }
+
+ /**
* Set up the page's own properties
* Like the current module type, page title, path to the listing page and wizard page template.
*
--- a/wordpress-popup/inc/class-hustle-module-validator.php
+++ b/wordpress-popup/inc/class-hustle-module-validator.php
@@ -0,0 +1,79 @@
+<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * Hustle_Module_Fields_Validator class file.
+ *
+ * @package Hustle
+ * @since 7.8.11
+ */
+class Hustle_Module_Fields_Validator {
+
+ /**
+ * Validates the module data before saving.
+ *
+ * @param array $fields The fields to validate, structured as [field_name] => rules.
+ * @param array $data The module data to validate.
+ * @return array An array containing 'is_valid' (boolean) and 'errors' (array) keys.
+ */
+ public function validate( $fields, $data ) {
+ $errors = array();
+
+ foreach ( $fields as $field_name => $rules ) {
+ $value = isset( $data[ $field_name ] ) ? $data[ $field_name ] : null;
+ // Check for required fields.
+ if ( isset( $rules['required'] ) && $rules['required'] && empty( $value ) ) {
+ $errors[ $field_name ][] = sprintf(
+ esc_html__( 'The field is required.', 'hustle' ),
+ );
+ continue;
+ }
+
+ // Check for required_if condition.
+ if (
+ isset( $rules['required_if'] ) &&
+ isset( $data[ $rules['required_if'] ] )
+ ) {
+ if ( 1 === (int) $data[ $rules['required_if'] ] ) {
+ if ( empty( $value ) ) {
+ $errors[ $field_name ][] = sprintf(
+ esc_html__( 'The field is required.', 'hustle' ),
+ );
+ continue;
+ }
+ } else {
+ // If the condition is not met, skip further validation for this field.
+ continue;
+ }
+ }
+
+ // If the field is not required and empty, skip further validation.
+ if ( empty( $value ) ) {
+ continue;
+ }
+
+ // Validate field type.
+ switch ( $rules['type'] ) {
+ case 'email':
+ $list = array_map( 'trim', explode( ',', $value ) );
+ foreach ( $list as $email ) {
+ if ( '{email}' === $email ) {
+ // If the placeholder is present, skip validation for this email.
+ continue;
+ }
+
+ if ( ! filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
+ $errors[ $field_name ][] = esc_html__( 'Please enter a valid email address.', 'hustle' );
+ break;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ return array(
+ 'is_valid' => empty( $errors ),
+ 'errors' => $errors,
+ );
+ }
+}
--- a/wordpress-popup/inc/class-hustle-notifications.php
+++ b/wordpress-popup/inc/class-hustle-notifications.php
@@ -264,21 +264,32 @@
*
* @param string $provider Provider's name.
* @param array $provider_data Provider's data.
+ * @param string $button_text Notice button text.
+ * @param string $button_url Notice button URL.
*/
- private function get_provider_auth_deprecation_notice_html( $provider, $provider_data = array() ) {
+ private function get_provider_auth_deprecation_notice_html( $provider, $provider_data = array(), $button_text = '', $button_url = '' ) {
$current_user = wp_get_current_user();
$username = ! empty( $current_user->user_firstname ) ? $current_user->user_firstname : $current_user->user_login;
- $migrate_url = add_query_arg(
- array(
- 'page' => Hustle_Data::INTEGRATIONS_PAGE,
- 'show_provider_migration' => $provider,
- 'action' => 'show_migration_message',
- ),
- 'admin.php'
- );
$provided_id = isset( $provider_data['id'] ) ? $provider . '_' . $provider_data['id'] : $provider;
+
+ if ( empty( $button_text ) ) {
+ $button_text = __( 'Re-authorize Now', 'hustle' );
+ }
+
+ if ( empty( $button_url ) ) {
+ $button_url = add_query_arg(
+ array(
+ 'page' => Hustle_Data::INTEGRATIONS_PAGE,
+ 'show_provider_migration' => $provider,
+ 'action' => 'show_migration_message',
+ 'integration_id' => isset( $provider_data['id'] ) ? $provider_data['id'] : '0',
+ ),
+ 'admin.php'
+ );
+ }
+
?>
<div
id='<?php echo esc_attr( "hustle_migration_notice__$provided_id" ); ?>'
@@ -290,7 +301,7 @@
<p>
<?php $this->get_provider_migration_content( $provider, $username, $provider_data['name'] ); ?>
</p>
- <p><a href="<?php echo esc_url( $migrate_url ); ?>" class="button-primary"><?php esc_html_e( 'Re-authorize Now', 'hustle' ); ?></a></p>
+ <p><a href="<?php echo esc_url( $button_url ); ?>" class="button-primary"><?php echo esc_html( $button_text ); ?></a></p>
</div>
<?php
}
@@ -315,6 +326,14 @@
/* translators: 1. user's name, */
$msg = sprintf( esc_html__( "Hey %1$s, we have updated our AWeber integration to support the oAuth 2.0. Since you are connected via oAuth 1.0, we recommend you to migrate your %2$s integration to the latest authorization method as we'll cease to support the deprecated oAuth method at some point.", 'hustle' ), $username, $identifier );
break;
+ case 'convertkit':
+ /* translators: 1. integration identifier */
+ $msg = sprintf( esc_html__( 'Hustle’s Kit (Formerly ConvertKit) integration has been upgraded to the latest API (v4) for improved security and reliability. To continue sending leads to Kit, please re-authorize your "%s" integration.', 'hustle' ), $identifier );
+ break;
+ case 'hubspot':
+ /* translators: 1. user's name, 2. provider's name */
+ $msg = sprintf( esc_html__( 'Hey %1$s, we have updated our HubSpot integration to support the latest API (v3).', 'hustle' ), $username );
+ break;
default:
$msg = '';
@@ -348,7 +367,6 @@
'hustle_page_hustle_integrations',
'hustle_page_hustle_entries',
'hustle_page_hustle_settings',
- 'hustle_page_hustle_tutorials',
),
)
);
@@ -681,6 +699,57 @@
$this->get_provider_auth_deprecation_notice_html( 'constantcontact', $provider_data );
}
}
+
+ $hubspot_instance = Hustle_HubSpot::get_instance();
+ if ( $hubspot_instance->migration_required() ) {
+ // Check if there is a connected HubSpot integration.
+ // If there is, show the notice.
+ if ( $hubspot_instance->is_connected() ) {
+ $provider_data = array(
+ 'name' => Hustle_HubSpot::SLUG,
+ 'id' => '0',
+ );
+
+ $url = add_query_arg(
+ array(
+ 'page' => Hustle_Data::INTEGRATIONS_PAGE,
+ 'show_provider_migration' => 'hubspot',
+ 'action' => 'migrate_provider_data',
+ 'nonce' => wp_create_nonce( 'hustle_provider_migrate' ),
+ ),
+ 'admin.php'
+ );
+
+ $this->get_provider_auth_deprecation_notice_html(
+ 'hubspot',
+ $provider_data,
+ __( 'Migrate Data', 'hustle' ),
+ $url
+ );
+ }
+ }
+
+ $ck_instance = Hustle_ConvertKit::get_instance();
+ // Check if there is a connected ConvertKit integration.
+ // If there is, show the notice.
+ if ( $ck_instance->is_connected() ) {
+ $connections = $ck_instance->get_settings_values();
+ if ( empty( $connections ) || ! is_array( $connections ) ) {
+ return;
+ }
+
+ foreach ( $connections as $connection_id => $connection ) {
+ if ( empty( $connection['version'] ) || version_compare( $connection['version'], '2.0', '<' ) ) {
+ // Show only one notification.
+ // This will work globally for all the instances.
+ $provider_data = array(
+ 'name' => $connection['name'],
+ 'id' => $connection_id,
+ );
+ $this->get_provider_auth_deprecation_notice_html( 'convertkit', $provider_data );
+ }
+ }
+ }
}
/**
--- a/wordpress-popup/inc/class-hustle-tutorials-page.php
+++ b/wordpress-popup/inc/class-hustle-tutorials-page.php
@@ -1,57 +0,0 @@
-<?php
-/**
- * File for Hustle_Admin_Page_Abstract class.
- *
- * @package Hustle
- * @since 4.4.6
- */
-
-/**
- * Class Hustle_Tutorials_Page
- *
- * @since 4.4.6
- */
-class Hustle_Tutorials_Page extends Hustle_Admin_Page_Abstract {
-
- /**
- * Initiates the page's properties
- *
- * @since 4.4.6
- */
- public function init() {
-
- $this->page = 'hustle_tutorials';
-
- $this->page_title = __( 'Tutorials', 'hustle' );
-
- $this->page_menu_title = __( 'Tutorials', 'hustle' );
-
- $this->page_capability = 'hustle_menu';
-
- $this->page_template_path = 'admin/tutorials';
-
- add_action( 'wp_ajax_hustle_hide_tutorials', array( $this, 'hide_tutorials' ) );
- }
-
- /**
- * Hide tutorials.
- */
- public function hide_tutorials() {
- check_ajax_referer( 'hustle_dismiss_notification' );
-
- update_option( 'hustle-hide_tutorials', true );
-
- wp_send_json_success();
- }
-
-
- /**
- * Get the arguments used when rendering the main page.
- *
- * @since 4.4.6
- * @return array
- */
- public function get_page_template_args() {
- return array();
- }
-}
--- a/wordpress-popup/inc/front/class-hustle-decorator-non-sshare.php
+++ b/wordpress-popup/inc/front/class-hustle-decorator-non-sshare.php
@@ -430,6 +430,8 @@
} else {
// If outside of @media block, add to output.
$output .= $prepared;
+ // reset $prepared for next styles.
+ $prepared = '';
}
} else {
$media_names[ $prev_media_names_key ]['styles'] = $prepared;
@@ -438,6 +440,8 @@
// If no @media, add styles to $output outside @media.
$output .= $prepared;
+ // reset $prepared for next styles.
+ $prepared = '';
}
// Increase index.
--- a/wordpress-popup/inc/front/class-hustle-decorator-sshare.php
+++ b/wordpress-popup/inc/front/class-hustle-decorator-sshare.php
@@ -57,14 +57,14 @@
if ( 'center' !== $display['float_desktop_position'] ) {
$styles .= sprintf(
- $prefix . '.hustle-float.hustle-displaying-in-large[data-desktop="true"][data-desktop-positionx="%1$s"] { %1$s: %2$spx }',
+ $prefix . '.hustle-float.hustle-displaying-in-large[data-desktop="true"][data-desktop-positionx="%1$s"] { %1$s: %2$spx !important }',
esc_html( $display['float_desktop_position'] ),
esc_attr( $desktop_x_offset )
);
}
$styles .= sprintf(
- $prefix . '.hustle-float.hustle-displaying-in-large[data-desktop="true"][data-desktop-positiony="%1$s"] { %1$s: %2$spx }',
+ $prefix . '.hustle-float.hustle-displaying-in-large[data-desktop="true"][data-desktop-positiony="%1$s"] { %1$s: %2$spx !important }',
esc_html( $display['float_desktop_position_y'] ),
esc_attr( $desktop_y_offset )
);
@@ -83,14 +83,14 @@
if ( 'center' !== $display['float_mobile_position'] ) {
$styles .= sprintf(
- $prefix . '.hustle-float.hustle-displaying-in-small[data-mobiles="true"][data-mobiles-positionx="%1$s"] { %1$s: %2$spx }',
+ $prefix . '.hustle-float.hustle-displaying-in-small[data-mobiles="true"][data-mobiles-positionx="%1$s"] { %1$s: %2$spx !important }',
esc_html( $display['float_mobile_position'] ),
esc_attr( $mobile_x_offset )
);
}
$styles .= sprintf(
- $prefix . '.hustle-float.hustle-displaying-in-small[data-mobiles="true"][data-mobiles-positiony="%1$s"] { %1$s: %2$spx }',
+ $prefix . '.hustle-float.hustle-displaying-in-small[data-mobiles="true"][data-mobiles-positiony="%1$s"] { %1$s: %2$spx !important }',
esc_html( $display['float_mobile_position_y'] ),
esc_attr( $mobile_y_offset )
);
--- a/wordpress-popup/inc/front/hustle-module-front-ajax.php
+++ b/wordpress-popup/inc/front/hustle-module-front-ajax.php
@@ -518,6 +518,30 @@
$response['behavior']['file'] = $emails_settings['auto_download_file'];
$response['behavior']['file_name'] = $file_name;
}
+
+ if (
+ ! empty( $emails_settings['notification_email'] ) &&
+ ! empty( $emails_settings['notification_email_recipient'] )
+ ) {
+ $notification_recipient = $this->replace_placeholders(
+ $module_id,
+ sanitize_email( $emails_settings['notification_email_recipient'] ),
+ $form_data
+ );
+ $notification_subject = $this->replace_placeholders(
+ $module_id,
+ sanitize_text_field( $emails_settings['notification_email_subject'] ),
+ $form_data
+ );
+ $notification_body = $this->replace_placeholders(
+ $module_id,
+ wp_kses_post( $emails_settings['notification_email_body'] ),
+ $form_data
+ );
+
+ // Send the notification email right away.
+ Hustle_Mail::send_email( $notification_recipient, $notification_subject, $notification_body );
+ }
}
if ( ! empty( $submit_errors ) ) {
@@ -747,7 +771,6 @@
return false;
}
- $tracking = Hustle_Tracking_Model::get_instance();
if ( 'social_sharing' === $module->module_type ) {
$action = 'conversion';
} elseif ( $cta ) {
@@ -755,11 +778,44 @@
} else {
$action = 'optin_conversion';
}
- $tracking->save_tracking( $module->id, $action, $module->module_type, $page_id, $module_sub_type );
+ $this->temp_log_conversion( $module, $action, $page_id, $module_sub_type );
return true;
}
+
+ /**
+ * Temporary log for conversions.
+ *
+ * @param Hustle_Module_Model $module Module.
+ * @param string $action Action.
+ * @param int $post_id Post ID.
+ * @param string|null $module_sub_type Module subtype.
+ */
+ private function temp_log_conversion( $module, $action, $post_id, $module_sub_type ) {
+ $settings = Hustle_Settings_Admin::get_privacy_settings();
+ $ip_tracking = ! isset( $settings['ip_tracking'] ) || 'on' === $settings['ip_tracking'];
+
+ if ( $ip_tracking ) {
+ $user_ip = Opt_In_Geo::get_user_ip();
+ } else {
+ $user_ip = '';
+ }
+
+ $logs = get_option( 'hustle_conversion_logs', array() );
+ $logs[] = array(
+ 'time' => time(),
+ 'module_id' => $module->id,
+ 'module_type' => $module->module_type,
+ 'post_id' => $post_id,
+ 'action' => $action,
+ 'ip' => $user_ip,
+ 'module_sub_type' => $module_sub_type,
+ );
+
+ update_option( 'hustle_conversion_logs', $logs );
+ }
+
/**
* Get an array with the connected integrations to show in entries.
*
@@ -1129,23 +1185,14 @@
}
if ( $module->id ) {
-
- $module_type = $data['module_type'];
$page_id = $data['page_id'];
$module_sub_type = isset( $data['module_sub_type'] ) ? $data['module_sub_type'] : null;
- $tracking = Hustle_Tracking_Model::get_instance();
- $res = $tracking->save_tracking( $module_id, 'view', $module_type, $page_id, $module_sub_type );
-
- } else {
- $res = false;
- }
-
- if ( ! $res ) {
- wp_send_json_error( __( 'Error saving stats', 'hustle' ) );
- } else {
+ $this->temp_log_conversion( $module, 'view', $page_id, $module_sub_type );
wp_send_json_success( __( 'Stats Successfully saved', 'hustle' ) );
}
+
+ wp_send_json_error( __( 'Error saving stats', 'hustle' ) );
}
/**
--- a/wordpress-popup/inc/front/hustle-module-front.php
+++ b/wordpress-popup/inc/front/hustle-module-front.php
@@ -98,6 +98,8 @@
}
// phpcs:enable
+ Hustle_Module_Inline_Style_Queue::init();
+
if ( ! $is_preview ) {
$this->prepare_for_front();
} else {
@@ -121,7 +123,7 @@
// Enqueue it in the footer to overrider all the css that comes with the popup.
add_action(
- 'wp_footer',
+ 'hustle_after_enqueue_inline_styles',
array( $this, 'register_styles' )
);
@@ -299,8 +301,8 @@
'is_admin' => is_admin(),
'real_page_id' => Opt_In_Utils::get_real_page_id(),
'thereferrer' => Opt_In_Utils::get_referrer(),
- 'actual_url' => Opt_In_Utils::get_current_actual_url(),
- 'full_actual_url' => Opt_In_Utils::get_current_actual_url( true ),
+ 'actual_url' => esc_url_raw( Opt_In_Utils::get_current_actual_url() ),
+ 'full_actual_url' => esc_url_raw( Opt_In_Utils::get_current_actual_url( true ) ),
'native_share_enpoints' => Hustle_SShare_Model::get_sharing_endpoints( false ),
'ajaxurl' => admin_url( 'admin-ajax.php', is_ssl() ? 'https' : 'http' ),
'page_id' => get_queried_object_id(), // Used in many places to decide whether to show the module and cookies.
@@ -723,10 +725,14 @@
if (
! empty( $mailchimp_settings ) &&
! is_null( $mailchimp_settings['group'] ) &&
- '-1' !== $mailchimp_settings['group'] &&
- 'dropdown' === $mailchimp_settings['group_type']
+ '-1' !== $mailchimp_settings['group']
) {
- $select2_found = true;
+ if (
+ isset( $mailchimp_settings['group_type'] ) &&
+ 'dropdown' === $mailchimp_settings['group_type']
+ ) {
+ $select2_found = true;
+ }
}
}
}
--- a/wordpress-popup/inc/front/hustle-module-inline-style-queue.php
+++ b/wordpress-popup/inc/front/hustle-module-inline-style-queue.php
@@ -0,0 +1,113 @@
+<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * Hustle_Module_Inline_Style_Queue
+ *
+ * @package Hustle
+ */
+class Hustle_Module_Inline_Style_Queue {
+
+ /**
+ * A queue of inline styles to be printed.
+ *
+ * @var array
+ */
+ private static $inline_styles = array();
+
+ /**
+ * Initialize the class
+ */
+ public static function init() {
+ add_action( 'wp_enqueue_scripts', array( __CLASS__, 'late_enqueue_bundle_inline_styles' ), 100 );
+ }
+
+ /**
+ * Enqueue inline style
+ *
+ * @param string $style_id Style ID.
+ * @param string $style Style.
+ */
+ public static function enqueue_inline_style( $style_id, $style ) {
+ $style = str_replace( array( "r", "n" ), '', $style );
+
+ if ( ! did_action( 'wp_enqueue_scripts' ) ) {
+ // Not yet enqueued, add to the bundle.
+ self::$inline_styles[ $style_id ] = $style;
+ } else {
+ self::$inline_styles[ $style_id ] = true;
+ // Already enqueued, add it immediately.
+ self::late_enqueue_single_style( $style_id, $style );
+ }
+ }
+
+ /**
+ * Late enqueue single inline style
+ *
+ * @param string $style_id Style ID.
+ * @param string $style Style.
+ */
+ public static function late_enqueue_single_style( $style_id, $style ) {
+ wp_register_style(
+ $style_id,
+ false,
+ array(),
+ '1.0.0'
+ );
+ wp_enqueue_style( $style_id );
+
+ wp_add_inline_style(
+ $style_id,
+ $style
+ );
+ }
+
+ /**
+ * Has inline style
+ *
+ * @param string $style_id Style ID.
+ * @return bool
+ */
+ public static function has_inline_style( $style_id ) {
+ return isset( self::$inline_styles[ $style_id ] );
+ }
+
+ /**
+ * Late enqueue bundle inline styles
+ *
+ * @return void
+ */
+ public static function late_enqueue_bundle_inline_styles() {
+ $all_styles = '';
+
+ foreach ( self::$inline_styles as $style ) {
+ if ( ! is_string( $style ) ) {
+ continue;
+ }
+
+ $all_styles .= $style;
+ }
+
+ if ( $all_styles ) {
+
+ if ( ! wp_style_is( 'hustle_inline_styles_front', 'enqueued' ) ) {
+ // Enqueue the bundle if not already enqueued.
+ wp_register_style(
+ 'hustle_inline_styles_front',
+ false,
+ array(),
+ '1.0.0'
+ );
+ wp_enqueue_style( 'hustle_inline_styles_front' );
+ }
+
+ wp_add_inline_style(
+ 'hustle_inline_styles_front',
+ $all_styles
+ );
+ }
+
+ /**
+ * Fires after enqueueing inline styles.
+ */
+ do_action( 'hustle_after_enqueue_inline_styles' );
+ }
+}
--- a/wordpress-popup/inc/front/hustle-renderer-abstract.php
+++ b/wordpress-popup/inc/front/hustle-renderer-abstract.php
@@ -107,12 +107,7 @@
$display_module = $this->module->active && $this->module->get_visibility()->is_allowed_to_display( $module->module_type, $sub_type );
}
if ( $is_preview || $display_module ) {
- if ( did_action( 'wp_head' ) ) {
- add_action( 'wp_footer', array( $this, 'print_styles' ), 9999 );
- } else {
- add_action( 'wp_head', array( $this, 'print_styles' ) );
- }
-
+ $this->enqueue_styles();
// Render form.
return $this->get_module( $sub_type, $custom_classes );
}
@@ -182,6 +177,28 @@
}
/**
+ * Enqueue styles
+ */
+ public function enqueue_styles() {
+ $disable_styles = apply_filters( 'hustle_disable_front_styles', false, $this->module, $this );
+
+ if ( ! $disable_styles ) {
+ $render_id = self::$render_ids[ $this->module->module_id ];
+
+ $style_id = 'hustle-module-' . esc_attr( $this->module->module_id ) . '-' . esc_attr( $render_id ) . '-styles';
+
+ if ( Hustle_Module_Inline_Style_Queue::has_inline_style( $style_id ) ) {
+ // Already enqueued.
+ return;
+ }
+
+ $style = $this->module->get_decorated()->get_module_styles( $this->module->module_type ); // it's already escaped.
+
+ Hustle_Module_Inline_Style_Queue::enqueue_inline_style( $style_id, $style );
+ }
+ }
+
+ /**
* Print styles
*/
public function print_styles() {
--- a/wordpress-popup/inc/helpers/class-hustle-layout-helper.php
+++ b/wordpress-popup/inc/helpers/class-hustle-layout-helper.php
@@ -116,10 +116,11 @@
* @param boolean $return_value Whether to echo or return the markup.
* @return string
*/
- public function get_html_for_options( $options, $return_value = false ) {
- $html = '';
+ public static function get_html_for_options( $options, $return_value = false ) {
+ $instance = new self();
+ $html = '';
foreach ( $options as $key => $option ) {
- $html .= $this->render( 'admin/commons/options', $option, $return_value );
+ $html .= $instance->render( 'admin/commons/options', $option, $return_value );
}
return $html;
}
--- a/wordpress-popup/inc/hustle-background-conversion-log.php
+++ b/wordpress-popup/inc/hustle-background-conversion-log.php
@@ -0,0 +1,129 @@
+<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * Hustle Background Conversion Log
+ *
+ * @package Hustle
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
+
+/**
+ * Class Hustle_Background_Conversion_Log
+ *
+ * Handles background processing for conversion log tasks.
+ */
+class Hustle_Background_Conversion_Log {
+
+ /**
+ * Instance of this class
+ *
+ * @var Hustle_Background_Conversion_Log
+ */
+ private static $instance = null;
+
+ /**
+ * Cron hook name
+ *
+ * @var string
+ */
+ private $cron_hook = 'hustle_conversion_log_cron';
+
+ /**
+ * Cron interval name
+ *
+ * @var string
+ */
+ private $cron_interval = 'hustle_every_minute';
+
+ /**
+ * Get the singleton instance
+ *
+ * @return Hustle_Background_Conversion_Log
+ */
+ public static function get_instance() {
+ if ( is_null( self::$instance ) ) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Constructor
+ */
+ private function __construct() {
+ add_action( $this->cron_hook, array( $this, 'process_task' ) );
+ add_filter( 'cron_schedules', array( $this, 'add_cron_interval' ) );
+ }
+
+ /**
+ * Initialize the background task
+ */
+ public function init() {
+ if ( ! wp_next_scheduled( $this->cron_hook ) ) {
+ wp_schedule_event( time(), $this->cron_interval, $this->cron_hook );
+ }
+ }
+
+ /**
+ * Add custom cron interval (every 15 minutes)
+ *
+ * @param array $schedules Existing schedules.
+ * @return array Modified schedules.
+ */
+ public function add_cron_interval( $schedules ) {
+ $schedules[ $this->cron_interval ] = array(
+ 'interval' => 900, // 900 seconds = 15 minutes
+ 'display' => esc_html__( 'Every Fifteen Minutes', 'hustle' ),
+ );
+ return $schedules;
+ }
+
+ /**
+ * Process the background task
+ */
+ public function process_task() {
+ self::save_conversion_logs();
+ }
+
+ /**
+ * Save temporary conversion logs.
+ *
+ * @since 7.8.11
+ */
+ public static function save_conversion_logs() {
+ $temp_conversions = get_option( 'hustle_conversion_logs', array() );
+ if ( ! empty( $temp_conversions ) ) {
+ foreach ( $temp_conversions as $conversion ) {
+
+ $date = date_i18n( 'Y-m-d H:i:s', $conversion['time'] );
+ Hustle_Tracking_Model::get_instance()->save_tracking(
+ $conversion['module_id'],
+ $conversion['action'],
+ $conversion['module_type'],
+ $conversion['post_id'],
+ $conversion['module_sub_type'],
+ $date,
+ $conversion['ip']
+ );
+ }
+ delete_option( 'hustle_conversion_logs' );
+ }
+ }
+
+ /**
+ * Stop the scheduled task
+ */
+ public function stop() {
+ $timestamp = wp_next_scheduled( $this->cron_hook );
+ if ( $timestamp ) {
+ wp_unschedule_event( $timestamp, $this->cron_hook );
+ }
+ }
+
+ /**
+ * Prevent cloning of the instance
+ */
+ private function __clone() {}
+}
--- a/wordpress-popup/inc/hustle-deletion.php
+++ b/wordpress-popup/inc/hustle-deletion.php
@@ -51,6 +51,7 @@
delete_option( 'hustle_custom_palettes' );
delete_option( 'hustle_notice_stop_support_m2' );
delete_option( 'hustle-hide_tutorials' );
+ delete_option( 'hustle_conversion_logs' );
}
/**
--- a/wordpress-popup/inc/hustle-init.php
+++ b/wordpress-popup/inc/hustle-init.php
@@ -51,15 +51,11 @@
new Hustle_Entries_Admin();
new Hustle_Settings_Page();
-
- $hide_docs = apply_filters( 'wpmudev_branding_hide_doc_link', false );
- if ( ! $hide_docs ) {
- new Hustle_Tutorials_Page();
- }
}
if ( is_admin() || wp_doing_cron() ) {
new Hustle_General_Data_Protection();
+ Hustle_Background_Conversion_Log::get_instance()->init();
}
if ( Opt_In_Utils::is_free() ) {
--- a/wordpress-popup/inc/hustle-module-model.php
+++ b/wordpress-popup/inc/hustle-module-model.php
@@ -567,6 +567,8 @@
if ( ! in_array( $key, array( 'main_content', 'emailmessage', 'email_message', 'success_message' ), true ) ) {
$value = wp_kses_post( $value );
}
+ } elseif ( 'custom_css' === $key ) {
+ $value = sanitize_textarea_field( $value );
} elseif ( ! is_int( $value ) ) {
$value = sanitize_text_field( $value );
}
@@ -602,10 +604,38 @@
return $errors;
}
+ $validator = $this->create_validator();
+
+ $field_errors = array();
+ foreach ( Hustle_Module_Fields::FIELDS as $section => $fields ) {
+ if ( ! isset( $data[ $section ] ) ) {
+ continue;
+ }
+
+ $result = $validator->validate( $fields, $data[ $section ] );
+ if ( ! $result['is_valid'] ) {
+ $field_errors[ $section ] = $result['errors'];
+ }
+ }
+
+ if ( ! empty( $field_errors ) ) {
+ $errors['error']['field_errors'] = $field_errors;
+ return $errors;
+ }
+
return true;
}
/**
+ * Create the validator instance.
+ *
+ * @return Hustle_Module_Fields_Validator
+ */
+ protected function create_validator() {
+ return new Hustle_Module_Fields_Validator();
+ }
+
+ /**
* Get newest value.
*
* @param string $option_name Option name.
--- a/wordpress-popup/inc/hustle-providers-admin.php
+++ b/wordpress-popup/inc/hustle-providers-admin.php
@@ -136,6 +136,43 @@
$response['integration_id'] = $multi_id;
}
+ if ( 'migrate_provider_data' === $action && ! empty( $provider ) ) {
+ $nonce = filter_input( INPUT_GET, 'nonce' );
+
+ if ( $nonce && wp_verify_nonce( $nonce, 'hustle_provider_migrate' ) ) {
+
+ $provider_instance = Hustle_Provider_Utils::get_provider_by_slug( $provider );
+
+ if (
+ $provider_instance instanceof Hustle_Provider_Abstract &&
+ method_exists( $provider_instance, 'process_data_migration' )
+ ) {
+ $result = $provider_instance->process_data_migration();
+
+ if ( is_wp_error( $result ) ) {
+ $response['migration_notificaiton'] = array(
+ 'action' => 'notification',
+ 'status' => 'error',
+ 'message' => $result->get_error_message(),
+ );
+ } else {
+ $response['migration_notificaiton'] = array(
+ 'action' => 'notification',
+ 'status' => 'success',
+ 'slug' => $provider,
+ 'message' => /* translators: integration type */ sprintf( esc_html__( '%s integration successfully migrated to the new API version.', 'hustle' ), '<strong>' . esc_html( ucfirst( $provider ) ) . '</strong>' ),
+ );
+ }
+ }
+ } else {
+ return array(
+ 'action' => 'notification',
+ 'status' => 'error',
+ 'message' => esc_html__( "You're not allowed to do this request.", 'hustle' ),
+ );
+ }
+ }
+
if ( 'external-redirect' === $action && true === $migration ) {
$nonce = filter_input( INPUT_GET, 'nonce' );
@@ -157,6 +194,9 @@
if ( 'infusionsoft' === $slug ) {
$response['migration_notificaiton']['message'] = /* translators: integration type */ sprintf( esc_html__( '%s integration successfully migrated to use the REST API.', 'hustle' ), '<strong>' . esc_html__( 'Keap', 'hustle' ) . '</strong>' );
}
+ if ( 'convertkit' === $slug ) {
+ $response['migration_notificaiton']['message'] = /* translators: integration type */ sprintf( esc_html__( '%s integration successfully migrated to use the latest API version.', 'hustle' ), '<strong>' . esc_html__( 'ConvertKit', 'hustle' ) . '</strong>' );
+ }
} else {
$response = array(
--- a/wordpress-popup/inc/hustle-sshare-model.php
+++ b/wordpress-popup/inc/hustle-sshare-model.php
@@ -286,7 +286,7 @@
public static function get_social_platform_names() {
$social_platform_names = array(
'facebook' => esc_html__( 'Facebook', 'hustle' ),
- 'twitter' => esc_html__( 'Twitter', 'hustle' ),
+ 'twitter' => esc_html__( 'X', 'hustle' ),
'pinterest' => esc_html__( 'Pinterest', 'hustle' ),
'reddit' => esc_html__( 'Reddit', 'hustle' ),
'linkedin' => esc_html__( 'LinkedIn', 'hustle' ),
--- a/wordpress-popup/inc/hustle-tracking-model.php
+++ b/wordpress-popup/inc/hustle-tracking-model.php
@@ -77,16 +77,19 @@
*/
public function save_tracking( $module_id, $action, $module_type, $page_id, $module_sub_type = null, $date_created = null, $ip = null ) {
global $wpdb;
+
/**
* IP Tracking
*/
- $ip_query = ' AND `ip` IS NULL';
$settings = Hustle_Settings_Admin::get_privacy_settings();
$ip_tracking = ! isset( $settings['ip_tracking'] ) || 'on' === $settings['ip_tracking'];
- if ( $ip_tracking ) {
- $ip = $ip ? $ip : Opt_In_Geo::get_user_ip();
+
+ if ( $ip && $ip_tracking ) {
$ip_query = ' AND `ip` = %s';
+ } else {
+ $ip_query = ' AND `ip` IS NULL';
}
+
if ( ! in_array( $action, array( 'conversion', 'cta_conversion', 'cta_1_conversion', 'cta_2_conversion', 'optin_conversion' ), true ) ) {
$action = 'view';
}
--- a/wordpress-popup/inc/metas/class-hustle-meta-base-emails.php
+++ b/wordpress-popup/inc/metas/class-hustle-meta-base-emails.php
@@ -41,27 +41,31 @@
*/
public function get_defaults() {
return array(
- 'form_elements' => $this->get_default_form_fields(),
- 'after_successful_submission' => 'show_success',
- 'success_message' => '',
- 'auto_close_success_message' => '0',
- 'auto_close_time' => 5,
- 'auto_close_unit' => 'seconds',
- 'redirect_url' => '',
- 'automated_email' => '0',
- 'email_time' => 'instant',
- 'recipient' => '{email}',
- 'day' => '',
- 'time' => '',
- 'auto_email_time' => '5',
- 'schedule_auto_email_time' => '5',
- 'auto_email_unit' => 'seconds',
- 'schedule_auto_email_unit' => 'seconds',
- 'email_subject' => '',
- 'email_body' => '',
- 'automated_file' => '0',
- 'auto_download_file' => '',
- 'redirect_tab' => '',
+ 'form_elements' => $this->get_default_form_fields(),
+ 'after_successful_submission' => 'show_success',
+ 'success_message' => '',
+ 'auto_close_success_message' => '0',
+ 'auto_close_time' => 5,
+ 'auto_close_unit' => 'seconds',
+ 'redirect_url' => '',
+ 'automated_email' => '0',
+ 'email_time' => 'instant',
+ 'recipient' => '{email}',
+ 'notification_email_recipient' => get_option( 'admin_email' ),
+ 'notification_email' => '0',
+ 'notification_email_subject' => '',
+ 'notification_email_body' => '',
+ 'day' => '',
+ 'time' => '',
+ 'auto_email_time' => '5',
+ 'schedule_auto_email_time' => '5',
+ 'auto_email_unit' => 'seconds',
+ 'schedule_auto_email_unit' => 'seconds',
+ 'email_subject' => '',
+ 'email_body' => '',
+ 'automated_file' => '0',
+ 'auto_download_file' => '',
+ 'redirect_tab' => '',
);
}
--- a/wordpress-popup/inc/opt-in-utils.php
+++ b/wordpress-popup/inc/opt-in-utils.php
@@ -134,8 +134,8 @@
return '';
}
- $host = filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ), FILTER_SANITIZE_SPECIAL_CHARS );
- $uri = filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_SPECIAL_CHARS );
+ $host = filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ), FILTER_SANITIZE_URL );
+ $uri = filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL );
$url = $host . $uri;
@@ -143,7 +143,7 @@
return $url;
}
- return esc_url( 'http' . ( isset( $_SERVER['HTTPS'] ) ? 's' : '' ) . '://' . $url );
+ return 'http' . ( isset( $_SERVER['HTTPS'] ) ? 's' : '' ) . '://' . $url;
}
/**
@@ -473,11 +473,11 @@
array_walk_recursive(
$value,
function ( &$val ) {
- $val = sanitize_text_field( $val );
+ $val = sanitize_textarea_field( $val );
}
);
} else {
- $value = sanitize_text_field( $value );
+ $value = sanitize_textarea_field( $value );
}
return $value;
@@ -1302,63 +1302,38 @@
*/
private static function is_hub_cache() {
$key_option = 'hustle_hub_cache_enabled';
- $key_time = 'hustle_hub_cache_timeout';
- $cache = get_site_option( $key_option, null );
- if ( ! is_null( $cache ) ) {
- $timeout = get_site_option( $key_time );
- if ( time() < $timeout || ! is_admin() ) {
- $return = $cache;
- }
- }
- if ( ! isset( $return ) ) {
- $return = self::get_hub_cache_status();
- update_site_option( $key_option, (int) $return );
- update_site_option( $key_time, time() + DAY_IN_SECONDS );
+ $cache = get_site_transient( $key_option );
+
+ if ( false === $cache ) {
+ $cache = self::is_hub_cache_enabled();
+ set_site_transient( $key_option, (int) $cache, DAY_IN_SECONDS );
}
- return (bool) $return;
+ return (bool) $cache;
}
/**
- * Check if Static Server Cache is enabled on HUB or not
+ * Check if Static Server Cache is enabled on HUB
*
* @return boolean
*/
- private static function get_hub_cache_status() {
- if ( ! class_exists( 'WPMUDEV_Dashboard' ) ) {
- return false;
- }
- try {
- $api = WPMUDEV_Dashboard::$api;
- $api_key = $api->get_key();
- $site_id = $api->get_site_id();
- $base = defined( 'WPMUDEV_CUSTOM_API_SERVER' ) && WPMUDEV_CUSTOM_API_SERVER
- ? WPMUDEV_CUSTOM_API_SERVER
- : 'https://wpmudev.com/';
- $url = "{$base}api/hub/v1/sites/$site_id/modules/hosting";
-
- $options = array(
- 'headers' => array(
- 'Authorization' => 'Basic ' . $api_key,
- 'apikey' => $api_key,
- ),
- );
- $data = array(
- 'domain' => network_site_url(),
- );
-
- $response = $api->call( $url, $data, 'GET', $options );
+ private static function is_hub_cache_enabled() {
+ return (bool) self::get_hosting_feature( 'static_cache' );
+ }
- if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
- $data = json_decode( wp_remote_retrieve_body( $response ), true );
- if ( ! empty( $data['static_cache']['is_active'] ) ) {
- return true;
- }
- }
- return false;
- } catch ( Exception $e ) {
- return false;
+ /**
+ * Get list of hosting features and their status
+ *
+ * @param string $name The name of the hosting feature to check.
+ * @return mixed The status of the hosting feature, or an empty string if not found.
+ */
+ private static function get_hosting_feature( $name ) {
+ if ( function_exists( 'wpmudev_hosting_features' ) ) {
+ $states = wpmudev_hosting_features();
+ return isset( $states[ $name ] ) ? $states[ $name ] : '';
}
+
+ return '';
}
/**
--- a/wordpress-popup/inc/provider/class-hustle-provider-admin-ajax.php
+++ b/wordpress-popup/inc/provider/class-hustle-provider-admin-ajax.php
@@ -77,6 +77,7 @@
add_action( 'wp_ajax_hustle_provider_migrate_aweber', array( $this, 'migrate_aweber' ) );
add_action( 'wp_ajax_hustle_provider_migrate_constantcontact', array( $this, 'migrate_constantcontact' ) );
add_action( 'wp_ajax_hustle_provider_migrate_infusionsoft', array( $this, 'migrate_infusionsoft' ) );
+ add_action( 'wp_ajax_hustle_provider_migrate_convertkit', array( $this, 'migrate_convertkit' ) );
}
/**
@@ -594,6 +595,48 @@
}
/**
+ * Migrate ConvertKit integration to V4
+ *
+ * @since 7.8.11
+ */
+ public function migrate_convertkit() {
+ $this->validate_ajax();
+
+ if ( isset( $_POST['data'] ) && is_array( $_POST['data'] ) ) {// phpcs:ignore
+ $post_data = filter_input( INPUT_POST, 'data', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
+ } else {
+ $post_data = filter_input( INPUT_POST, 'data' );
+ }
+ $sanitized_post_data = Opt_In_Utils::validate_and_sanitize_fields( $post_data, array( 'slug', 'api_key', 'global_multi_id' ) );
+
+ $convertkit = Hustle_ConvertKit::get_instance()->configure_api_key(
+ $sanitized_post_data
+ );
+
+ if ( ! empty( $convertkit['errors'] ) ) {
+ wp_send_json_error();
+ }
+
+ $integration_id = filter_var( $sanitized_post_data['global_multi_id'], FILTER_SANITIZE_SPECIAL_CHARS );
+
+ wp_send_json_success(
+ array(
+ 'redirect' => add_query_arg(
+ array(
+ 'page' => Hustle_Data::INTEGRATIONS_PAGE,
+ 'migration' => true,
+ 'slug' => 'convertkit',
+ 'nonce' => wp_create_nonce( 'hustle_provider_external_redirect' ),
+ 'action' => 'external-redirect',
+ 'integration_id' => $integration_id,
+ ),
+ admin_url( 'admin.php' )
+ ),
+ )
+ );
+ }
+
+ /**
* Check if is active on module
*
* @since 4.0.1
--- a/wordpress-popup/inc/providers/campaignmonitor/hustle-campaignmonitor-form-hooks.php
+++ b/wordpress-popup/inc/providers/campaignmonitor/hustle-campaignmonitor-form-hooks.php
@@ -68,12 +68,22 @@
if ( isset( $submitted_data['last_name'] ) ) {
$name['last_name'] = $submitted_data['last_name'];
}
- $name = implode( ' ', $name );
+
+ if ( empty( $name ) ) {
+
+ $name = '';
+ if ( isset( $submitted_data['name'] ) ) {
+ $name = $submitted_data['name'];
+ }
+ } else {
+ $name = implode( ' ', $name );
+ }
// Remove unwanted fields.
foreach ( $submitted_data as $key => $sub_d ) {
if ( 'email' === $key ||
+ 'name' === $key ||
'first_name' === $key ||
'last_name' === $key || 'gdpr' === $key ) {
continue;
--- a/wordpress-popup/inc/providers/convertkit/convertkit.php
+++ b/wordpress-popup/inc/providers/convertkit/convertkit.php
@@ -8,7 +8,9 @@
/**
* Direct Load
*/
+require_once __DIR__ . '/hustle-convertkit-api-intefrace.php';
require_once __DIR__ . '/hustle-convertkit.php';
+require_once __DIR__ . '/hustle-convertkit-v2.php';
require_once __DIR__ . '/hustle-convertkit-form-settings.php';
require_once __DIR__ . '/hustle-convertkit-form-hooks.php';
-Hustle_Providers::get_instance()->register( 'Hustle_ConvertKit' );
+Hustle_Providers::get_instance()->register( 'Hustle_ConvertKit_V2' );
--- a/wordpress-popup/inc/providers/convertkit/hustle-convertkit-api-intefrace.php
+++ b/wordpress-popup/inc/providers/convertkit/hustle-convertkit-api-intefrace.php
@@ -0,0 +1,92 @@
+<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * Hustle_ConvertKit_Api_Interface interface
+ *
+ * @package Hustle
+ */
+
+/**
+ * ConvertKit API Interface
+ *
+ * @interface Hustle_ConvertKit_Api_Interface
+ **/
+interface Hustle_ConvertKit_Api_Interface {
+
+ /**
+ * Retrieves ConvertKit forms as array of objects
+ *
+ * @return array|WP_Error
+ */
+ public function get_forms();
+
+ /**
+ * Retrieves ConvertKit subscribers as array of objects
+ *
+ * @return array|WP_Error
+ */
+ public function get_subscribers();
+
+ /**
+ * Retrieves ConvertKit form's custom fields as array of objects
+ *
+ * @return array|WP_Error
+ */
+ public function get_form_custom_fields();
+
+ /**
+ * Add new custom fields to subscription
+ *
+ * @param array $field_data Fields data.
+ * @return array|mixed|object|WP_Error
+ */
+ public function create_custom_fields( $field_data );
+
+ /**
+ * Add new subscriber
+ *
+ * @param string $form_id Form ID.
+ * @param array $data Data.
+ * @return array|mixed|object|WP_Error
+ */
+ public function subscribe( $form_id, $data );
+
+ /**
+ * Update subscriber
+ *
+ * @since 4.0
+ *
+ * @param string $id ID.
+ * @param array $data Data.
+ * @return array|mixed|object|WP_Error
+ */
+ public function update_subscriber( $id, $data );
+
+ /**
+ * Delete subscriber from the list
+ *
+ * @param string $list_id List ID.
+ * @param string $email Email.
+ *
+ * @return bool
+ */
+ public function delete_email( $list_id, $email );
+
+ /**
+ * Verify if an email is already a subscriber.
+ *
+ * @param string $email Email.
+ *
+ * @return object|false Returns data of existing subscriber if exist otherwise false.
+ **/
+ public function is_subscriber( $email );
+
+ /**
+ * Verify if an email is already a subscriber in a form.
+ *
+ * @param string $email Email.
+ * @param integer $form_id Form ID.
+ *
+ * @return boolean|integer True if the subscriber exists, otherwise false.
+ **/
+ public function is_form_subscriber( $email, $form_id );
+}
--- a/wordpress-popup/inc/providers/convertkit/hustle-convertkit-api-v2.php
+++ b/wordpress-popup/inc/providers/convertkit/hustle-convertkit-api-v2.php
@@ -0,0 +1,386 @@
+<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * Hustle_ConvertKit_API_V2 class
+ *
+ * @package Hustle
+ */
+
+/**
+ * ConvertKit API V2 (implements Kit API V4)
+ *
+ * @class Hustle_ConvertKit_API_V2
+ **/
+class Hustle_ConvertKit_API_V2 implements Hustle_ConvertKit_Api_Interface {
+
+ /**
+ * Api key
+ *
+ * @var string
+ */
+ private $api_key;
+
+ /**
+ * Endpoint
+ *
+ * @var string
+ */
+ private $endpoint = 'https://api.kit.com/v4/';
+
+ /**
+ * Constructs class with required data
+ *
+ * Hustle_ConvertKit_API_V2 constructor.
+ *
+ * @param string $api_key Api key.
+ * @param string $api_secret Api secret (not used in v4).
+ */
+ public function __construct( $api_key, $api_secret = '' ) {
+ $this->api_key = $api_key;
+ }
+
+ /**
+ * Sends request to the endpoint url with the provided $action
+ *
+ * @param string $action rest action.
+ * @param string $verb Verb.
+ * @param array $args Args.
+ * @return object|WP_Error
+ */
+ private function request( $action, $verb = 'GET', $args = array() ) {
+ $url = trailingslashit( $this->endpoint ) . $action;
+
+ $_args = array(
+ 'method' => $verb,
+ 'headers' => array(
+ 'X-Kit-Api-Key' => $this->api_key,
+ 'Content-Type' => 'application/json;charset=utf-8',
+ ),
+ );
+
+ if ( 'GET' === $verb ) {
+ if ( ! empty( $args ) ) {
+ $url .= ( '?' . http_build_query( $args ) );
+ }
+ } else {
+ $_args['body'] = wp_json_encode( $args );
+ }
+
+ $res = wp_remote_request( $url, $_args );
+
+ // logging data.
+ $utils = Hustle_Provider_Utils::get_instance();
+ $utils->last_url_request = $url;
+ $utils->last_data_sent = $_args;
+ $utils->last_data_received = $res;
+
+ if ( ! is_wp_error( $res ) && is_array( $res ) ) {
+ $code = $res['response']['code'];
+
+ if ( $code >= 200 && $code < 300 ) {
+ $body = wp_remote_retrieve_body( $res );
+ return json_decode( $body );
+ }
+
+ $err = new WP_Error();
+ $err->add( $code, $res['response']['message'] );
+ return $err;
+ }
+
+ return $res;
+ }
+
+ /**
+ * Sends rest GET request
+ *
+ * @param string $action Action.
+ * @param array $args Args.
+ * @return array|mixed|object|WP_Error
+ */
+ private function get( $action, $args = array() ) {
+ return $this->request( $action, 'GET', $args );
+ }
+
+ /**
+ * Sends rest POST request
+ *
+ * @param string $action Action.
+ * @param array $args Args.
+ * @return array|mixed|object|WP_Error
+ */
+ private function post( $action, $args = array() ) {
+ return $this->request( $action, 'POST', $args );
+ }
+
+ /**
+ * Sends rest PUT request
+ *
+ * @param string $action Action.
+ * @param array $args Args.
+ * @return array|mixed|object|WP_Error
+ */
+ private function put( $action, $args = array() ) {
+ return $this->request( $action, 'PUT', $args );
+ }
+
+ /**
+ * Retrieves ConvertKit forms as array of objects
+ *
+ * @return array|WP_Error
+ */
+ public function get_forms() {
+ $response = $this->get( 'forms' );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ if ( ! isset( $response->forms ) ) {
+ return new WP_Error( 'forms_not_found', __( 'Not found forms with this api key.', 'hustle' ) );
+ }
+
+ return $response->forms;
+ }
+
+ /**
+ * Retrieves ConvertKit subscribers as array of objects
+ *
+ * @return array|WP_Error
+ */
+ public function get_subscribers() {
+ $response = $this->get( 'subscribers' );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ if ( ! isset( $response->subscribers ) ) {
+ return new WP_Error( 'subscribers_not_found', __( 'Not found subscribers with this api key.', 'hustle' ) );
+ }
+
+ return $response->subscribers;
+ }
+
+ /**
+ * Retrieves ConvertKit form's custom fields as array of objects
+ *
+ * @return array|WP_Error
+ */
+ public function get_form_custom_fields() {
+ $response = $this->get( 'custom_fields' );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ if ( ! isset( $response->custom_fields ) ) {
+ return new WP_Error( 'custom_fields_not_found', __( 'Not found custom fields with this api key.', 'hustle' ) );
+ }
+
+ return $response->custom_fields;
+ }
+
+ /**
+ * Add new custom fields to subscription
+ *
+ * @param array $field_data Fields data.
+ * @return array|mixed|object|WP_Error
+ */
+ public function create_custom_fields( $field_data ) {
+ $args = array(
+ 'label' => $field_data['label'],
+ );
+
+ $res = $this->post( 'custom_fields', $args );
+
+ return is_wp_error( $res ) ? $res : __( 'Successfully added custom field', 'hustle' );
+ }
+
+ /**
+ * Add new subscriber
+ *
+ * @param string $form_id Form ID.
+ * @param array $data Data.
+ * @return array|mixed|object|WP_Error
+ */
+ public function subscribe( $form_id, $data ) {
+ // First, create or update the subscriber.
+ $subscriber_data = array(
+ 'email_address' => $data['email'],
+ );
+
+ if ( isset( $data['first_name'] ) ) {
+ $subscriber_data['first_name'] = $data['first_name'];
+ }
+
+ if ( isset( $data['fields'] ) ) {
+ $subscriber_data['fields'] = $data['fields'];
+ }
+
+ // Create subscriber using v4 API.
+ $subscriber_res = $this->post( 'subscribers', $subscriber_data );
+
+ if ( is_wp_error( $subscriber_res ) ) {
+ return $subscriber_res;
+ }
+
+ if ( ! isset( $subscriber_res->subscriber ) ) {
+ return new WP_Error( 'subscriber_creation_failed', __( 'Failed to create subscriber.', 'hustle' ) );
+ }
+
+ $subscriber_id = $subscriber_res->subscriber->id;
+
+ // Now add subscriber to the form.
+ $form_data = array(
+ 'referrer' => isset( $data['referrer'] ) ? $data['referrer'] : '',
+ );
+
+ $url = 'forms/' . $form_id . '/subscribers/' . $subscriber_id;
+ $res = $this->post( $url, $form_data );
+
+ return is_wp_error( $res ) ? $res : __( 'Successful subscription', 'hustle' );
+ }
+
+ /**
+ * Update subscriber
+ *
+ * @since 4.0
+ *
+ * @param string $id ID.
+ * @param array $data Data.
+ * @return array|mixed|object|WP_Error
+ */
+ public function update_subscriber( $id, $data ) {
+ $url = 'subscribers/' . $id;
+ $res = $this->put( $url, $data );
+
+ return is_wp_error( $res ) ? $res : __( 'Successful subscription', 'hustle' );
+ }
+
+ /**
+ * Delete subscriber from the list
+ *
+ * @param string $list_id List ID.
+ * @param string $email Email.
+ *
+ * @return bool
+ */
+ public function delete_email( $list_id, $email ) {
+ // Get subscriber by email first.
+ $subscriber = $this->is_subscriber( $email );
+
+ if ( ! $subscriber ) {
+ return false;
+ }
+
+ $subscriber_id = is_object( $subscriber ) ? $subscriber->id : false;
+
+ if ( ! $subscriber_id ) {
+ return false;
+ }
+
+ // Unsubscribe the subscriber using v4 API.
+ $url = 'subscribers/' . $subscriber_id . '/unsubscribe';
+ $res = $this->post( $url, array() );
+
+ return ! is_wp_error( $res );
+ }
+
+ /**
+ * Verify if an email is already a subscriber.
+ *
+ * @param string $email Email.
+ *
+ * @return object|false Returns data of existing subscriber if exist otherwise false.
+ **/
+ public function is_subscriber( $email ) {
+ $args = array(
+ 'email_address' => $email,
+ );
+
+ $res = $this->get( 'subscribers', $args );
+
+ if ( is_wp_error( $res ) ) {
+ return false;
+ }
+
+ if ( ! empty( $res->subscribers ) && is_array( $res->subscribers ) ) {
+ return array_shift( $res->subscribers );
+ }
+
+ return false;
+ }
+
+ /**
+ * Verify if an email is already a subscriber in a form.
+ *
+ * @param string $email Email.
+ * @param integer $form_id Form ID.
+ *
+ * @return boolean|integer Subscriber ID if the subscriber exists, otherwise false.
+ **/
+ public function is_form_subscriber( $email, $form_id ) {
+ $url = 'forms/' . $form_id . '/subscriptions';
+ $res = $this->get( $url );
+ $exist = false;
+
+ $utils = Hustle_Provider_Utils::get_instance();
+ $utils->last_data_received = $res;
+ $utils->last_url_request = trailingslashit( $this->endpoint ) . $url;
+
+ if ( is_wp_error( $res ) ) {
+ Hustle_Provider_Utils::maybe_log( 'There was an error retrieving the subscribers from Kit: ' . $res->get_error_message() );
+ return false;
+ }
+
+ if ( empty( $res->subscriptions ) ) {
+ return false;
+ }
+
+ // Check subscribers in the current page.
+ $subscribers = wp_list_pluck( $res->subscriptions, 'subscriber' );
+ $emails = wp_list_pluck( $subscribers, 'email_address' );
+ $subscribers_id = wp_list_pluck( $subscribers, 'id' );
+
+ $key = array_search( $email, $emails, true );
+ if ( false !== $key ) {
+ return $subscribers_id[ $key ];
+ }
+
+ // Handle pagination if there are more pages.
+ if ( isset( $res->pagination ) && $res->pagination->has_next_page && ! empty( $res->pagination->end_cursor ) ) {
+ $cursor = $res->pagination->end_cursor;
+
+ while ( $cursor ) {
+ $args = array( 'after' => $cursor );
+ $res = $this->get( $url, $args );
+
+ $utils = Hustle_Provider_Utils::get_instance();
+ $utils->last_data_received = $res;
+ $utils->last_url_request = trailingslashit( $this->endpoint ) . $url;
+ $utils->last_data_sent = $args;
+
+ if ( is_wp_error( $res ) || empty( $res->subscriptions ) ) {
+ break;
+ }
+
+ $subscribers = wp_list_pluck( $res->subscriptions, 'subscriber' );
+ $emails = wp_list_pluck( $subscribers, 'email_address' );
+ $subscribers_id = wp_list_pluck( $subscribers, 'id' );
+
+ $key = array_search( $email, $emails, true );
+ if ( false !== $key ) {
+ return $subscribers_id[ $key ];
+ }
+
+ // Move to next page if available.
+ if ( isset( $res->pagination ) && $res->pagination->has_next_page && ! empty( $res->pagination->end_cursor ) ) {
+ $cursor = $res->pagination->end_cursor;
+ } else {
+ break;
+ }
+ }
+ }
+
+ return false;
+ }
+}
--- a/wordpress-popup/inc/providers/convertkit/hustle-convertkit-api.php
+++ b/wordpress-popup/inc/providers/convertkit/hustle-convertkit-api.php
@@ -10,7 +10,7 @@
*
* @class Hustle_ConvertKit_Api
**/
-class Hustle_ConvertKit_Api {
+class Hustle_ConvertKit_Api implements Hustle_ConvertKit_Api_Interface {
/**
* Api key
--- a/wordpress-popup/inc/providers/convertkit/hustle-convertkit-form-hooks.php
+++ b/wordpress-popup/inc/providers/convertkit/hustle-convertkit-form-hooks.php
@@ -14,6 +14,36 @@
class Hustle_ConvertKit_Form_Hooks extends Hustle_Provider_Form_Hooks_Abstract {
/**
+ * Hustle_Provider_Form_Hooks_Abstract constructor.
+ *
+ * @param Hustle_Provider_Abstract $addon Provider's instance.
+ * @param int $module_id ID of the module the form belogngs to.
+ *
+ * @since 7.8.11
+ */
+ public function __construct( Hustle_Provider_Abstract $addon, $module_id ) {
+ $this->module_id = $module_id;
+
+ $this->form_settings_instance = $addon->get_provider_form_settings( $this->module_id );
+ $form_settings_values = $this->form_settings_instance->get_form_settings_values();
+
+ if ( isset( $form_settings_values['selected_global_multi_id'] ) ) {
+ // Get the version of the addon for the selected global multi id.
+ $version = $addon->get_setting( 'version', '1.0', $form_settings_values['selected_global_multi_id'] );
+ if ( version_compare( $version, '2.0', '<' ) ) {
+ // Fallback to ConvertKit V1 for older versions.
+ // The plugin uses new ConvertKit V2 by default.
+ $addon = new Hustle_ConvertKit();
+ // get the form settings instance to be available throughout cycle.
+ $this->form_settings_instance = $addon->get_provider_form_settings( $this->module_id );
+ }
+ }
+
+ $this->addon = $addon; // TODO: replace this by $this->provider in 4.0.1 and adapt all providers to this.
+ $this->provider = $addon;
+ }
+
+ /**
* Add ConvertKit data to entry.
*
* @since 4.0
--- a/wordpress-popup/inc/providers/convertkit/hustle-convertkit-v2.php
+++ b/wordpress-popup/inc/providers/convertkit/hustle-convertkit-v2.php
@@ -0,0 +1,330 @@
+<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
+/**
+ * Hustle_ConvertKit_V2 class
+ *
+ * @package Hustle
+ */
+
+if ( ! class_exists( 'Hustle_ConvertKit_V2' ) ) :
+
+ include_once 'hustle-convertkit-api-v2.php';
+
+ /**
+ * ConvertKit V2 Email Integration (API Key only)
+ *
+ * @class Hustle_ConvertKit_V2
+ * @version 7.8.11
+ **/
+ class Hustle_ConvertKit_V2 extends Hustle_ConvertKit {
+
+ /**
+ * Api
+ *
+ * @var Hustle_ConvertKit_API_V2
+ */
+ protected static $api;
+
+ /**
+ * Version
+ *
+ * @since 7.8.11
+ * @var string
+ */
+ protected $version = '2.0';
+
+ /**
+ * Array of options which should exist for confirming that settings are completed
+ *
+ * @since 7.8.11
+ * @var array
+ */
+ protected $completion_options = array( 'api_key' );
+
+ /**
+ * Provider constructor.
+ */
+ public function __construct() {
+ $this->icon_2x = plugin_dir_url( __FILE__ ) . 'images/icon.png';
+ $this->logo_2x = plugin_dir_url( __FILE__ ) . 'images/logo.jpg';
+ }
+
+ /**
+ * Configure the API key settings. Global settings.
+ *
+ * @since 7.8.11
+ *
+ * @param array $submitted_data Submitted data.
+ * @return array
+ */
+ public function configure_api_key( $submitted_data ) {
+ $has_errors = false;
+ $default_data = array(
+ 'api_key' => '',
+ 'name' => '',
+ );
+ $current_data = $this->get_current_data( $default_data, $submitted_data );
+ $is_submit = isset( $submitted_data['api_key'] );
+ $global_multi_id = $this->get_global_multi_id( $submitted_data );
+
+ $api_key_valid = true;
+
+ if ( $is_submit ) {
+
+ $api_key_valid = ! empty( $current_data['api_key'] );
+ $api_key_validated = $api_key_valid && $this->validate_credentials( $submitted_data['api_key'] );
+
+ if ( ! $api_key_validated ) {
+ $error_message = $this->provider_connection_falied();
+ $api_key_valid = false;
+ $has_errors = true;
+ }
+
+ if ( ! $has_errors ) {
+ $settings_to_save = array(
+ 'api_key' => $current_data['api_key'],
+ 'name' => $current_data['name'],
+ 'version' => $this->version,
+ );
+ // If not active, activate it.
+ // TODO: Wrap this in a friendlier method.
+ if ( Hustle_Provider_Utils::is_provider_active( $this->slug )
+ || Hustle_Providers::get_instance()->activate_addon( $this->slug ) ) {
+ $this->save_multi_settings_values( $global_multi_id, $settings_to_save );
+ } else {
+ $error_message = __( "Provider couldn't be activated.", 'hustle' );
+ $has_errors = true;
+ }
+ }
+
+ if ( ! $has_errors ) {
+
+ return array(
+ 'html' => Hustle_Provider_Utils::get_integration_modal_title_markup( __( 'Kit Added', 'hustle' ), __( 'You can now go to your pop-ups, slide-ins and embeds and assign them to this integration', 'hustle' ) ),
+ 'buttons' => array(
+ 'close' => array(
+ 'markup' => Hustle_Provider_Utils::get_provider_button_markup( __( 'Close', 'hustle' ), 'sui-button-ghost', 'close' ),
+ ),
+ ),
+ 'redirect' => false,
+ 'has_errors' => false,
+ 'notification' => array(
+ 'type' => 'success',
+ 'text' => '<strong>' . $this->get_title() . '</strong> ' . esc_html__( 'Successfully connected', 'hustle' ),
+ ),
+ );
+
+ }
+ }
+
+ $options = array(
+ array(
+ 'type' => 'wrapper',
+ 'class' => $api_key_valid ? '' : 'sui-form-field-error',
+ 'elements' => array(
+ 'label' => array(
+ 'type' => 'label',
+ 'for' => 'api_key',
+ 'value' => __( 'API Key', 'hustle' ),
+ ),
+ 'api_