is_acquis_screen() ) return; remove_all_actions( 'admin_notices' ); remove_all_actions( 'all_admin_notices' ); add_action( 'admin_notices', array( $this, 'render_promo_banner' ) ); do_action( 'acquis_reregister_admin_notices' ); } /** * Whether the current admin screen belongs to AcquisitionSuite (Lite or * Pro, both register every submenu under the same 'acquisitionsuite' * top-level parent). Deliberately does NOT use * get_current_screen()->parent_base: confirmed via debug logging that * it's still empty at the point 'in_admin_header' fires (parent_base * only gets populated later, while WordPress renders the admin menu * sidebar, which happens after in_admin_header in the page's own * output order) meaning every check built on it, including the * original version of suppress_foreign_admin_notices(), silently * never matched anything since this suppression was first added. * $screen->id, in contrast, is set as soon as set_current_screen() * runs and reliably follows WordPress's own "{parent_slug}_page_ * {submenu_slug}" convention (or "toplevel_page_{slug}" for the * top-level item itself), confirmed empirically for both Lite and Pro * submenu screens. * * @return bool */ private function is_acquis_screen() { $screen = get_current_screen(); if ( ! $screen ) return false; return 'toplevel_page_acquisitionsuite' === $screen->id || 0 === strpos( $screen->id, 'acquisitionsuite_page_' ); } /** * Output global admin CSS via WP Stylesheet API. * Uses a virtual handle (no external file) so wp_add_inline_style() works * on every admin screen without requiring a physical CSS file. */ public function output_global_icon_css() { wp_register_style( 'acquis-admin-global', false, array(), ACQUIS_VERSION ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- version passed as 4th arg wp_enqueue_style( 'acquis-admin-global' ); wp_add_inline_style( 'acquis-admin-global', ' /* ── Menu icon — WP renders the SVG data-URI as , let it breathe ─ */ #adminmenu #toplevel_page_acquisitionsuite .wp-menu-image img { width: 18px !important; height: 18px !important; padding: 0 !important; margin-top: 7px !important; opacity: 0.6; } #adminmenu #toplevel_page_acquisitionsuite:hover .wp-menu-image img, #adminmenu #toplevel_page_acquisitionsuite.wp-has-current-submenu .wp-menu-image img, #adminmenu #toplevel_page_acquisitionsuite.current .wp-menu-image img { opacity: 1; } /* ── Add-ons: the one menu entry that stands out ───────────── */ #adminmenu .wp-submenu li a[href*="page=acquis-addons"] { margin: 3px 10px 3px 0 !important; padding: 6px 10px !important; border-radius: 6px; background: linear-gradient(135deg, #f97316, #ea580c); color: #fff !important; font-weight: 700 !important; box-shadow: 0 1px 4px rgba(249, 115, 22, .35); } #adminmenu .wp-submenu li a[href*="page=acquis-addons"]::before { content: "\26A1"; margin-right: 5px; font-size: 11px; } #adminmenu .wp-submenu li a[href*="page=acquis-addons"]:hover, #adminmenu .wp-submenu li a[href*="page=acquis-addons"]:focus, #adminmenu .wp-submenu li.current a[href*="page=acquis-addons"] { background: linear-gradient(135deg, #ea580c, #c2410c); color: #fff !important; } #adminmenu .wp-submenu li a[href*="page=acquis-addons"] .update-plugins { margin-left: 6px; } ' ); } public function enqueue_assets( $hook ) { if ( strpos( $hook, 'acquisitionsuite' ) === false ) return; wp_enqueue_style( 'acquis-admin', ACQUIS_PLUGIN_URL . 'admin/acquis-admin.css', array(), acquis_asset_ver( ACQUIS_PLUGIN_DIR . 'admin/acquis-admin.css' ) ); wp_enqueue_style( 'wp-color-picker' ); wp_enqueue_script( 'acquis-admin', ACQUIS_PLUGIN_URL . 'admin/acquis-admin.js', array( 'jquery', 'wp-color-picker' ), acquis_asset_ver( ACQUIS_PLUGIN_DIR . 'admin/acquis-admin.js' ), true ); wp_enqueue_media(); // The dashboard has a bar that opens the Lead Funnels wizard in place. if ( 'toplevel_page_acquisitionsuite' === $hook && class_exists( 'ACQUIS_Funnels_Admin' ) && current_user_can( 'manage_options' ) ) { ( new ACQUIS_Funnels_Admin() )->enqueue_wizard(); } // MailWizz field-map data — needed for initMailwizzFieldMap() in acquis-admin.js $mw_field_map = get_option( 'acquis_mailwizz_custom_fields', array( array( 'tag' => '', 'source' => '' ) ) ); $mw_sources = array( 'full_name' => 'Full Name', 'email' => 'Email', 'file_name' => 'File Name', 'file_ext' => 'File Extension', 'site_name' => 'Site Name', 'site_url' => 'Site URL', ); wp_localize_script( 'acquis-admin', 'ACQUIS_Admin', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'acquis_admin_nonce' ), 'i18n' => array( 'sending' => __( 'Sending…', 'acquisitionsuite' ), 'loading' => __( 'Loading…', 'acquisitionsuite' ), 'testing' => __( 'Testing…', 'acquisitionsuite' ), 'select_logo' => __( 'Select Logo', 'acquisitionsuite' ), 'use_logo' => __( 'Use This Image', 'acquisitionsuite' ), ), ) ); // Pass MailWizz field-map config as a separate object so acquis-admin.js // can initialise the dynamic row builder without any inline script in PHP. wp_add_inline_script( 'acquis-admin', 'window.ACQUIS_MW = ' . wp_json_encode( array( 'idx' => count( (array) $mw_field_map ), 'sources' => $mw_sources, ) ) . ';', 'before' ); } public function allow_mime_types( $mimes ) { return array_merge( $mimes, ACQUIS_File_Manager::allowed_mime_types() ); } // DASHBOARD public function render_dashboard() { $total_leads = ACQUIS_Database::count_leads(); $total_sent = ACQUIS_Database::count_email_logs( 'sent' ); $total_failed = ACQUIS_Database::count_email_logs( 'failed' ); $fq = new WP_Query( array( 'post_type' => ACQUIS_File_Manager::POST_TYPE, 'posts_per_page' => -1, 'fields' => 'ids' ) ); $total_files = $fq->found_posts; $recent_leads = ACQUIS_Database::get_leads( 5, 1 ); ?>
admin_header( esc_html__( 'Dashboard', 'acquisitionsuite' ) ); ?>

Email
full_name ); ?> email ); ?> file_name ?: ' ' ); ?> created_at ) ) ); ?>

ACQUIS_Mailchimp::is_enabled(), 'MailWizz' => ACQUIS_Mailwizz::is_enabled(), 'Brevo' => ACQUIS_Brevo::is_enabled() ) as $name => $on ) : ?>
' . esc_html__( 'Active', 'acquisitionsuite' ) . '' : '' . esc_html__( 'Disabled', 'acquisitionsuite' ) . ''; ?>

get_notice_html(); $per_page = 20; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only pagination $page = max( 1, absint( $_GET['acquis_page'] ?? 1 ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display filter $fq = new WP_Query( array( 'post_type' => ACQUIS_File_Manager::POST_TYPE, 'posts_per_page' => $per_page, 'paged' => $page, 'orderby' => 'date', 'order' => 'DESC', ) ); $total_files = (int) $fq->found_posts; $total_pages = max( 1, (int) $fq->max_num_pages ); $download_counts = class_exists( 'ACQUIS_P3_DB' ) ? ACQUIS_P3_DB::get_download_counts_by_file() : array(); ?>
admin_header( esc_html__( 'Gated Files', 'acquisitionsuite' ) ); echo wp_kses_post( $notice ); ?>

PDF, ZIP, DOCX, XLSX, PPTX, MP4, MP3, JPG, PNG

have_posts() ) : ?>
have_posts() ) : $fq->the_post(); $pid = get_the_ID(); $source_type = get_post_meta( $pid, '_acquis_file_source_type', true ) ?: 'upload'; $ext = strtoupper( get_post_meta( $pid, '_acquis_file_ext', true ) ?: get_post_meta( $pid, '_sll_file_ext', true ) ); $size = ACQUIS_File_Manager::format_bytes( get_post_meta( $pid, '_acquis_file_size', true ) ?: get_post_meta( $pid, '_sll_file_size', true ) ); $dl_count = $download_counts[ $pid ] ?? 0; ?>
# 0 && $cfg_sale < (float) $pay_cfg['price'] ) : ?>
[acquis_download id=""]
1 ) : ?>
1 ) : ?>« 1 ) echo '…'; for ( $i = $start; $i <= $end; $i++ ) : ?> …'; ?> »

admin_header( esc_html__( 'Leads', 'acquisitionsuite' ), "" . number_format( $total ) . " leads" ); ?>
id ) : array(); $segment = class_exists( 'ACQUIS_Automation_Engine' ) ? ACQUIS_Automation_Engine::get_lead_segment( $l->id ) : ''; $score = class_exists( 'ACQUIS_Lead_Scorer' ) ? ACQUIS_Lead_Scorer::current_total( $l->id ) : 0; $tier = class_exists( 'ACQUIS_Lead_Scorer' ) ? ACQUIS_Lead_Scorer::score_label( $score ) : array( 'label' => ' ', 'class' => 'acquis-badge-gray' ); $stage = class_exists( 'ACQUIS_Pipeline' ) ? ACQUIS_Pipeline::get_lead_stage( $l->id ) : null; ?>
lead_sort_arrow('full_name', $orderby, $order), array( 'span' => array( 'class' => array() ) ) ); ?> Emaillead_sort_arrow('email', $orderby, $order), array( 'span' => array( 'class' => array() ) ) ); ?> lead_sort_arrow('created_at', $orderby, $order), array( 'span' => array( 'class' => array() ) ) ); ?>
full_name); ?>
email); ?> file_name ?: ' '); ?>
name); ?>
device ) ) : ?>device . ' · ' . $l->browser ); ?>
country ) ) : ?>city ? $l->city . ', ' . $l->country : $l->country ); ?>ip_address ); ?>
created_at))); ?>
1 ) : ?>
1) : ?>« 1) echo '…'; for($i=$start;$i<=$end;$i++): ?> …'; ?> »

' . esc_html__( 'Integration settings saved.', 'acquisitionsuite' ) . '

' : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display filter ?>
admin_header( esc_html__( 'Email Marketing Integrations', 'acquisitionsuite' ) ); echo wp_kses_post( $saved_msg ); ?>
Mailchimp MailWizz Brevo

Mailchimp

admin_header( esc_html__( 'Email Logs', 'acquisitionsuite' ) ); ?>
status === 'sent' ? 'acquis-badge-green' : ( $log->status === 'failed' ? 'acquis-badge-red' : 'acquis-badge-gray' ); ?>
Email
full_name ?: ' ' ); ?> email ); ?> subject ); ?> status ) ); ?> status === 'failed' && $log->error_msg ) : ?> provider ); ?> created_at ) ) ); ?>
1 ) : ?>
1 ) echo '…'; for ( $i = $start; $i <= $end; $i++ ) : ?>…'; ?>
'absint', 'acquis_free_download_limit' => 'absint', 'acquis_success_message' => 'sanitize_textarea_field', 'acquis_success_message_standalone' => 'sanitize_textarea_field', 'acquis_button_label' => 'sanitize_text_field', 'acquis_modal_title' => 'sanitize_text_field', 'acquis_modal_subtitle' => 'sanitize_textarea_field', ) as $key => $fn ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized via call_user_func( $fn, ... ) above, $fn is always one of the whitelisted sanitizer callables in the array key above, never derived from request data update_option( $key, call_user_func( $fn, wp_unslash( $_POST[ $key ] ?? '' ) ) ); } // Branding opt-in — checkbox: save '1' if checked, '0' if not update_option( 'acquis_show_powered_by', isset( $_POST['acquis_show_powered_by'] ) ? '1' : '0' ); update_option( 'acquis_modal_header_color', sanitize_hex_color( wp_unslash( $_POST['acquis_modal_header_color'] ?? '#0D1B4B' ) ) ?: '#0D1B4B' ); update_option( 'acquis_modal_accent_color', sanitize_hex_color( wp_unslash( $_POST['acquis_modal_accent_color'] ?? '#F97316' ) ) ?: '#F97316' ); // Uninstall data deletion opt-in update_option( 'acquis_delete_data_on_uninstall', isset( $_POST['acquis_delete_data_on_uninstall'] ) ? '1' : '0' ); echo '

' . esc_html__( 'Settings saved.', 'acquisitionsuite' ) . '

'; } $saved_msg = isset( $_GET['acquis_saved'] ) ? '

' . esc_html__( 'Email settings saved.', 'acquisitionsuite' ) . '

' : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display filter $preview_ctx = array( 'full_name' => 'Amara Osei', 'email' => 'amara@example.com', 'file_name' => 'Marketing_Guide_2024.pdf', 'download_link' => '#preview', 'site_name' => get_bloginfo( 'name' ) ); $preview_html = ACQUIS_Email_Template::render( $preview_ctx ); $expiry_mins = absint( get_option( 'acquis_link_expiry_minutes', 30 ) ); ?>
admin_header( esc_html__( 'Settings', 'acquisitionsuite' ) ); echo wp_kses_post( $saved_msg ); ?>

= 60 ) { $hrs = floor( $expiry_mins / 60 ); $mins = $expiry_mins % 60; echo ''; echo '= ' . absint( $hrs ) . ' ' . esc_html( _n( 'hour', 'hours', $hrs, 'acquisitionsuite' ) ); if ( $mins ) echo ' ' . absint( $mins ) . ' ' . esc_html( _n( 'minute', 'minutes', absint( $mins ), 'acquisitionsuite' ) ); echo ''; } else { echo '= ' . absint( $expiry_mins ) . ' ' . esc_html__( 'minutes', 'acquisitionsuite' ) . ''; } ?>

' . esc_html__( 'Payments → Settings → General', 'acquisitionsuite' ) . '' ); ?>

array('width'=>true,'cellpadding'=>true,'cellspacing'=>true,'style'=>true,'align'=>true,'bgcolor'=>true),'td'=>array('align'=>true,'style'=>true,'colspan'=>true,'rowspan'=>true,'bgcolor'=>true),'tr'=>array('style'=>true,'bgcolor'=>true),'th'=>array('style'=>true),'img'=>array('src'=>true,'alt'=>true,'style'=>true,'width'=>true,'height'=>true,'border'=>true),'a'=>array('href'=>true,'style'=>true,'target'=>true,'rel'=>true),'center'=>array('style'=>true),'font'=>array('style'=>true,'color'=>true,'size'=>true) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized via wp_kses() on the next line $template = wp_unslash( $_POST['acquis_email_template'] ?? '' ); update_option( 'acquis_email_template', wp_kses( $template, $allowed_html ) ); update_option( 'acquis_standalone_email_subject', sanitize_text_field( wp_unslash( $_POST['acquis_standalone_email_subject'] ?? '' ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized via wp_kses() on the next line $standalone_template = wp_unslash( $_POST['acquis_standalone_email_template'] ?? '' ); update_option( 'acquis_standalone_email_template', wp_kses( $standalone_template, $allowed_html ) ); wp_safe_redirect( add_query_arg( array( 'page' => 'acquisitionsuite-settings', 'acquis_saved' => '1' ), admin_url( 'admin.php' ) ) ); exit; } public function handle_save_integrations() { if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Unauthorized', 403 ); check_admin_referer( 'acquis_save_integrations' ); // Mailchimp update_option( 'acquis_mailchimp_enabled', isset( $_POST['acquis_mailchimp_enabled'] ) ? '1' : '0' ); update_option( 'acquis_mailchimp_api_key', sanitize_text_field( wp_unslash( $_POST['acquis_mailchimp_api_key'] ?? '' ) ) ); update_option( 'acquis_mailchimp_list_id', sanitize_text_field( wp_unslash( $_POST['acquis_mailchimp_list_id'] ?? '' ) ) ); update_option( 'acquis_mailchimp_tags', sanitize_text_field( wp_unslash( $_POST['acquis_mailchimp_tags'] ?? '' ) ) ); update_option( 'acquis_mailchimp_double_optin', isset( $_POST['acquis_mailchimp_double_optin'] ) ? '1' : '0' ); // MailWizz update_option( 'acquis_mailwizz_enabled', isset( $_POST['acquis_mailwizz_enabled'] ) ? '1' : '0' ); update_option( 'acquis_mailwizz_api_url', esc_url_raw( wp_unslash( $_POST['acquis_mailwizz_api_url'] ?? '' ) ) ); update_option( 'acquis_mailwizz_public_key', sanitize_text_field( wp_unslash( $_POST['acquis_mailwizz_public_key'] ?? '' ) ) ); update_option( 'acquis_mailwizz_private_key', sanitize_text_field( wp_unslash( $_POST['acquis_mailwizz_private_key'] ?? '' ) ) ); update_option( 'acquis_mailwizz_list_uid', sanitize_text_field( wp_unslash( $_POST['acquis_mailwizz_list_uid'] ?? '' ) ) ); update_option( 'acquis_mailwizz_double_optin', isset( $_POST['acquis_mailwizz_double_optin'] ) ? '1' : '0' ); $clean_fields = array(); foreach ( (array) wp_unslash( $_POST['acquis_mailwizz_custom_fields'] ?? array() ) as $row ) { if ( ! empty( $row['tag'] ) && ! empty( $row['source'] ) ) { $clean_fields[] = array( 'tag' => sanitize_text_field( $row['tag'] ), 'source' => sanitize_text_field( $row['source'] ) ); } } update_option( 'acquis_mailwizz_custom_fields', $clean_fields ); // Brevo update_option( 'acquis_brevo_enabled', isset( $_POST['acquis_brevo_enabled'] ) ? '1' : '0' ); update_option( 'acquis_brevo_api_key', sanitize_text_field( wp_unslash( $_POST['acquis_brevo_api_key'] ?? '' ) ) ); update_option( 'acquis_brevo_list_id', absint( $_POST['acquis_brevo_list_id'] ?? 0 ) ); update_option( 'acquis_brevo_double_optin', isset( $_POST['acquis_brevo_double_optin'] ) ? '1' : '0' ); update_option( 'acquis_brevo_doi_template_id', absint( $_POST['acquis_brevo_doi_template_id'] ?? 0 ) ); // Allow Pro to save its own integration options (e.g. WhatsApp, Geo Lookup) do_action( 'acquis_save_integrations_extra' ); wp_safe_redirect( add_query_arg( array( 'page' => 'acquisitionsuite-integrations', 'acquis_saved' => '1' ), admin_url( 'admin.php' ) ) ); exit; } public function handle_file_upload() { if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Unauthorized', 403 ); check_admin_referer( 'acquis_upload_file', 'acquis_upload_nonce' ); $title = sanitize_text_field( wp_unslash( $_POST['file_title'] ?? '' ) ); $source_type = ( 'url' === sanitize_text_field( wp_unslash( $_POST['acquis_source_type'] ?? 'upload' ) ) ) ? 'url' : 'upload'; if ( empty( $title ) ) { wp_safe_redirect( add_query_arg( 'acquis_msg', 'err_move', admin_url( 'admin.php?page=acquisitionsuite-files' ) ) ); exit; } if ( 'url' === $source_type ) { $url = esc_url_raw( wp_unslash( $_POST['file_url'] ?? '' ) ); $result = ACQUIS_File_Manager::handle_url_file( $url, $title ); $msg = is_wp_error( $result ) ? 'err_url' : 'uploaded'; } else { if ( ! isset( $_FILES['acquis_file']['error'] ) || $_FILES['acquis_file']['error'] !== UPLOAD_ERR_OK ) { wp_safe_redirect( add_query_arg( 'acquis_msg', 'err_move', admin_url( 'admin.php?page=acquisitionsuite-files' ) ) ); exit; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- raw $_FILES array is validated/moved by ACQUIS_File_Manager::handle_upload() (wp_handle_upload()); nothing here reads it as trusted text $result = ACQUIS_File_Manager::handle_upload( $_FILES['acquis_file'], $title ); $msg = is_wp_error( $result ) ? ( $result->get_error_code() === 'acquis_invalid_type' ? 'err_type' : 'err_move' ) : 'uploaded'; } if ( ! is_wp_error( $result ) ) { // Allow Pro to save paid download config for the new file do_action( 'acquis_after_file_upload', $result ); } wp_safe_redirect( add_query_arg( 'acquis_msg', $msg, admin_url( 'admin.php?page=acquisitionsuite-files' ) ) ); exit; } public function handle_file_delete() { if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Unauthorized', 403 ); $fid = absint( $_POST['file_id'] ?? 0 ); check_admin_referer( 'acquis_delete_file_' . $fid, 'acquis_delete_nonce' ); $file = ACQUIS_File_Manager::get_file_data( $fid ); if ( $file && file_exists( $file['path'] ) ) { wp_delete_file( $file['path'] ); // WP Filesystem API per WP.org Guideline 7 } wp_delete_post( $fid, true ); wp_safe_redirect( add_query_arg( 'acquis_msg', 'deleted', admin_url( 'admin.php?page=acquisitionsuite-files' ) ) ); exit; } private function admin_header( $title, $extra = '' ) { $title_text = trim( wp_strip_all_tags( $title ) ); ?>

v
id; if ( ! $is_dashboard && ! $this->is_acquis_screen() ) return; $snoozed_until = (int) get_user_meta( get_current_user_id(), 'acquis_promo_snoozed_until', true ); if ( $snoozed_until && time() < $snoozed_until ) return; $activated = (int) get_option( 'acquis_activated_time', 0 ); if ( ! $activated || ( time() - $activated ) < 7 * DAY_IN_SECONDS ) return; $pro_active = class_exists( 'ACQUIS_License_Manager' ) && ACQUIS_License_Manager::is_active(); // sub_confirmation=1 is a real, documented YouTube URL parameter // that pops the subscribe confirmation dialog on arrival, not // something invented here. $youtube_url = 'https://www.youtube.com/@UfirstDEV?sub_confirmation=1'; // X's own Web Intent endpoint for composing a pre-filled post, not // a "follow this account" link, since there's no confirmed // AcquisitionSuite X handle to point that at. $tweet_url = add_query_arg( array( 'text' => rawurlencode( 'Turning file downloads into leads with AcquisitionSuite, the WordPress plugin for lead capture, automation, and paid downloads.' ), 'url' => rawurlencode( 'https://acquisitionsuite.ufirstdev.com' ), ), 'https://twitter.com/intent/tweet' ); $ajax_url = admin_url( 'admin-ajax.php' ); $nonce = wp_create_nonce( 'acquis_admin_nonce' ); ?>

array('success', 'File saved successfully.'), 'deleted' => array('success','File deleted.'), 'err_type' => array('error','File type not allowed.'), 'err_move' => array('error','Upload failed.'), 'err_url' => array('error','Please enter a valid http(s) URL.'), 'pricing_saved' => array('success','Pricing settings saved.') ); $key = sanitize_key( $_GET['acquis_msg'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display filter if ( ! isset( $msgs[$key] ) ) return ''; return '

' . esc_html( $msgs[$key][1] ) . '

'; } /** * Sortable-column helpers for the Leads table. These were previously * declared as global functions inside render_leads_page() itself, * which would trigger a fatal "cannot redeclare" error if that method * were ever called more than once in the same request converted to * private class methods to fix that. */ private function lead_sort_url( $col, $ob, $o ) { $no = ( $col === $ob && $o === 'ASC' ) ? 'DESC' : 'ASC'; return add_query_arg( array( 'orderby' => $col, 'order' => $no, 'acquis_page' => 1 ) ); } private function lead_sort_arrow( $col, $ob, $o ) { return $col != $ob ? '' : ( $o === 'ASC' ? ' ▲' : ' ▼' ); } public function ajax_delete_lead() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized' ); } check_ajax_referer( 'acquis_admin_nonce', 'nonce' ); $lead_id = absint( $_POST['lead_id'] ?? 0 ); if ( ! $lead_id ) { wp_send_json_error( 'Invalid lead ID' ); } global $wpdb; // Delete all data associated with this lead cascade to every // related table so no orphaned records remain. The lead row is // the central entity; without it, all downstream records are // meaningless and waste space. // 1. Submission values (must go before submissions so FK is clean) $sub_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}acquis_submissions WHERE lead_id=%d", $lead_id ) ); if ( $sub_ids ) { $ids = array_map( 'absint', $sub_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $lt is internal table name; placeholders built with array_fill+implode $ph = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); $lt_sv = $wpdb->prefix . 'acquis_submission_values'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix + hardcoded string; $ph is %d placeholders only $wpdb->query( $wpdb->prepare( "DELETE FROM `{$lt_sv}` WHERE submission_id IN ({$ph})", $ids ) ); } // 2. Submissions $wpdb->delete( $wpdb->prefix . 'acquis_submissions', array( 'lead_id' => $lead_id ) ); // 3. Download tokens $wpdb->delete( $wpdb->prefix . 'acquis_tokens', array( 'lead_id' => $lead_id ) ); // 4. Email logs $wpdb->delete( $wpdb->prefix . 'acquis_email_logs', array( 'lead_id' => $lead_id ) ); // Pro plugin handles its own cascade deletes via this hook do_action( 'acquis_before_lead_deleted', $lead_id ); // 10. Finally delete the lead itself $deleted = $wpdb->delete( $wpdb->prefix . 'acquis_leads', array( 'id' => $lead_id ) ); if ( $deleted === false ) { wp_send_json_error( 'Delete failed' ); } wp_send_json_success( array( 'id' => $lead_id ) ); } /** * AJAX — bulk delete multiple leads by ID array. * Each deletion follows the same path as single-lead delete so Pro cascade hook fires. */ public function ajax_bulk_delete_leads() { check_ajax_referer( 'acquis_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Forbidden.' ), 403 ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below per-ID $raw_ids = isset( $_POST['ids'] ) ? (array) wp_unslash( $_POST['ids'] ) : array(); $ids = array_filter( array_map( 'absint', $raw_ids ) ); if ( empty( $ids ) ) { wp_send_json_error( array( 'message' => esc_html__( 'No leads selected.', 'acquisitionsuite' ) ) ); } $deleted = 0; foreach ( $ids as $lead_id ) { $this->delete_lead( $lead_id ); $deleted++; } wp_send_json_success( array( 'message' => sprintf( /* translators: %d: number of leads deleted */ esc_html__( '%d lead(s) deleted.', 'acquisitionsuite' ), $deleted ), 'deleted' => $deleted, ) ); } public function ajax_delete_email_log() { if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error(); check_ajax_referer( 'acquis_admin_nonce', 'nonce' ); global $wpdb; $id = absint( $_POST['id'] ?? 0 ); if ( ! $id ) wp_send_json_error(); $wpdb->delete( $wpdb->prefix . 'acquis_email_logs', array( 'id' => $id ) ); wp_send_json_success(); } public function ajax_bulk_clear_email_logs() { if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error(); check_ajax_referer( 'acquis_admin_nonce', 'nonce' ); global $wpdb; $status = sanitize_key( wp_unslash( $_POST['status'] ?? '' ) ); $before = sanitize_text_field( wp_unslash( $_POST['before'] ?? '' ) ); $table = $wpdb->prefix . 'acquis_email_logs'; $where = '1=1'; $args = array(); if ( $status ) { $where .= ' AND status=%s'; $args[] = $status; } if ( $before ) { $where .= ' AND created_at<%s'; $args[] = $before . ' 00:00:00'; } // Use %i placeholder for table name so plugin check sees no bare interpolation. // $where is built entirely from wpdb->prepare() calls — safe to pass as string. if ( $args ) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $where built with prepare(); $table from $wpdb->prefix + hardcoded string $count = $wpdb->query( $wpdb->prepare( "DELETE FROM `{$table}` WHERE {$where}", ...$args ) ); } else { // No filters applied — $where is the literal string '1=1'; $table from $wpdb->prefix only // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $table from $wpdb->prefix + hardcoded string; no user input in query $count = $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE 1=1', $table ) ); } wp_send_json_success( array( 'count' => (int) $count ) ); } /** * AJAX — resend the download email from the Email Logs page. * Looks up the original log row, fetches the lead + file, and re-sends. */ public function ajax_resend_email_log() { check_ajax_referer( 'acquis_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Forbidden.' ), 403 ); } $log_id = absint( $_POST['log_id'] ?? 0 ); if ( ! $log_id ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid log ID.', 'acquisitionsuite' ) ) ); } global $wpdb; $log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}acquis_email_logs WHERE id = %d", $log_id ) ); if ( ! $log ) { wp_send_json_error( array( 'message' => esc_html__( 'Email log not found.', 'acquisitionsuite' ) ) ); } $lead = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}acquis_leads WHERE id = %d", absint( $log->lead_id ) ) ); if ( ! $lead ) { wp_send_json_error( array( 'message' => esc_html__( 'Lead not found.', 'acquisitionsuite' ) ) ); } // Regenerate a fresh download token so the link is valid $token = ACQUIS_Database::create_token( $lead->id, absint( $log->file_id ) ); $dl_url = add_query_arg( array( 'acquis_download' => 1, 'token' => $token ), home_url( '/' ) ); $file = ACQUIS_File_Manager::get_file_data( absint( $log->file_id ) ); if ( class_exists( 'ACQUIS_Email_Engine' ) ) { ACQUIS_Email_Engine::send( $lead->email, $lead->full_name, $file, $dl_url ); } wp_send_json_success( array( 'message' => esc_html__( 'Email resent successfully.', 'acquisitionsuite' ) ) ); } /** Daily cron: auto-purge old records per retention settings. */ public function run_daily_cleanup() { global $wpdb; $log_days = absint( get_option( 'acquis_retention_email_logs', 30 ) ); if ( $log_days > 0 ) { $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}acquis_email_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)", $log_days ) ); } // Pro retention cleanup (sequences, orders, transactions) handled by Pro do_action( 'acquis_daily_cleanup_pro' ); } // ══════════════════════════════════════════════════════════════════════════ // SYSTEM STATUS PAGE // ══════════════════════════════════════════════════════════════════════════ public function render_system_status() { global $wpdb; // Helper: render one status row $row = function( $label, $value, $status = 'ok', $note = '' ) { $colors = array( 'ok' => '#16a34a', 'warn' => '#d97706', 'fail' => '#dc2626' ); $icons = array( 'ok' => '✓', 'warn' => '!', 'fail' => '✗' ); $color = $colors[ $status ] ?? $colors['ok']; $icon = $icons[ $status ] ?? '✓'; echo ''; echo '' . esc_html( $label ) . ''; echo ''; echo ''; echo '' . esc_html( $icon ) . ''; echo esc_html( $value ); echo ''; if ( $note ) echo ' ' . esc_html( $note ) . ''; echo ''; }; $section = function( $title ) { echo '' . esc_html( $title ) . ''; }; ?>
admin_header( ' ' . esc_html__( 'System Status', 'acquisitionsuite' ) ); ?>

=' ); $row( 'WordPress Version', $wp_ver . ' (required: 6.2+)', $wp_ok ? 'ok' : 'fail', $wp_ok ? '' : 'Please update WordPress.' ); $php_ver = PHP_VERSION; $php_ok = version_compare( $php_ver, '7.4', '>=' ); $row( 'PHP Version', $php_ver . ' (required: 7.4+)', $php_ok ? 'ok' : 'fail', $php_ok ? '' : 'AcquisitionSuite requires PHP 7.4 or higher. Please upgrade.' ); $php_warn = version_compare( $php_ver, '8.0', '>=' ); $row( 'PHP 8.0+ (recommended)', $php_warn ? 'Yes' : 'No', $php_warn ? 'ok' : 'warn', $php_warn ? '' : 'PHP 8.0+ is recommended for best performance and security.' ); $memory_limit = ini_get('memory_limit'); $memory_mb = (int) $memory_limit; $mem_status = $memory_mb >= 256 ? 'ok' : ( $memory_mb >= 128 ? 'warn' : 'fail' ); $row( 'PHP Memory Limit', $memory_limit, $mem_status, $memory_mb < 256 ? '256MB+ recommended.' : '' ); $max_upload = ini_get('upload_max_filesize'); $row( 'Max Upload Size (upload_max_filesize)', $max_upload, 'ok' ); $post_max = ini_get('post_max_size'); $row( 'Max POST Size (post_max_size)', $post_max, 'ok' ); $max_exec = ini_get('max_execution_time'); $exec_ok = (int) $max_exec === 0 || (int) $max_exec >= 30; $row( 'Max Execution Time', $max_exec . 's', $exec_ok ? 'ok' : 'warn', $exec_ok ? '' : 'At least 30s recommended.' ); $row( 'WordPress Timezone', wp_timezone_string(), 'ok' ); $row( 'Server Time (UTC)', gmdate( 'Y-m-d H:i:s' ), 'ok' ); $row( 'WordPress Time', current_time('Y-m-d H:i:s'), 'ok' ); // ── PHP Extensions ───────────────────────────────────────────────── $section( esc_html__( 'PHP Extensions', 'acquisitionsuite' ) ); foreach ( array( 'curl' => array( true, 'Required for payment gateway and email-service API calls.' ), 'json' => array( true, 'Required for all API communication.' ), 'mbstring' => array( true, 'Required for multi-byte string handling.' ), 'openssl' => array( true, 'Required for webhook signature verification (Stripe, Paystack, Flutterwave).' ), 'fileinfo' => array( false, 'Recommended for better MIME type detection on file uploads.' ), 'zip' => array( false, 'Optional needed only if you plan to serve ZIP files.' ), ) as $ext => list( $required, $note ) ) { $has = extension_loaded( $ext ); $status = $has ? 'ok' : ( $required ? 'fail' : 'warn' ); $row( 'PHP Extension: ' . $ext, $has ? 'Enabled' : 'Disabled', $status, $has ? '' : $note ); } // ── WordPress Configuration ───────────────────────────────────────── $section( esc_html__( 'WordPress Configuration', 'acquisitionsuite' ) ); $row( 'WordPress Cron (WP-Cron)', defined('DISABLE_WP_CRON') && DISABLE_WP_CRON ? 'Disabled' : 'Enabled', defined('DISABLE_WP_CRON') && DISABLE_WP_CRON ? 'warn' : 'ok', defined('DISABLE_WP_CRON') && DISABLE_WP_CRON ? 'WP-Cron is disabled. Sequences and time-based automation will not fire unless you have a real server-side cron job configured.' : '' ); $row( 'SSL / HTTPS', is_ssl() ? 'Yes' : 'No', is_ssl() ? 'ok' : 'warn', is_ssl() ? '' : 'HTTPS is required for live payment gateway webhooks.' ); $row( 'WP Debug Mode', WP_DEBUG ? 'On' : 'Off', WP_DEBUG ? 'warn' : 'ok', WP_DEBUG ? 'Turn off WP_DEBUG on production sites.' : '' ); $row( 'WP Debug Log', ( defined('WP_DEBUG_LOG') && WP_DEBUG_LOG ) ? 'On' : 'Off', 'ok' ); $row( 'Plugin Version', ACQUIS_LITE_VERSION, 'ok' ); $row( 'Database Version', get_option('acquis_db_version', 'not set'), get_option('acquis_db_version') === ACQUIS_LITE_VERSION ? 'ok' : 'warn', get_option('acquis_db_version') !== ACQUIS_LITE_VERSION ? 'Run the upgrade by deactivating and reactivating the plugin, or upgrading to the latest version.' : '' ); // ── File System ───────────────────────────────────────────────────── $section( esc_html__( 'File System & Protected Directory', 'acquisitionsuite' ) ); $upload_dir = wp_upload_dir(); $protect_dir = $upload_dir['basedir'] . '/acquis-protected'; $row( 'Protected Upload Directory', $protect_dir, 'ok' ); $row( 'Directory Exists', is_dir( $protect_dir ) ? 'Yes' : 'No', is_dir( $protect_dir ) ? 'ok' : 'fail', ! is_dir( $protect_dir ) ? 'Upload a file to create it.' : '' ); $row( 'Directory Writable', is_writable( $protect_dir ) ? 'Yes' : ( is_dir( $protect_dir ) ? 'No ACTION REQUIRED' : 'N/A' ), is_dir( $protect_dir ) ? ( is_writable( $protect_dir ) ? 'ok' : 'fail' ) : 'ok', ! is_writable( $protect_dir ) && is_dir( $protect_dir ) ? 'WordPress must be able to write to this directory to store gated files.' : '' ); $htaccess_ok = file_exists( $protect_dir . '/.htaccess' ); $row( '.htaccess Protection File', $htaccess_ok ? 'Present' : 'Missing', $htaccess_ok ? 'ok' : 'fail', ! $htaccess_ok ? 'Upload any file to regenerate it.' : '' ); // ── Database ─────────────────────────────────────────────────────── $section( esc_html__( 'Database Tables', 'acquisitionsuite' ) ); // Only check Lite-owned tables. Pro plugin adds its own table check // via the acquis_system_status_tables filter. $lite_tables = array( 'acquis_leads', 'acquis_tokens', 'acquis_email_logs', 'acquis_integration_logs', 'acquis_forms', 'acquis_form_fields', 'acquis_submissions', 'acquis_submission_values', 'acquis_analytics', ); $expected_tables = apply_filters( 'acquis_system_status_tables', $lite_tables ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery -- % appended after esc_like() so it is the LIKE wildcard, not a literal $existing_tables = $wpdb->get_col( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->esc_like( $wpdb->prefix . 'acquis_' ) . '%' ) ); $missing = array(); foreach ( $expected_tables as $t ) { if ( ! in_array( $wpdb->prefix . $t, $existing_tables, true ) ) { $missing[] = $t; } } $row( 'Expected Tables', count( $expected_tables ), 'ok' ); $row( 'Tables Found', count( $existing_tables ), count( $missing ) === 0 ? 'ok' : 'fail' ); if ( $missing ) { $row( 'Missing Tables', implode( ', ', $missing ), 'fail', 'Deactivate and reactivate the plugin to recreate missing tables.' ); } // Delay unit migration check $steps_table = $wpdb->prefix . 'acquis_sequence_steps'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- INFORMATION_SCHEMA is a system catalog; table name from $wpdb->prefix + hardcoded string $has_unit = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'delay_unit'", DB_NAME, $steps_table ) ); $row( 'Sequence Steps: delay_unit column', $has_unit ? 'Present' : 'Missing upgrade required', $has_unit ? 'ok' : 'warn', ! $has_unit ? 'Run maybe_upgrade() by deactivating and reactivating the plugin.' : '' ); // ── Cron ──────────────────────────────────────────────────────────── $section( esc_html__( 'Scheduled Tasks (Cron)', 'acquisitionsuite' ) ); $seq_hook = class_exists( 'ACQUIS_Sequences' ) ? ACQUIS_Sequences::CRON_HOOK : 'acquis_process_sequences'; $tb_hook = class_exists( 'ACQUIS_Time_Based_Scheduler' ) ? ACQUIS_Time_Based_Scheduler::CRON_HOOK : 'acquis_process_time_based_rules'; $seq_next = wp_next_scheduled( $seq_hook ); $tb_next = wp_next_scheduled( $tb_hook ); $row( 'Email Sequences Cron', $seq_next ? 'Scheduled (next: ' . date_i18n( 'Y-m-d H:i:s', $seq_next ) . ')' : 'NOT SCHEDULED', $seq_next ? 'ok' : 'fail', ! $seq_next ? 'Re-activate the plugin to reschedule.' : '' ); $row( 'Time-Based Automation Cron', $tb_next ? 'Scheduled (next: ' . date_i18n( 'Y-m-d H:i:s', $tb_next ) . ')' : 'NOT SCHEDULED', $tb_next ? 'ok' : 'fail', ! $tb_next ? 'Re-activate the plugin to reschedule.' : '' ); // ── Payment Gateways ─────────────────────────────────────────────── $section( esc_html__( 'Payment Gateways', 'acquisitionsuite' ) ); if ( class_exists( 'ACQUIS_Pay_Gateway_Registry' ) ) { foreach ( ACQUIS_Pay_Gateway_Registry::get_all() as $slug => $gateway ) { $configured = $gateway->is_configured(); $row( $gateway->get_label() . ' (' . $slug . ')', $configured ? 'Configured' : 'Not configured', $configured ? 'ok' : 'warn', $configured ? '' : 'Add API credentials under Payments → Settings.' ); } } else { $row( 'Gateway Registry', 'Unavailable', 'fail' ); } $currency = get_option( 'acquis_pay_default_currency', 'USD' ); $symbol = class_exists( 'ACQUIS_Pay_DB' ) ? ACQUIS_Pay_DB::get_currency_symbol( $currency ) : $currency; $row( 'Active Currency', $currency . ' (' . $symbol . ')', 'ok' ); // ── Email ────────────────────────────────────────────────────────── $section( esc_html__( 'Email', 'acquisitionsuite' ) ); $from_name = get_option( 'acquis_email_from_name', '' ); $from_address = get_option( 'acquis_email_from_address', '' ); $row( 'From Name', $from_name ?: esc_html__( '(using site name)', 'acquisitionsuite' ), $from_name ? 'ok' : 'warn' ); $row( 'From Address', $from_address ?: esc_html__( '(using admin email)', 'acquisitionsuite' ), ( $from_address && is_email( $from_address ) ) ? 'ok' : 'warn', ( $from_address && ! is_email( $from_address ) ) ? 'Invalid email address.' : '' ); // Detect SMTP / transactional mail plugin checks common plugins including AWS SES $smtp_plugin_name = ''; if ( defined( 'WPMS_PLUGIN_VER' ) || class_exists( 'WPMailSMTP\Core' ) || function_exists( 'wp_mail_smtp' ) ) { $smtp_plugin_name = 'WP Mail SMTP'; } elseif ( class_exists( 'WP_Offload_SES' ) || class_exists( 'WP_Offload_SES_Lite' ) ) { $smtp_plugin_name = 'WP Offload SES (AWS SES)'; } elseif ( get_option( 'wpses_settings' ) || class_exists( 'WP_SES' ) ) { $smtp_plugin_name = 'WP SES (AWS SES)'; } elseif ( defined( 'FLUENT_MAIL_PLUGIN_FILE' ) ) { $smtp_plugin_name = 'FluentSMTP'; } elseif ( class_exists( 'Postmark_Mail' ) || defined( 'POSTMARK_API_KEY' ) ) { $smtp_plugin_name = 'Postmark'; } elseif ( function_exists( 'mailgun_init' ) || get_option( 'mailgun' ) ) { $smtp_plugin_name = 'Mailgun'; } elseif ( defined( 'SENDGRID_API_KEY' ) ) { $smtp_plugin_name = 'SendGrid'; } elseif ( defined( 'SENDINBLUE_SMTP' ) ) { $smtp_plugin_name = 'Brevo / Sendinblue'; } elseif ( has_action( 'phpmailer_init' ) ) { $smtp_plugin_name = 'Custom SMTP handler detected'; } $smtp_detected = ! empty( $smtp_plugin_name ); $row( 'SMTP / Mail Plugin', $smtp_detected ? $smtp_plugin_name : 'None detected', $smtp_detected ? 'ok' : 'warn', ! $smtp_detected ? 'No SMTP plugin found. Without one, emails likely fail silently. Install WP Offload SES, WP Mail SMTP, or FluentSMTP.' : '' ); ?>