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' ) ); ?>
| |
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 ); ?>
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 ) : ?>
admin_header( esc_html__( 'Leads', 'acquisitionsuite' ), "
" . number_format( $total ) . " leads" ); ?>
' . 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 ); ?>
admin_header( esc_html__( 'Email Logs', 'acquisitionsuite' ) ); ?>
'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 ); ?>
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 ) );
?>
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' ) ); ?>