Edit

# Check if zone has Cloudflare marker
sub whm_has_cf_marker {
    my ($domain) = @_;
    my $result = `dig +short TXT _cloudflare.$domain \@localhost 2>/dev/null`;
    return $result =~ /cloudflare-dns/;
}

# Check if zone has Cloudflare marker
sub whm_has_cf_marker {
    my ($domain) = @_;
    my $result = `dig +short TXT _cloudflare.$domain \@localhost 2>/dev/null`;
    return $result =~ /cloudflare-dns/;
}

# Parse BIND zone file format (from paste or file)
sub parse_zone_file {
    my ($zone_text, $domain) = @_;

    my @records;
    my $origin = $domain;
    my $default_ttl = 3600;

    for my $line (split /\n/, $zone_text) {
        chomp $line;
        $line =~ s/\s+$//;  # Trim trailing whitespace

        next if $line =~ /^\s*$/;      # Skip empty lines
        next if $line =~ /^\s*;/;      # Skip comments
        next if $line =~ /^;/;         # Skip comments
        next if $line =~ /SOA/i;       # Skip SOA records

        # Handle $ORIGIN directive
        if ($line =~ /^\$ORIGIN\s+(\S+)/i) {
            $origin = $1;
            $origin =~ s/\.$//;
            next;
        }

        # Handle $TTL directive
        if ($line =~ /^\$TTL\s+(\d+)/i) {
            $default_ttl = $1;
            next;
        }

        # Parse record: name [ttl] [class] type content
        # Supports formats like:
        #   @  3600  IN  A  1.2.3.4
        #   www      IN  CNAME  @
        #   mail  600  IN  MX  10 mailserver.com.

        if ($line =~ /^(\S+)\s+(?:(\d+)\s+)?(?:IN\s+)?(A|AAAA|CNAME|TXT|MX|SRV|NS|CAA)\s+(.+)$/i) {
            my ($name, $ttl, $type, $content) = ($1, $2, uc($3), $4);
            $ttl ||= $default_ttl;

            # Normalize name
            if ($name eq '@') {
                $name = $domain;
            } elsif ($name !~ /\.$/ && $name !~ /\Q$domain\E$/) {
                $name = "$name.$domain";
            }
            $name =~ s/\.$//;

            # Clean content
            $content =~ s/\s+$//;
            $content =~ s/\.$// if $type =~ /^(CNAME|MX|NS|SRV)$/;

            # Handle TXT records with quotes
            if ($type eq 'TXT') {
                $content =~ s/^"//;
                $content =~ s/"$//;
                # Handle multi-part TXT records
                $content =~ s/"\s+"//g;
            }

            # Skip NS records for domain root (Cloudflare manages these)
            next if $type eq 'NS' && $name eq $domain;

            # Skip "Parked" A records
            next if $type eq 'A' && $content =~ /parked/i;

            push @records, {
                name => $name,
                type => $type,
                content => $content,
                ttl => int($ttl)
            };
        }
    }

    return \@records;
}

# Scan external DNS for a domain
sub scan_external_dns {
    my ($domain) = @_;

    my @records;
    my @types = qw(A AAAA CNAME MX TXT SRV);

    # Common subdomains to check
    my @subdomains = (
        '',           # root
        'www',
        'mail',
        'ftp',
        'webmail',
        'cpanel',
        'whm',
        'autodiscover',
        'autoconfig',
        '_dmarc',
        'selector1._domainkey',
        'selector2._domainkey',
        'enterpriseenrollment',
        'enterpriseregistration',
        'lyncdiscover',
        'sip',
        '_sip._tls',
        '_sipfederationtls._tcp',
    );

    for my $sub (@subdomains) {
        my $fqdn = $sub ? "$sub.$domain" : $domain;

        for my $type (@types) {
            # Skip SRV for non-SRV names
            next if $type eq 'SRV' && $sub !~ /^_/;
            # Only check SRV for SRV-style names
            next if $type ne 'SRV' && $sub =~ /^_.*\._/;

            my $result;
            if ($type eq 'SRV') {
                $result = `dig +short SRV $fqdn 2>/dev/null`;
            } else {
                $result = `dig +short $type $fqdn 2>/dev/null`;
            }

            next unless $result;
            chomp $result;

            for my $line (split /\n/, $result) {
                next unless $line;
                next if $line =~ /^;;/;  # Skip dig comments

                my $content = $line;
                $content =~ s/\.$//;  # Remove trailing dot

                # Clean TXT records
                if ($type eq 'TXT') {
                    $content =~ s/^"//;
                    $content =~ s/"$//;
                }

                # Skip Cloudflare IPs if proxied
                next if $type eq 'A' && $content =~ /^(104\.1[6-9]\.|104\.2[0-9]\.|172\.6[4-9]\.|172\.7[0-1]\.)/;

                push @records, {
                    name => $fqdn,
                    type => $type,
                    content => $content,
                    ttl => 3600
                };
            }
        }
    }

    return \@records;
}


The file C:/Users/MikeSwanson/claude-projects/cloudflare-whm/whm-plugin/cgi/addon_cloudflareDNS.cgi has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   146→
   147→    return \@zones;
   148→}
   149→
   150→# Check if zone has Cloudflare marker
   151→sub whm_has_cf_marker {
   152→    my ($domain) = @_;
   153→    my $result = `dig +short TXT _cloudflare.$domain \@localhost 2>/dev/null`;
   154→    return $result =~ /cloudflare-dns/;
   155→}
   156→
   157→# Parse BIND zone file format (from paste or file)
   158→sub parse_zone_file {
   159→    my ($zone_text, $domain) = @_;
   160→
   161→    my @records;
   162→    my $origin = $domain;
   163→    my $default_ttl = 3600;
   164→
   165→    for my $line (split /\n/, $zone_text) {
   166→        chomp $line;
   167→        $line =~ s/\s+$//;  # Trim trailing whitespace
   168→
   169→        next if $line =~ /^\s*$/;      # Skip empty lines
   170→        next if $line =~ /^\s*;/;      # Skip comments
   171→        next if $line =~ /^;/;         # Skip comments
   172→        next if $line =~ /SOA/i;       # Skip SOA records
   173→
   174→        # Handle $ORIGIN directive
   175→        if ($line =~ /^\$ORIGIN\s+(\S+)/i) {
   176→            $origin = $1;
   177→            $origin =~ s/\.$//;
   178→            next;
   179→        }
   180→
   181→        # Handle $TTL directive
   182→        if ($line =~ /^\$TTL\s+(\d+)/i) {
   183→            $default_ttl = $1;
   184→            next;
   185→        }
   186→
   187→        # Parse record: name [ttl] [class] type content
   188→        # Supports formats like:
   189→        #   @  3600  IN  A  1.2.3.4
   190→        #   www      IN  CNAME  @
   191→        #   mail  600  IN  MX  10 mailserver.com.
   192→
   193→        if ($line =~ /^(\S+)\s+(?:(\d+)\s+)?(?:IN\s+)?(A|AAAA|CNAME|TXT|MX|SRV|NS|CAA)\s+(.+)$/i) {
   194→            my ($name, $ttl, $type, $content) = ($1, $2, uc($3), $4);
   195→            $ttl ||= $default_ttl;
   196→
   197→            # Normalize name
   198→            if ($name eq '@') {
   199→                $name = $domain;
   200→            } elsif ($name !~ /\.$/ && $name !~ /\Q$domain\E$/) {
   201→                $name = "$name.$domain";
   202→            }
   203→            $name =~ s/\.$//;
   204→
   205→            # Clean content
   206→            $content =~ s/\s+$//;
   207→            $content =~ s/\.$// if $type =~ /^(CNAME|MX|NS|SRV)$/;
   208→
   209→            # Handle TXT records with quotes
   210→            if ($type eq 'TXT') {
   211→                $content =~ s/^"//;
   212→                $content =~ s/"$//;
   213→                # Handle multi-part TXT records
   214→                $content =~ s/"\s+"//g;
   215→            }
   216→
   217→            # Skip NS records for domain root (Cloudflare manages these)
   218→            next if $type eq 'NS' && $name eq $domain;
   219→
   220→            # Skip "Parked" A records
   221→            next if $type eq 'A' && $content =~ /parked/i;
   222→
   223→            push @records, {
   224→                name => $name,
   225→                type => $type,
   226→                content => $content,
   227→                ttl => int($ttl)
   228→            };
   229→        }
   230→    }
   231→
   232→    return \@records;
   233→}
   234→
   235→# Scan external DNS for a domain
   236→sub scan_external_dns {
   237→    my ($domain) = @_;
   238→
   239→    my @records;
   240→    my @types = qw(A AAAA CNAME MX TXT SRV);
   241→
   242→    # Common subdomains to check
   243→    my @subdomains = (
   244→        '',           # root
   245→        'www',
   246→        'mail',
   247→        'ftp',
   248→        'webmail',
   249→        'cpanel',
   250→        'whm',
   251→        'autodiscover',
   252→        'autoconfig',
   253→        '_dmarc',
   254→        'selector1._domainkey',
   255→        'selector2._domainkey',
   256→        'enterpriseenrollment',
   257→        'enterpriseregistration',
   258→        'lyncdiscover',
   259→        'sip',
   260→        '_sip._tls',
   261→        '_sipfederationtls._tcp',
   262→    );
   263→
   264→    for my $sub (@subdomains) {
   265→        my $fqdn = $sub ? "$sub.$domain" : $domain;
   266→
   267→        for my $type (@types) {
   268→            # Skip SRV for non-SRV names
   269→            next if $type eq 'SRV' && $sub !~ /^_/;
   270→            # Only check SRV for SRV-style names
   271→            next if $type ne 'SRV' && $sub =~ /^_.*\._/;
   272→
   273→            my $result;
   274→            if ($type eq 'SRV') {
   275→                $result = `dig +short SRV $fqdn 2>/dev/null`;
   276→            } else {
   277→                $result = `dig +short $type $fqdn 2>/dev/null`;
   278→            }
   279→
   280→            next unless $result;
   281→            chomp $result;
   282→
   283→            for my $line (split /\n/, $result) {
   284→                next unless $line;
   285→                next if $line =~ /^;;/;  # Skip dig comments
   286→
   287→                my $content = $line;
   288→                $content =~ s/\.$//;  # Remove trailing dot
   289→
   290→                # Clean TXT records
   291→                if ($type eq 'TXT') {
   292→                    $content =~ s/^"//;
   293→                    $content =~ s/"$//;
   294→                }
   295→
   296→                # Skip Cloudflare IPs if proxied
   297→                next if $type eq 'A' && $content =~ /^(104\.1[6-9]\.|104\.2[0-9]\.|172\.6[4-9]\.|172\.7[0-1]\.)/;
   298→
   299→                push @records, {
   300→                    name => $fqdn,
   301→                    type => $type,
   302→                    content => $content,
   303→                    ttl => 3600
   304→                };
   305→            }
   306→        }
   307→    }
   308→
   309→    return \@records;
   310→}
   311→
   312→# Main
   313→my $cgi = CGI->new;
   314→my $action = $cgi->param('action') || 'home';

# Check if zone has Cloudflare marker
sub whm_has_cf_marker {
    my ($domain) = @_;
    my $result = `dig +short TXT _cloudflare.$domain \@localhost 2>/dev/null`;
    return $result =~ /cloudflare-dns/;
}

# Parse BIND zone file format (from paste or file)
sub parse_zone_file {
    my ($zone_text, $domain) = @_;

    my @records;
    my $origin = $domain;
    my $default_ttl = 3600;

    for my $line (split /\n/, $zone_text) {
        chomp $line;
        $line =~ s/\s+$//;  # Trim trailing whitespace

        next if $line =~ /^\s*$/;      # Skip empty lines
        next if $line =~ /^\s*;/;      # Skip comments
        next if $line =~ /^;/;         # Skip comments
        next if $line =~ /SOA/i;       # Skip SOA records

        # Handle $ORIGIN directive
        if ($line =~ /^\$ORIGIN\s+(\S+)/i) {
            $origin = $1;
            $origin =~ s/\.$//;
            next;
        }

        # Handle $TTL directive
        if ($line =~ /^\$TTL\s+(\d+)/i) {
            $default_ttl = $1;
            next;
        }

        # Parse record: name [ttl] [class] type content
        # Supports formats like:
        #   @  3600  IN  A  1.2.3.4
        #   www      IN  CNAME  @
        #   mail  600  IN  MX  10 mailserver.com.

        if ($line =~ /^(\S+)\s+(?:(\d+)\s+)?(?:IN\s+)?(A|AAAA|CNAME|TXT|MX|SRV|NS|CAA)\s+(.+)$/i) {
            my ($name, $ttl, $type, $content) = ($1, $2, uc($3), $4);
            $ttl ||= $default_ttl;

            # Normalize name
            if ($name eq '@') {
                $name = $domain;
            } elsif ($name !~ /\.$/ && $name !~ /\Q$domain\E$/) {
                $name = "$name.$domain";
            }
            $name =~ s/\.$//;

            # Clean content
            $content =~ s/\s+$//;
            $content =~ s/\.$// if $type =~ /^(CNAME|MX|NS|SRV)$/;

            # Handle TXT records with quotes
            if ($type eq 'TXT') {
                $content =~ s/^"//;
                $content =~ s/"$//;
                # Handle multi-part TXT records
                $content =~ s/"\s+"//g;
            }

            # Skip NS records for domain root (Cloudflare manages these)
            next if $type eq 'NS' && $name eq $domain;

            # Skip "Parked" A records
            next if $type eq 'A' && $content =~ /parked/i;

            push @records, {
                name => $name,
                type => $type,
                content => $content,
                ttl => int($ttl)
            };
        }
    }

    return \@records;
}

# Scan external DNS for a domain
sub scan_external_dns {
    my ($domain) = @_;

    my @records;
    my @types = qw(A AAAA CNAME MX TXT SRV);

    # Common subdomains to check
    my @subdomains = (
        '',           # root
        'www',
        'mail',
        'ftp',
        'webmail',
        'cpanel',
        'whm',
        'autodiscover',
        'autoconfig',
        '_dmarc',
        'selector1._domainkey',
        'selector2._domainkey',
        'enterpriseenrollment',
        'enterpriseregistration',
        'lyncdiscover',
        'sip',
        '_sip._tls',
        '_sipfederationtls._tcp',
    );

    for my $sub (@subdomains) {
        my $fqdn = $sub ? "$sub.$domain" : $domain;

        for my $type (@types) {
            # Skip SRV for non-SRV names
            next if $type eq 'SRV' && $sub !~ /^_/;
            # Only check SRV for SRV-style names
            next if $type ne 'SRV' && $sub =~ /^_.*\._/;

            my $result;
            if ($type eq 'SRV') {
                $result = `dig +short SRV $fqdn 2>/dev/null`;
            } else {
                $result = `dig +short $type $fqdn 2>/dev/null`;
            }

            next unless $result;
            chomp $result;

            for my $line (split /\n/, $result) {
                next unless $line;
                next if $line =~ /^;;/;  # Skip dig comments

                my $content = $line;
                $content =~ s/\.$//;  # Remove trailing dot

                # Clean TXT records
                if ($type eq 'TXT') {
                    $content =~ s/^"//;
                    $content =~ s/"$//;
                }

                # Skip Cloudflare IPs if proxied
                next if $type eq 'A' && $content =~ /^(104\.1[6-9]\.|104\.2[0-9]\.|172\.6[4-9]\.|172\.7[0-1]\.)/;

                push @records, {
                    name => $fqdn,
                    type => $type,
                    content => $content,
                    ttl => 3600
                };
            }
        }
    }

    return \@records;
}

#!/usr/local/cpanel/3rdparty/bin/perl
#
# Cloudflare DNS Manager - WHM Plugin v1.1
# Features: Zone transfer, mass delete, record editing
#

use strict;
use warnings;

BEGIN {
    unshift @INC, '/usr/local/cpanel/3rdparty/perl/542/cpanel-lib';
    unshift @INC, '/var/cpanel/plugins/cloudflare_dns/lib';
}

use CGI;
use JSON;
use LWP::UserAgent;
use HTTP::Request;

# Config file path
my $CONFIG_FILE = '/var/cpanel/plugins/cloudflare_dns/config.json';

# Load or initialize config
sub load_config {
    if (-f $CONFIG_FILE) {
        open my $fh, '<', $CONFIG_FILE or return {};
        local $/;
        my $json = <$fh>;
        close $fh;
        return eval { decode_json($json) } || {};
    }
    return {};
}

sub save_config {
    my ($config) = @_;
    open my $fh, '>', $CONFIG_FILE or die "Cannot write config: $!";
    print $fh encode_json($config);
    close $fh;
    chmod 0600, $CONFIG_FILE;
}

# Cloudflare API
sub cf_api {
    my ($token, $method, $endpoint, $data) = @_;

    my $ua = LWP::UserAgent->new(timeout => 30);
    my $url = "https://api.cloudflare.com/client/v4" . $endpoint;

    my $req = HTTP::Request->new($method => $url);
    $req->header('Authorization' => "Bearer $token");
    $req->header('Content-Type' => 'application/json');

    if ($data) {
        $req->content(encode_json($data));
    }

    my $res = $ua->request($req);

    if ($res->is_success) {
        return decode_json($res->content);
    } else {
        return { success => 0, errors => [{ message => $res->status_line }] };
    }
}

# WHM API - Get zone from local DNS
sub whm_get_zone {
    my ($domain) = @_;

    # Use cPanel's DNS functions
    my $zonefile = "/var/named/$domain.db";
    return undef unless -f $zonefile;

    open my $fh, '<', $zonefile or return undef;
    my @lines = <$fh>;
    close $fh;

    my @records;
    my $origin = $domain;

    for my $line (@lines) {
        chomp $line;
        next if $line =~ /^\s*;/;  # Skip comments
        next if $line =~ /^\s*$/;  # Skip empty lines
        next if $line =~ /^\$TTL/; # Skip TTL directive

        if ($line =~ /^\$ORIGIN\s+(\S+)/) {
            $origin = $1;
            $origin =~ s/\.$//;
            next;
        }

        # Parse DNS records: name ttl class type content
        if ($line =~ /^(\S+)\s+(\d+)?\s*IN\s+(A|AAAA|CNAME|TXT|MX|SRV|NS)\s+(.+)$/i) {
            my ($name, $ttl, $type, $content) = ($1, $2 || 3600, uc($3), $4);

            # Normalize name
            $name = $origin if $name eq '@';
            $name = "$name.$origin" unless $name =~ /\.$/;
            $name =~ s/\.$//;

            # Clean content
            $content =~ s/\s+$//;
            $content =~ s/^"(.*)"$/$1/ if $type eq 'TXT';

            # Skip NS records for the domain itself (Cloudflare manages these)
            next if $type eq 'NS' && $name eq $domain;

            push @records, {
                name => $name,
                type => $type,
                content => $content,
                ttl => int($ttl)
            };
        }
    }

    return \@records;
}

# WHM API - Remove zone or add marker
sub whm_mark_zone_transferred {
    my ($domain, $remove) = @_;

    if ($remove) {
        # Use WHM API to remove zone
        my $result = `whmapi1 killdns domain=$domain 2>&1`;
        return $result !~ /error/i;
    } else {
        # Add marker TXT record
        my $marker = "v=cloudflare-dns; managed=true; transferred=" . time();
        system("whmapi1 addzonerecord domain=$domain name=_cloudflare type=TXT txtdata=\"$marker\" 2>&1");
        return 1;
    }
}

# Get list of WHM zones
sub whm_list_zones {
    my $result = `whmapi1 listzones 2>&1`;
    my @zones;

    while ($result =~ /domain:\s*(\S+)/g) {
        push @zones, $1;
    }

    return \@zones;
}

# Check if zone has Cloudflare marker
sub whm_has_cf_marker {
    my ($domain) = @_;
    my $result = `dig +short TXT _cloudflare.$domain \@localhost 2>/dev/null`;
    return $result =~ /cloudflare-dns/;
}

# Main
my $cgi = CGI->new;
my $action = $cgi->param('action') || 'home';

# Print header
print $cgi->header('text/html');

# Load config
my $config = load_config();
my $api_token = $config->{api_token} || '';

# HTML header
print_html_header();

# Route actions
if ($action eq 'home') {
    show_home($cgi, $config, $api_token);
}
elsif ($action eq 'save_token') {
    save_token($cgi, $config);
}
elsif ($action eq 'list_zones') {
    list_zones($api_token);
}
elsif ($action eq 'view_zone') {
    view_zone($cgi, $api_token);
}
elsif ($action eq 'add_record') {
    add_record($cgi, $api_token);
}
elsif ($action eq 'edit_record') {
    edit_record($cgi, $api_token);
}
elsif ($action eq 'save_record') {
    save_record($cgi, $api_token);
}
elsif ($action eq 'delete_record') {
    delete_record($cgi, $api_token);
}
elsif ($action eq 'mass_delete') {
    mass_delete($cgi, $api_token);
}
elsif ($action eq 'add_m365') {
    add_m365($cgi, $api_token);
}
elsif ($action eq 'import_zone') {
    import_zone($cgi, $api_token);
}
elsif ($action eq 'transfer_zone') {
    transfer_zone($cgi, $api_token);
}
elsif ($action eq 'do_transfer') {
    do_transfer($cgi, $api_token);
}
elsif ($action eq 'whm_zones') {
    whm_zones($cgi, $api_token);
}
else {
    print "
Unknown action: $action
";
}

print_html_footer();

# ============ Actions ============

sub show_home {
    my ($cgi, $config, $token) = @_;

    print '
Cloudflare DNS Manager
';

    if (!$token) {
        print '
';
        print 'Setup Required: Please configure your Cloudflare API token.';
        print '
';
        show_token_form($config);
        return;
    }

    my $verify = cf_api($token, 'GET', '/user/tokens/verify');
    if (!$verify->{success}) {
        print '
API Token is invalid or expired.
';
        show_token_form($config);
        return;
    }

    print '
✓ Connected to Cloudflare
';

    print '
';
    print 'Cloudflare Zones ';
    print 'WHM Zones ';
    print 'Import New Zone ';
    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
    print '
';

    print '
';
    show_token_form($config);
    print '
';

    # Quick stats
    my $cf_zones = cf_api($token, 'GET', '/zones?per_page=50');
    if ($cf_zones->{success}) {
        my $active = scalar grep { $_->{status} eq 'active' } @{$cf_zones->{result}};
        my $pending = scalar grep { $_->{status} eq 'pending' } @{$cf_zones->{result}};
        print '
';
        print "
Cloudflare Zones: $active active, $pending pending
";
        print '
';
    }
}

sub show_token_form {
    my ($config) = @_;
    my $current = $config->{api_token} ? '********' . substr($config->{api_token}, -4) : 'Not set';

    print '<form method="post" action="?action=save_token" style="margin: 20px 0; padding: 20px; background: #f5f5f5; border-radius: 5px;">';
    print '
Cloudflare API Token
';
    print "
Current: $current
";
    print '
';
    print 'API Token:
';
    print '<input type="password" name="api_token" class="form-control" style="width: 400px;" placeholder="Enter Cloudflare API token">';
    print '
';
    print '<button type="submit" class="btn btn-success">Save Token</button>';
    print '
Required: Zone:Read, Zone:Edit, DNS:Read, DNS:Edit
';
    print '</form>';
}

sub save_token {
    my ($cgi, $config) = @_;
    my $token = $cgi->param('api_token');

    if ($token) {
        $config->{api_token} = $token;
        save_config($config);
        print '
API Token saved successfully.
';
        print '
Return to Home
';
    } else {
        print '
No token provided.
';
    }
}

sub list_zones {
    my ($token) = @_;

    print '
Cloudflare Zones
';
    print '
← Back
';

    my $result = cf_api($token, 'GET', '/zones?per_page=50');

    if (!$result->{success}) {
        print '
Failed to load zones
';
        return;
    }

    print '
';
    print '

Domain	Status	Plan	Actions

';
    print '
';

    for my $zone (sort { $a->{name} cmp $b->{name} } @{$result->{result}}) {
        my $status_class = $zone->{status} eq 'active' ? 'success' : 'warning';
        print "
";
        print "$zone->{name}	";
        print "$zone->{status}	";
        print "$zone->{plan}{name}	";
        print "";
        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-primary'>DNS ";
        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-info'>+M365";
        print "	";
        print "
";
    }

    print '

';
}

# NEW: WHM Zones list with transfer option
sub whm_zones {
    my ($cgi, $token) = @_;

    print '
WHM DNS Zones
';
    print '
← Back
';
    print '
Transfer zones from WHM to Cloudflare for centralized DNS management.
';

    my $zones = whm_list_zones();

    # Get Cloudflare zones for comparison
    my $cf_result = cf_api($token, 'GET', '/zones?per_page=100');
    my %cf_zones;
    if ($cf_result->{success}) {
        $cf_zones{$_->{name}} = $_->{status} for @{$cf_result->{result}};
    }

    print '
';
    print '

Domain	WHM Status	Cloudflare Status	Actions

';
    print '
';

    for my $domain (sort @$zones) {
        my $has_marker = whm_has_cf_marker($domain);
        my $cf_status = $cf_zones{$domain} || 'Not in Cloudflare';

        my $whm_status = $has_marker ? 'Transferred' : 'Active';
        my $cf_label = $cf_status eq 'active' ? 'Active' :
                       $cf_status eq 'pending' ? 'Pending' :
                       '-';

        print "
";
        print "$domain	";
        print "$whm_status	";
        print "$cf_label	";
        print "";

        if (!exists $cf_zones{$domain}) {
            print "Transfer to CF";
        } elsif ($cf_status eq 'active' && !$has_marker) {
            print "Mark Transferred";
        } else {
            print "-";
        }

        print "	";
        print "
";
    }

    print '

';
}

# NEW: Transfer zone from WHM to Cloudflare
sub transfer_zone {
    my ($cgi, $token) = @_;

    my $domain = $cgi->param('domain');
    my $mark_only = $cgi->param('mark_only');

    print "
Transfer Zone: $domain
";
    print '
← Back to WHM Zones
';

    if ($mark_only) {
        # Just mark it, zone already in Cloudflare
        print '
Zone already exists in Cloudflare. Choose how to handle WHM zone:
';
    } else {
        # Preview records to transfer
        my $records = whm_get_zone($domain);

        if (!$records || !@$records) {
            print '
Could not read zone file or no records found.
';
            return;
        }

        print '
Records to Transfer
';
        print '
';
        print '

Type	Name	Content	TTL

';
        print '
';

        for my $rec (@$records) {
            my $content = length($rec->{content}) > 60 ? substr($rec->{content}, 0, 57) . '...' : $rec->{content};
            print "
$rec->{type}	$rec->{name}	$content	$rec->{ttl}
";
        }

        print '

';
        print "
Total: " . scalar(@$records) . " records
";
    }

    print '<form method="post" action="?action=do_transfer" style="margin-top: 20px; padding: 20px; background: #f5f5f5; border-radius: 5px;">';
    print "<input type='hidden' name='domain' value='$domain'>";
    print "<input type='hidden' name='mark_only' value='$mark_only'>";

    print '
After Transfer Options
';
    print '
';
    print '<input type="radio" name="whm_action" value="marker" checked> Add marker record (keep zone in WHM, add _cloudflare TXT)
';
    print '<input type="radio" name="whm_action" value="remove"> Remove zone from WHM (delete local zone file)
';
    print '<input type="radio" name="whm_action" value="none"> Do nothing (keep both)';
    print '
';

    unless ($mark_only) {
        print '
';
        print 'Important: After transfer, you must update nameservers at the registrar to point to Cloudflare.';
        print '
';
    }

    print '<button type="submit" class="btn btn-success">Proceed with Transfer</button>';
    print ' Cancel';
    print '</form>';
}

# NEW: Execute zone transfer
sub do_transfer {
    my ($cgi, $token) = @_;

    my $domain = $cgi->param('domain');
    my $mark_only = $cgi->param('mark_only');
    my $whm_action = $cgi->param('whm_action') || 'marker';

    print "
Transferring: $domain
";

    my $zone_id;
    my @results;

    unless ($mark_only) {
        # Step 1: Create zone in Cloudflare
        print '
Step 1: Create Zone in Cloudflare
';
        my $create_result = cf_api($token, 'POST', '/zones', { name => $domain, jump_start => \0 });

        if ($create_result->{success}) {
            $zone_id = $create_result->{result}{id};
            print "
Zone created! ID: $zone_id
";
            print '
Nameservers:

';
            print "
$_
" for @{$create_result->{result}{name_servers}};
            print '
';
        } elsif ($create_result->{errors}[0]{code} == 1061) {
            # Zone already exists
            my $existing = cf_api($token, 'GET', "/zones?name=$domain");
            if ($existing->{success} && @{$existing->{result}}) {
                $zone_id = $existing->{result}[0]{id};
                print "
Zone already exists in Cloudflare. ID: $zone_id
";
            }
        } else {
            print "
Failed: $create_result->{errors}[0]{message}
";
            return;
        }

        # Step 2: Import records
        print '
Step 2: Import DNS Records
';
        my $records = whm_get_zone($domain);

        print '
';
        print '

Record	Status

';
        print '
';

        for my $rec (@$records) {
            my $data = {
                type => $rec->{type},
                name => $rec->{name},
                content => $rec->{content},
                ttl => $rec->{ttl} > 0 ? $rec->{ttl} : 1
            };

            # Handle MX priority
            if ($rec->{type} eq 'MX' && $rec->{content} =~ /^(\d+)\s+(.+)/) {
                $data->{priority} = int($1);
                $data->{content} = $2;
            }

            my $result = cf_api($token, 'POST', "/zones/$zone_id/dns_records", $data);
            my $status = $result->{success} ? 'Added' : ($result->{errors}[0]{message} || 'Failed');
            my $class = $result->{success} ? 'success' : 'warning';

            print "
$rec->{type} $rec->{name}	$status
";
            push @results, { record => "$rec->{type} $rec->{name}", success => $result->{success} };
        }

        print '

';
    } else {
        print '
Skipped zone creation (already in Cloudflare)
';
    }

    # Step 3: Handle WHM zone
    print '
Step 3: WHM Zone Action
';

    if ($whm_action eq 'marker') {
        whm_mark_zone_transferred($domain, 0);
        print '
Added _cloudflare marker record to WHM zone.
';
    } elsif ($whm_action eq 'remove') {
        if (whm_mark_zone_transferred($domain, 1)) {
            print '
Removed zone from WHM.
';
        } else {
            print '
Could not remove zone from WHM (may require manual removal).
';
        }
    } else {
        print '
No changes made to WHM zone.
';
    }

    print '
';
    print 'Next Step: Update nameservers at your registrar to Cloudflare\'s nameservers.';
    print '
';

    print '
';
    print 'View Cloudflare Zones ';
    print 'View WHM Zones';
    print '
';
}

sub view_zone {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');

    print "
DNS Records: $domain
";
    print '
← Back to Zones
';

    # Add record form
    print '
';
    print '
Add Record
';
    print '<form method="post" action="?action=add_record">';
    print "<input type='hidden' name='zone_id' value='$zone_id'>";
    print "<input type='hidden' name='domain' value='$domain'>";
    print '<select name="type" class="form-control" style="width:100px; display:inline-block;">';
    print '<option>A</option><option>AAAA</option><option>CNAME</option><option>TXT</option><option>MX</option>';
    print '</select> ';
    print '<input type="text" name="name" placeholder="Name" class="form-control" style="width: 150px; display:inline-block;"> ';
    print '<input type="text" name="content" placeholder="Content" class="form-control" style="width: 300px; display:inline-block;"> ';
    print '<input type="checkbox" name="proxied"> Proxied ';
    print '<button type="submit" class="btn btn-success">Add</button>';
    print '</form>
';

    my $result = cf_api($token, 'GET', "/zones/$zone_id/dns_records?per_page=100");

    if (!$result->{success}) {
        print '
Failed to load records
';
        return;
    }

    # Mass delete form wrapper
    print '<form method="post" action="?action=mass_delete" id="mass-delete-form">';
    print "<input type='hidden' name='zone_id' value='$zone_id'>";
    print "<input type='hidden' name='domain' value='$domain'>";

    print '
';
    print '<button type="button" onclick="toggleAll()" class="btn btn-sm btn-secondary">Select All</button> ';
    print '<button type="submit" class="btn btn-sm btn-danger" onclick="return confirmMassDelete()">Delete Selected</button>';
    print '
';

    print '
';
    print '

<input type="checkbox" id="select-all" onclick="toggleAll()">	Type	Name	Content	TTL	Proxied	Actions

';
    print '
';

    for my $rec (sort { $a->{type} cmp $b->{type} || $a->{name} cmp $b->{name} } @{$result->{result}}) {
        my $content = length($rec->{content}) > 40 ? substr($rec->{content}, 0, 37) . '...' : $rec->{content};
        my $ttl = $rec->{ttl} == 1 ? 'Auto' : $rec->{ttl};
        my $proxied = $rec->{proxied} ? '✓' : '';

        print "
";
        print "<input type='checkbox' name='record_ids' value='$rec->{id}' class='record-checkbox'>	";
        print "$rec->{type}	";
        print "$rec->{name}	";
        print "{content}' style='font-size:12px;'>$content	";
        print "$ttl	";
        print "$proxied	";
        print "";
        print "{id}' class='btn btn-sm btn-info'>Edit ";
        print "{id}' class='btn btn-sm btn-danger' onclick='return confirm(\"Delete?\")'>Del";
        print "	";
        print "
";
    }

    print '

';
    print '</form>';

    # JavaScript for select all
    print <<'JS';

JS
}

# NEW: Edit record form
sub edit_record {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my $record_id = $cgi->param('record_id');

    # Get current record
    my $result = cf_api($token, 'GET', "/zones/$zone_id/dns_records/$record_id");

    if (!$result->{success}) {
        print '
Record not found
';
        return;
    }

    my $rec = $result->{result};

    print "
Edit Record
";
    print "
← Back to $domain
";

    print '<form method="post" action="?action=save_record" style="max-width: 600px;">';
    print "<input type='hidden' name='zone_id' value='$zone_id'>";
    print "<input type='hidden' name='domain' value='$domain'>";
    print "<input type='hidden' name='record_id' value='$record_id'>";
    print "<input type='hidden' name='type' value='$rec->{type}'>";

    print '
';
    print "Type: $rec->{type}";
    print '
';

    print '
';
    print 'Name:
';
    print "<input type='text' name='name' value='$rec->{name}' class='form-control' style='width:100%;'>";
    print '
';

    print '
';
    print 'Content:
';
    if ($rec->{type} eq 'TXT') {
        print "<textarea name='content' class='form-control' style='width:100%; height:100px;'>$rec->{content}</textarea>";
    } else {
        print "<input type='text' name='content' value='$rec->{content}' class='form-control' style='width:100%;'>";
    }
    print '
';

    print '
';
    print 'TTL:
';
    print '<select name="ttl" class="form-control" style="width:150px;">';
    my @ttls = (1, 60, 120, 300, 600, 900, 1800, 3600, 7200, 18000, 43200, 86400);
    my %ttl_labels = (1 => 'Auto', 60 => '1 min', 120 => '2 min', 300 => '5 min', 600 => '10 min',
                      900 => '15 min', 1800 => '30 min', 3600 => '1 hour', 7200 => '2 hours',
                      18000 => '5 hours', 43200 => '12 hours', 86400 => '1 day');
    for my $t (@ttls) {
        my $sel = $rec->{ttl} == $t ? 'selected' : '';
        print "<option value='$t' $sel>$ttl_labels{$t}</option>";
    }
    print '</select>';
    print '
';

    if ($rec->{type} =~ /^(A|AAAA|CNAME)$/) {
        my $checked = $rec->{proxied} ? 'checked' : '';
        print '
';
        print "<input type='checkbox' name='proxied' $checked> Proxied (orange cloud)";
        print '
';
    }

    print '<button type="submit" class="btn btn-success">Save Changes</button>';
    print " Cancel";
    print '</form>';
}

# NEW: Save edited record
sub save_record {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my $record_id = $cgi->param('record_id');
    my $type = $cgi->param('type');
    my $name = $cgi->param('name');
    my $content = $cgi->param('content');
    my $ttl = $cgi->param('ttl') || 1;
    my $proxied = $cgi->param('proxied') ? \1 : \0;

    my $data = {
        type => $type,
        name => $name,
        content => $content,
        ttl => int($ttl),
        proxied => $proxied
    };

    my $result = cf_api($token, 'PUT', "/zones/$zone_id/dns_records/$record_id", $data);

    if ($result->{success}) {
        print '
Record updated successfully.
';
    } else {
        my $err = $result->{errors}[0]{message} || 'Unknown error';
        print "
Failed: $err
";
    }

    print "
Return to DNS Records
";
}

# NEW: Mass delete records
sub mass_delete {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my @record_ids = $cgi->multi_param('record_ids');

    print "
Mass Delete Results
";

    if (!@record_ids) {
        print '
No records selected.
';
        print "
Back to DNS Records
";
        return;
    }

    print '
';
    print '

Record ID	Status

';
    print '
';

    my $success = 0;
    my $failed = 0;

    for my $record_id (@record_ids) {
        my $result = cf_api($token, 'DELETE', "/zones/$zone_id/dns_records/$record_id");

        if ($result->{success}) {
            print "
$record_id	Deleted
";
            $success++;
        } else {
            print "
$record_id	Failed
";
            $failed++;
        }
    }

    print '

';
    print "
Deleted: $success, Failed: $failed
";
    print "
Return to DNS Records
";
}

sub add_record {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my $type = $cgi->param('type');
    my $name = $cgi->param('name');
    my $content = $cgi->param('content');
    my $proxied = $cgi->param('proxied') ? \1 : \0;

    my $data = {
        type => $type,
        name => $name,
        content => $content,
        ttl => 1,
        proxied => $proxied
    };

    my $result = cf_api($token, 'POST', "/zones/$zone_id/dns_records", $data);

    if ($result->{success}) {
        print '
Record added successfully.
';
    } else {
        my $err = $result->{errors}[0]{message} || 'Unknown error';
        print "
Failed: $err
";
    }

    print "
Return to DNS Records
";
}

sub delete_record {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my $record_id = $cgi->param('record_id');

    my $result = cf_api($token, 'DELETE', "/zones/$zone_id/dns_records/$record_id");

    if ($result->{success}) {
        print '
Record deleted.
';
    } else {
        print '
Failed to delete record.
';
    }

    print "
Return to DNS Records
";
}

sub add_m365 {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my $tenant = $cgi->param('tenant');

    if (!$tenant) {
        print "
Add M365 Records: $domain
";
        print '
← Back
';

        print '<form method="post" action="?action=add_m365" style="max-width: 500px;">';
        print "<input type='hidden' name='zone_id' value='$zone_id'>";
        print "<input type='hidden' name='domain' value='$domain'>";
        print '
';
        print 'Microsoft 365 Tenant Name
';
        print '<input type="text" name="tenant" class="form-control" placeholder="e.g., contoso (from contoso.onmicrosoft.com)">';
        print '
Enter the prefix of your .onmicrosoft.com domain
';
        print '
';
        print '
Records that will be added:
';
        print '
';
        print '
MX → mail.protection.outlook.com
';
        print '
SPF (TXT) → include:spf.protection.outlook.com
';
        print '
DMARC (TXT) → v=DMARC1; p=reject
';
        print '
DKIM selector1 & selector2 (CNAME)
';
        print '
Autodiscover (CNAME)
';
        print '
';
        print '<button type="submit" class="btn btn-primary">Add M365 Records</button>';
        print '</form>';
        return;
    }

    print "
Adding M365 Records: $domain
";

    my $domain_clean = $domain;
    $domain_clean =~ s/\./-/g;

    my $tenant_clean = lc($tenant);
    $tenant_clean =~ s/\.//g;

    my @records = (
        { type => 'MX', name => $domain, content => "$domain.mail.protection.outlook.com", priority => 0 },
        { type => 'TXT', name => $domain, content => "v=spf1 include:spf.protection.outlook.com -all" },
        { type => 'TXT', name => "_dmarc.$domain", content => "v=DMARC1; p=reject; rua=mailto:admin\@$domain" },
        { type => 'CNAME', name => "selector1._domainkey.$domain", content => "selector1-${domain_clean}._domainkey.${tenant_clean}.onmicrosoft.com" },
        { type => 'CNAME', name => "selector2._domainkey.$domain", content => "selector2-${domain_clean}._domainkey.${tenant_clean}.onmicrosoft.com" },
        { type => 'CNAME', name => "autodiscover.$domain", content => 'autodiscover.outlook.com' },
    );

    print '
';
    print '

Record	Status

';
    print '
';

    for my $rec (@records) {
        my $result = cf_api($token, 'POST', "/zones/$zone_id/dns_records", $rec);
        my $status = $result->{success} ? 'Added' : ($result->{errors}[0]{message} || 'Failed');
        my $class = $result->{success} ? 'success' : 'warning';
        print "
$rec->{type} $rec->{name}	$status
";
    }

    print '

';
    print '
Remember to enable DKIM signing in Microsoft 365 Defender portal.
';
    print "
View DNS Records
";
}

sub import_zone {
    my ($cgi, $token) = @_;

    my $domain = $cgi->param('domain');

    if (!$domain) {
        print '
Import Zone to Cloudflare
';
        print '
← Back
';

        print '<form method="post" action="?action=import_zone" style="max-width: 400px;">';
        print '
';
        print 'Domain Name
';
        print '<input type="text" name="domain" class="form-control" placeholder="example.com">';
        print '
';
        print '<button type="submit" class="btn btn-primary">Import Zone</button>';
        print '</form>';
        return;
    }

    print "
Importing: $domain
";

    my $result = cf_api($token, 'POST', '/zones', { name => $domain, jump_start => \1 });

    if ($result->{success}) {
        my $zone = $result->{result};
        print '
Zone added successfully!
';
        print "
Zone ID: $zone->{id}
";
        print "
Status: $zone->{status}
";
        print '
Update Nameservers
';
        print '
Change nameservers at your registrar to:
';
        print '
';
        print "
$_
" for @{$zone->{name_servers}};
        print '
';
    } else {
        my $err = $result->{errors}[0]{message} || 'Unknown error';
        print "
Failed: $err
";
    }

    print '
View All Zones
';
}

# ============ HTML Templates ============

sub print_html_header {
    print <<'HTML';
<!DOCTYPE html>

    <title>Cloudflare DNS Manager</title>

HTML
}

sub print_html_footer {
    print <<'HTML';

Cloudflare DNS Manager v1.1 | AZ Computer Guru

HTML
}

+# Parse BIND zone file format (from paste or file)

+    for my $line (split /\n/, $zone_text) {

+        $line =~ s/\s+$//;  # Trim trailing whitespace

+        next if $line =~ /^\s*$/;      # Skip empty lines

+        next if $line =~ /^\s*;/;      # Skip comments

+        next if $line =~ /^;/;         # Skip comments

+        next if $line =~ /SOA/i;       # Skip SOA records

+        # Parse record: name [ttl] [class] type content

+        #   mail  600  IN  MX  10 mailserver.com.

+                $name = "$name.$domain";

+            $content =~ s/\.$// if $type =~ /^(CNAME|MX|NS|SRV)$/;

+            # Handle TXT records with quotes

+                # Handle multi-part TXT records

+            # Skip NS records for domain root (Cloudflare manages these)

+            next if $type eq 'NS' && $name eq $domain;

+            next if $type eq 'A' && $content =~ /parked/i;

+    my @types = qw(A AAAA CNAME MX TXT SRV);

+        my $fqdn = $sub ? "$sub.$domain" : $domain;

+            # Skip SRV for non-SRV names

+            next if $type eq 'SRV' && $sub !~ /^_/;

+            # Only check SRV for SRV-style names

+                $result = `dig +short SRV $fqdn 2>/dev/null`;

+                $result = `dig +short $type $fqdn 2>/dev/null`;

+            for my $line (split /\n/, $result) {

+                next if $line =~ /^;;/;  # Skip dig comments

+                $content =~ s/\.$//;  # Remove trailing dot

+                # Skip Cloudflare IPs if proxied

+                    content => $content,


Now let me add the new routes and UI for these features:


elsif ($action eq 'whm_zones') {
    whm_zones($cgi, $api_token);
}
else {
    print "
Unknown action: $action
";
}

elsif ($action eq 'whm_zones') {
    whm_zones($cgi, $api_token);
}
elsif ($action eq 'import_zonefile') {
    import_zonefile($cgi, $api_token);
}
elsif ($action eq 'do_zonefile_import') {
    do_zonefile_import($cgi, $api_token);
}
elsif ($action eq 'scan_dns') {
    scan_dns($cgi, $api_token);
}
elsif ($action eq 'do_scan_import') {
    do_scan_import($cgi, $api_token);
}
else {
    print "
Unknown action: $action
";
}


<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>

Error: File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.


360→elsif ($action eq 'transfer_zone') {
   361→    transfer_zone($cgi, $api_token);
   362→}
   363→elsif ($action eq 'do_transfer') {
   364→    do_transfer($cgi, $api_token);
   365→}
   366→elsif ($action eq 'whm_zones') {
   367→    whm_zones($cgi, $api_token);
   368→}
   369→else {
   370→    print "
Unknown action: $action
";
   371→}
   372→
   373→print_html_footer();
   374→
   375→# ============ Actions ============
   376→
   377→sub show_home {
   378→    my ($cgi, $config, $token) = @_;
   379→
   380→    print '
Cloudflare DNS Manager
';
   381→
   382→    if (!$token) {
   383→        print '
';
   384→        print 'Setup Required: Please configure your Cloudflare API token.';
   385→        print '
';
   386→        show_token_form($config);
   387→        return;
   388→    }
   389→
   390→    my $verify = cf_api($token, 'GET', '/user/tokens/verify');
   391→    if (!$verify->{success}) {
   392→        print '
API Token is invalid or expired.
';
   393→        show_token_form($config);
   394→        return;
   395→    }
   396→
   397→    print '
✓ Connected to Cloudflare
';
   398→
   399→    print '
';
   400→    print 'Cloudflare Zones ';
   401→    print 'WHM Zones ';
   402→    print 'Import New Zone ';
   403→    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
   404→    print '
';
   405→
   406→    print '
';
   407→    show_token_form($config);
   408→    print '
';
   409→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

elsif ($action eq 'transfer_zone') {
    transfer_zone($cgi, $api_token);
}
elsif ($action eq 'do_transfer') {
    do_transfer($cgi, $api_token);
}
elsif ($action eq 'whm_zones') {
    whm_zones($cgi, $api_token);
}
else {
    print "
Unknown action: $action
";
}

print_html_footer();

# ============ Actions ============

sub show_home {
    my ($cgi, $config, $token) = @_;

    print '
Cloudflare DNS Manager
';

    if (!$token) {
        print '
';
        print 'Setup Required: Please configure your Cloudflare API token.';
        print '
';
        show_token_form($config);
        return;
    }

    my $verify = cf_api($token, 'GET', '/user/tokens/verify');
    if (!$verify->{success}) {
        print '
API Token is invalid or expired.
';
        show_token_form($config);
        return;
    }

    print '
✓ Connected to Cloudflare
';

    print '
';
    print 'Cloudflare Zones ';
    print 'WHM Zones ';
    print 'Import New Zone ';
    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
    print '
';

    print '
';
    show_token_form($config);
    print '
';


elsif ($action eq 'whm_zones') {
    whm_zones($cgi, $api_token);
}
else {
    print "
Unknown action: $action
";
}

elsif ($action eq 'whm_zones') {
    whm_zones($cgi, $api_token);
}
elsif ($action eq 'import_zonefile') {
    import_zonefile($cgi, $api_token);
}
elsif ($action eq 'do_zonefile_import') {
    do_zonefile_import($cgi, $api_token);
}
elsif ($action eq 'scan_dns') {
    scan_dns($cgi, $api_token);
}
elsif ($action eq 'do_scan_import') {
    do_scan_import($cgi, $api_token);
}
else {
    print "
Unknown action: $action
";
}


The file C:/Users/MikeSwanson/claude-projects/cloudflare-whm/whm-plugin/cgi/addon_cloudflareDNS.cgi has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   362→}
   363→elsif ($action eq 'do_transfer') {
   364→    do_transfer($cgi, $api_token);
   365→}
   366→elsif ($action eq 'whm_zones') {
   367→    whm_zones($cgi, $api_token);
   368→}
   369→elsif ($action eq 'import_zonefile') {
   370→    import_zonefile($cgi, $api_token);
   371→}
   372→elsif ($action eq 'do_zonefile_import') {
   373→    do_zonefile_import($cgi, $api_token);
   374→}
   375→elsif ($action eq 'scan_dns') {
   376→    scan_dns($cgi, $api_token);
   377→}
   378→elsif ($action eq 'do_scan_import') {
   379→    do_scan_import($cgi, $api_token);
   380→}
   381→else {
   382→    print "
Unknown action: $action
";
   383→}
   384→
   385→print_html_footer();
   386→
   387→# ============ Actions ============

elsif ($action eq 'whm_zones') {
    whm_zones($cgi, $api_token);
}
elsif ($action eq 'import_zonefile') {
    import_zonefile($cgi, $api_token);
}
elsif ($action eq 'do_zonefile_import') {
    do_zonefile_import($cgi, $api_token);
}
elsif ($action eq 'scan_dns') {
    scan_dns($cgi, $api_token);
}
elsif ($action eq 'do_scan_import') {
    do_scan_import($cgi, $api_token);
}
else {
    print "
Unknown action: $action
";
}

#!/usr/local/cpanel/3rdparty/bin/perl
#
# Cloudflare DNS Manager - WHM Plugin v1.1
# Features: Zone transfer, mass delete, record editing
#

use strict;
use warnings;

BEGIN {
    unshift @INC, '/usr/local/cpanel/3rdparty/perl/542/cpanel-lib';
    unshift @INC, '/var/cpanel/plugins/cloudflare_dns/lib';
}

use CGI;
use JSON;
use LWP::UserAgent;
use HTTP::Request;

# Config file path
my $CONFIG_FILE = '/var/cpanel/plugins/cloudflare_dns/config.json';

# Load or initialize config
sub load_config {
    if (-f $CONFIG_FILE) {
        open my $fh, '<', $CONFIG_FILE or return {};
        local $/;
        my $json = <$fh>;
        close $fh;
        return eval { decode_json($json) } || {};
    }
    return {};
}

sub save_config {
    my ($config) = @_;
    open my $fh, '>', $CONFIG_FILE or die "Cannot write config: $!";
    print $fh encode_json($config);
    close $fh;
    chmod 0600, $CONFIG_FILE;
}

# Cloudflare API
sub cf_api {
    my ($token, $method, $endpoint, $data) = @_;

    my $ua = LWP::UserAgent->new(timeout => 30);
    my $url = "https://api.cloudflare.com/client/v4" . $endpoint;

    my $req = HTTP::Request->new($method => $url);
    $req->header('Authorization' => "Bearer $token");
    $req->header('Content-Type' => 'application/json');

    if ($data) {
        $req->content(encode_json($data));
    }

    my $res = $ua->request($req);

    if ($res->is_success) {
        return decode_json($res->content);
    } else {
        return { success => 0, errors => [{ message => $res->status_line }] };
    }
}

# WHM API - Get zone from local DNS
sub whm_get_zone {
    my ($domain) = @_;

    # Use cPanel's DNS functions
    my $zonefile = "/var/named/$domain.db";
    return undef unless -f $zonefile;

    open my $fh, '<', $zonefile or return undef;
    my @lines = <$fh>;
    close $fh;

    my @records;
    my $origin = $domain;

    for my $line (@lines) {
        chomp $line;
        next if $line =~ /^\s*;/;  # Skip comments
        next if $line =~ /^\s*$/;  # Skip empty lines
        next if $line =~ /^\$TTL/; # Skip TTL directive

        if ($line =~ /^\$ORIGIN\s+(\S+)/) {
            $origin = $1;
            $origin =~ s/\.$//;
            next;
        }

        # Parse DNS records: name ttl class type content
        if ($line =~ /^(\S+)\s+(\d+)?\s*IN\s+(A|AAAA|CNAME|TXT|MX|SRV|NS)\s+(.+)$/i) {
            my ($name, $ttl, $type, $content) = ($1, $2 || 3600, uc($3), $4);

            # Normalize name
            $name = $origin if $name eq '@';
            $name = "$name.$origin" unless $name =~ /\.$/;
            $name =~ s/\.$//;

            # Clean content
            $content =~ s/\s+$//;
            $content =~ s/^"(.*)"$/$1/ if $type eq 'TXT';

            # Skip NS records for the domain itself (Cloudflare manages these)
            next if $type eq 'NS' && $name eq $domain;

            push @records, {
                name => $name,
                type => $type,
                content => $content,
                ttl => int($ttl)
            };
        }
    }

    return \@records;
}

# WHM API - Remove zone or add marker
sub whm_mark_zone_transferred {
    my ($domain, $remove) = @_;

    if ($remove) {
        # Use WHM API to remove zone
        my $result = `whmapi1 killdns domain=$domain 2>&1`;
        return $result !~ /error/i;
    } else {
        # Add marker TXT record
        my $marker = "v=cloudflare-dns; managed=true; transferred=" . time();
        system("whmapi1 addzonerecord domain=$domain name=_cloudflare type=TXT txtdata=\"$marker\" 2>&1");
        return 1;
    }
}

# Get list of WHM zones
sub whm_list_zones {
    my $result = `whmapi1 listzones 2>&1`;
    my @zones;

    while ($result =~ /domain:\s*(\S+)/g) {
        push @zones, $1;
    }

    return \@zones;
}

# Check if zone has Cloudflare marker
sub whm_has_cf_marker {
    my ($domain) = @_;
    my $result = `dig +short TXT _cloudflare.$domain \@localhost 2>/dev/null`;
    return $result =~ /cloudflare-dns/;
}

# Parse BIND zone file format (from paste or file)
sub parse_zone_file {
    my ($zone_text, $domain) = @_;

    my @records;
    my $origin = $domain;
    my $default_ttl = 3600;

    for my $line (split /\n/, $zone_text) {
        chomp $line;
        $line =~ s/\s+$//;  # Trim trailing whitespace

        next if $line =~ /^\s*$/;      # Skip empty lines
        next if $line =~ /^\s*;/;      # Skip comments
        next if $line =~ /^;/;         # Skip comments
        next if $line =~ /SOA/i;       # Skip SOA records

        # Handle $ORIGIN directive
        if ($line =~ /^\$ORIGIN\s+(\S+)/i) {
            $origin = $1;
            $origin =~ s/\.$//;
            next;
        }

        # Handle $TTL directive
        if ($line =~ /^\$TTL\s+(\d+)/i) {
            $default_ttl = $1;
            next;
        }

        # Parse record: name [ttl] [class] type content
        # Supports formats like:
        #   @  3600  IN  A  1.2.3.4
        #   www      IN  CNAME  @
        #   mail  600  IN  MX  10 mailserver.com.

        if ($line =~ /^(\S+)\s+(?:(\d+)\s+)?(?:IN\s+)?(A|AAAA|CNAME|TXT|MX|SRV|NS|CAA)\s+(.+)$/i) {
            my ($name, $ttl, $type, $content) = ($1, $2, uc($3), $4);
            $ttl ||= $default_ttl;

            # Normalize name
            if ($name eq '@') {
                $name = $domain;
            } elsif ($name !~ /\.$/ && $name !~ /\Q$domain\E$/) {
                $name = "$name.$domain";
            }
            $name =~ s/\.$//;

            # Clean content
            $content =~ s/\s+$//;
            $content =~ s/\.$// if $type =~ /^(CNAME|MX|NS|SRV)$/;

            # Handle TXT records with quotes
            if ($type eq 'TXT') {
                $content =~ s/^"//;
                $content =~ s/"$//;
                # Handle multi-part TXT records
                $content =~ s/"\s+"//g;
            }

            # Skip NS records for domain root (Cloudflare manages these)
            next if $type eq 'NS' && $name eq $domain;

            # Skip "Parked" A records
            next if $type eq 'A' && $content =~ /parked/i;

            push @records, {
                name => $name,
                type => $type,
                content => $content,
                ttl => int($ttl)
            };
        }
    }

    return \@records;
}

# Scan external DNS for a domain
sub scan_external_dns {
    my ($domain) = @_;

    my @records;
    my @types = qw(A AAAA CNAME MX TXT SRV);

    # Common subdomains to check
    my @subdomains = (
        '',           # root
        'www',
        'mail',
        'ftp',
        'webmail',
        'cpanel',
        'whm',
        'autodiscover',
        'autoconfig',
        '_dmarc',
        'selector1._domainkey',
        'selector2._domainkey',
        'enterpriseenrollment',
        'enterpriseregistration',
        'lyncdiscover',
        'sip',
        '_sip._tls',
        '_sipfederationtls._tcp',
    );

    for my $sub (@subdomains) {
        my $fqdn = $sub ? "$sub.$domain" : $domain;

        for my $type (@types) {
            # Skip SRV for non-SRV names
            next if $type eq 'SRV' && $sub !~ /^_/;
            # Only check SRV for SRV-style names
            next if $type ne 'SRV' && $sub =~ /^_.*\._/;

            my $result;
            if ($type eq 'SRV') {
                $result = `dig +short SRV $fqdn 2>/dev/null`;
            } else {
                $result = `dig +short $type $fqdn 2>/dev/null`;
            }

            next unless $result;
            chomp $result;

            for my $line (split /\n/, $result) {
                next unless $line;
                next if $line =~ /^;;/;  # Skip dig comments

                my $content = $line;
                $content =~ s/\.$//;  # Remove trailing dot

                # Clean TXT records
                if ($type eq 'TXT') {
                    $content =~ s/^"//;
                    $content =~ s/"$//;
                }

                # Skip Cloudflare IPs if proxied
                next if $type eq 'A' && $content =~ /^(104\.1[6-9]\.|104\.2[0-9]\.|172\.6[4-9]\.|172\.7[0-1]\.)/;

                push @records, {
                    name => $fqdn,
                    type => $type,
                    content => $content,
                    ttl => 3600
                };
            }
        }
    }

    return \@records;
}

# Main
my $cgi = CGI->new;
my $action = $cgi->param('action') || 'home';

# Print header
print $cgi->header('text/html');

# Load config
my $config = load_config();
my $api_token = $config->{api_token} || '';

# HTML header
print_html_header();

# Route actions
if ($action eq 'home') {
    show_home($cgi, $config, $api_token);
}
elsif ($action eq 'save_token') {
    save_token($cgi, $config);
}
elsif ($action eq 'list_zones') {
    list_zones($api_token);
}
elsif ($action eq 'view_zone') {
    view_zone($cgi, $api_token);
}
elsif ($action eq 'add_record') {
    add_record($cgi, $api_token);
}
elsif ($action eq 'edit_record') {
    edit_record($cgi, $api_token);
}
elsif ($action eq 'save_record') {
    save_record($cgi, $api_token);
}
elsif ($action eq 'delete_record') {
    delete_record($cgi, $api_token);
}
elsif ($action eq 'mass_delete') {
    mass_delete($cgi, $api_token);
}
elsif ($action eq 'add_m365') {
    add_m365($cgi, $api_token);
}
elsif ($action eq 'import_zone') {
    import_zone($cgi, $api_token);
}
elsif ($action eq 'transfer_zone') {
    transfer_zone($cgi, $api_token);
}
elsif ($action eq 'do_transfer') {
    do_transfer($cgi, $api_token);
}
elsif ($action eq 'whm_zones') {
    whm_zones($cgi, $api_token);
}
else {
    print "
Unknown action: $action
";
}

print_html_footer();

# ============ Actions ============

sub show_home {
    my ($cgi, $config, $token) = @_;

    print '
Cloudflare DNS Manager
';

    if (!$token) {
        print '
';
        print 'Setup Required: Please configure your Cloudflare API token.';
        print '
';
        show_token_form($config);
        return;
    }

    my $verify = cf_api($token, 'GET', '/user/tokens/verify');
    if (!$verify->{success}) {
        print '
API Token is invalid or expired.
';
        show_token_form($config);
        return;
    }

    print '
✓ Connected to Cloudflare
';

    print '
';
    print 'Cloudflare Zones ';
    print 'WHM Zones ';
    print 'Import New Zone ';
    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
    print '
';

    print '
';
    show_token_form($config);
    print '
';

    # Quick stats
    my $cf_zones = cf_api($token, 'GET', '/zones?per_page=50');
    if ($cf_zones->{success}) {
        my $active = scalar grep { $_->{status} eq 'active' } @{$cf_zones->{result}};
        my $pending = scalar grep { $_->{status} eq 'pending' } @{$cf_zones->{result}};
        print '
';
        print "
Cloudflare Zones: $active active, $pending pending
";
        print '
';
    }
}

sub show_token_form {
    my ($config) = @_;
    my $current = $config->{api_token} ? '********' . substr($config->{api_token}, -4) : 'Not set';

    print '<form method="post" action="?action=save_token" style="margin: 20px 0; padding: 20px; background: #f5f5f5; border-radius: 5px;">';
    print '
Cloudflare API Token
';
    print "
Current: $current
";
    print '
';
    print 'API Token:
';
    print '<input type="password" name="api_token" class="form-control" style="width: 400px;" placeholder="Enter Cloudflare API token">';
    print '
';
    print '<button type="submit" class="btn btn-success">Save Token</button>';
    print '
Required: Zone:Read, Zone:Edit, DNS:Read, DNS:Edit
';
    print '</form>';
}

sub save_token {
    my ($cgi, $config) = @_;
    my $token = $cgi->param('api_token');

    if ($token) {
        $config->{api_token} = $token;
        save_config($config);
        print '
API Token saved successfully.
';
        print '
Return to Home
';
    } else {
        print '
No token provided.
';
    }
}

sub list_zones {
    my ($token) = @_;

    print '
Cloudflare Zones
';
    print '
← Back
';

    my $result = cf_api($token, 'GET', '/zones?per_page=50');

    if (!$result->{success}) {
        print '
Failed to load zones
';
        return;
    }

    print '
';
    print '

Domain	Status	Plan	Actions

';
    print '
';

    for my $zone (sort { $a->{name} cmp $b->{name} } @{$result->{result}}) {
        my $status_class = $zone->{status} eq 'active' ? 'success' : 'warning';
        print "
";
        print "$zone->{name}	";
        print "$zone->{status}	";
        print "$zone->{plan}{name}	";
        print "";
        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-primary'>DNS ";
        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-info'>+M365";
        print "	";
        print "
";
    }

    print '

';
}

# NEW: WHM Zones list with transfer option
sub whm_zones {
    my ($cgi, $token) = @_;

    print '
WHM DNS Zones
';
    print '
← Back
';
    print '
Transfer zones from WHM to Cloudflare for centralized DNS management.
';

    my $zones = whm_list_zones();

    # Get Cloudflare zones for comparison
    my $cf_result = cf_api($token, 'GET', '/zones?per_page=100');
    my %cf_zones;
    if ($cf_result->{success}) {
        $cf_zones{$_->{name}} = $_->{status} for @{$cf_result->{result}};
    }

    print '
';
    print '

Domain	WHM Status	Cloudflare Status	Actions

';
    print '
';

    for my $domain (sort @$zones) {
        my $has_marker = whm_has_cf_marker($domain);
        my $cf_status = $cf_zones{$domain} || 'Not in Cloudflare';

        my $whm_status = $has_marker ? 'Transferred' : 'Active';
        my $cf_label = $cf_status eq 'active' ? 'Active' :
                       $cf_status eq 'pending' ? 'Pending' :
                       '-';

        print "
";
        print "$domain	";
        print "$whm_status	";
        print "$cf_label	";
        print "";

        if (!exists $cf_zones{$domain}) {
            print "Transfer to CF";
        } elsif ($cf_status eq 'active' && !$has_marker) {
            print "Mark Transferred";
        } else {
            print "-";
        }

        print "	";
        print "
";
    }

    print '

';
}

# NEW: Transfer zone from WHM to Cloudflare
sub transfer_zone {
    my ($cgi, $token) = @_;

    my $domain = $cgi->param('domain');
    my $mark_only = $cgi->param('mark_only');

    print "
Transfer Zone: $domain
";
    print '
← Back to WHM Zones
';

    if ($mark_only) {
        # Just mark it, zone already in Cloudflare
        print '
Zone already exists in Cloudflare. Choose how to handle WHM zone:
';
    } else {
        # Preview records to transfer
        my $records = whm_get_zone($domain);

        if (!$records || !@$records) {
            print '
Could not read zone file or no records found.
';
            return;
        }

        print '
Records to Transfer
';
        print '
';
        print '

Type	Name	Content	TTL

';
        print '
';

        for my $rec (@$records) {
            my $content = length($rec->{content}) > 60 ? substr($rec->{content}, 0, 57) . '...' : $rec->{content};
            print "
$rec->{type}	$rec->{name}	$content	$rec->{ttl}
";
        }

        print '

';
        print "
Total: " . scalar(@$records) . " records
";
    }

    print '<form method="post" action="?action=do_transfer" style="margin-top: 20px; padding: 20px; background: #f5f5f5; border-radius: 5px;">';
    print "<input type='hidden' name='domain' value='$domain'>";
    print "<input type='hidden' name='mark_only' value='$mark_only'>";

    print '
After Transfer Options
';
    print '
';
    print '<input type="radio" name="whm_action" value="marker" checked> Add marker record (keep zone in WHM, add _cloudflare TXT)
';
    print '<input type="radio" name="whm_action" value="remove"> Remove zone from WHM (delete local zone file)
';
    print '<input type="radio" name="whm_action" value="none"> Do nothing (keep both)';
    print '
';

    unless ($mark_only) {
        print '
';
        print 'Important: After transfer, you must update nameservers at the registrar to point to Cloudflare.';
        print '
';
    }

    print '<button type="submit" class="btn btn-success">Proceed with Transfer</button>';
    print ' Cancel';
    print '</form>';
}

# NEW: Execute zone transfer
sub do_transfer {
    my ($cgi, $token) = @_;

    my $domain = $cgi->param('domain');
    my $mark_only = $cgi->param('mark_only');
    my $whm_action = $cgi->param('whm_action') || 'marker';

    print "
Transferring: $domain
";

    my $zone_id;
    my @results;

    unless ($mark_only) {
        # Step 1: Create zone in Cloudflare
        print '
Step 1: Create Zone in Cloudflare
';
        my $create_result = cf_api($token, 'POST', '/zones', { name => $domain, jump_start => \0 });

        if ($create_result->{success}) {
            $zone_id = $create_result->{result}{id};
            print "
Zone created! ID: $zone_id
";
            print '
Nameservers:

';
            print "
$_
" for @{$create_result->{result}{name_servers}};
            print '
';
        } elsif ($create_result->{errors}[0]{code} == 1061) {
            # Zone already exists
            my $existing = cf_api($token, 'GET', "/zones?name=$domain");
            if ($existing->{success} && @{$existing->{result}}) {
                $zone_id = $existing->{result}[0]{id};
                print "
Zone already exists in Cloudflare. ID: $zone_id
";
            }
        } else {
            print "
Failed: $create_result->{errors}[0]{message}
";
            return;
        }

        # Step 2: Import records
        print '
Step 2: Import DNS Records
';
        my $records = whm_get_zone($domain);

        print '
';
        print '

Record	Status

';
        print '
';

        for my $rec (@$records) {
            my $data = {
                type => $rec->{type},
                name => $rec->{name},
                content => $rec->{content},
                ttl => $rec->{ttl} > 0 ? $rec->{ttl} : 1
            };

            # Handle MX priority
            if ($rec->{type} eq 'MX' && $rec->{content} =~ /^(\d+)\s+(.+)/) {
                $data->{priority} = int($1);
                $data->{content} = $2;
            }

            my $result = cf_api($token, 'POST', "/zones/$zone_id/dns_records", $data);
            my $status = $result->{success} ? 'Added' : ($result->{errors}[0]{message} || 'Failed');
            my $class = $result->{success} ? 'success' : 'warning';

            print "
$rec->{type} $rec->{name}	$status
";
            push @results, { record => "$rec->{type} $rec->{name}", success => $result->{success} };
        }

        print '

';
    } else {
        print '
Skipped zone creation (already in Cloudflare)
';
    }

    # Step 3: Handle WHM zone
    print '
Step 3: WHM Zone Action
';

    if ($whm_action eq 'marker') {
        whm_mark_zone_transferred($domain, 0);
        print '
Added _cloudflare marker record to WHM zone.
';
    } elsif ($whm_action eq 'remove') {
        if (whm_mark_zone_transferred($domain, 1)) {
            print '
Removed zone from WHM.
';
        } else {
            print '
Could not remove zone from WHM (may require manual removal).
';
        }
    } else {
        print '
No changes made to WHM zone.
';
    }

    print '
';
    print 'Next Step: Update nameservers at your registrar to Cloudflare\'s nameservers.';
    print '
';

    print '
';
    print 'View Cloudflare Zones ';
    print 'View WHM Zones';
    print '
';
}

sub view_zone {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');

    print "
DNS Records: $domain
";
    print '
← Back to Zones
';

    # Add record form
    print '
';
    print '
Add Record
';
    print '<form method="post" action="?action=add_record">';
    print "<input type='hidden' name='zone_id' value='$zone_id'>";
    print "<input type='hidden' name='domain' value='$domain'>";
    print '<select name="type" class="form-control" style="width:100px; display:inline-block;">';
    print '<option>A</option><option>AAAA</option><option>CNAME</option><option>TXT</option><option>MX</option>';
    print '</select> ';
    print '<input type="text" name="name" placeholder="Name" class="form-control" style="width: 150px; display:inline-block;"> ';
    print '<input type="text" name="content" placeholder="Content" class="form-control" style="width: 300px; display:inline-block;"> ';
    print '<input type="checkbox" name="proxied"> Proxied ';
    print '<button type="submit" class="btn btn-success">Add</button>';
    print '</form>
';

    my $result = cf_api($token, 'GET', "/zones/$zone_id/dns_records?per_page=100");

    if (!$result->{success}) {
        print '
Failed to load records
';
        return;
    }

    # Mass delete form wrapper
    print '<form method="post" action="?action=mass_delete" id="mass-delete-form">';
    print "<input type='hidden' name='zone_id' value='$zone_id'>";
    print "<input type='hidden' name='domain' value='$domain'>";

    print '
';
    print '<button type="button" onclick="toggleAll()" class="btn btn-sm btn-secondary">Select All</button> ';
    print '<button type="submit" class="btn btn-sm btn-danger" onclick="return confirmMassDelete()">Delete Selected</button>';
    print '
';

    print '
';
    print '

<input type="checkbox" id="select-all" onclick="toggleAll()">	Type	Name	Content	TTL	Proxied	Actions

';
    print '
';

    for my $rec (sort { $a->{type} cmp $b->{type} || $a->{name} cmp $b->{name} } @{$result->{result}}) {
        my $content = length($rec->{content}) > 40 ? substr($rec->{content}, 0, 37) . '...' : $rec->{content};
        my $ttl = $rec->{ttl} == 1 ? 'Auto' : $rec->{ttl};
        my $proxied = $rec->{proxied} ? '✓' : '';

        print "
";
        print "<input type='checkbox' name='record_ids' value='$rec->{id}' class='record-checkbox'>	";
        print "$rec->{type}	";
        print "$rec->{name}	";
        print "{content}' style='font-size:12px;'>$content	";
        print "$ttl	";
        print "$proxied	";
        print "";
        print "{id}' class='btn btn-sm btn-info'>Edit ";
        print "{id}' class='btn btn-sm btn-danger' onclick='return confirm(\"Delete?\")'>Del";
        print "	";
        print "
";
    }

    print '

';
    print '</form>';

    # JavaScript for select all
    print <<'JS';

JS
}

# NEW: Edit record form
sub edit_record {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my $record_id = $cgi->param('record_id');

    # Get current record
    my $result = cf_api($token, 'GET', "/zones/$zone_id/dns_records/$record_id");

    if (!$result->{success}) {
        print '
Record not found
';
        return;
    }

    my $rec = $result->{result};

    print "
Edit Record
";
    print "
← Back to $domain
";

    print '<form method="post" action="?action=save_record" style="max-width: 600px;">';
    print "<input type='hidden' name='zone_id' value='$zone_id'>";
    print "<input type='hidden' name='domain' value='$domain'>";
    print "<input type='hidden' name='record_id' value='$record_id'>";
    print "<input type='hidden' name='type' value='$rec->{type}'>";

    print '
';
    print "Type: $rec->{type}";
    print '
';

    print '
';
    print 'Name:
';
    print "<input type='text' name='name' value='$rec->{name}' class='form-control' style='width:100%;'>";
    print '
';

    print '
';
    print 'Content:
';
    if ($rec->{type} eq 'TXT') {
        print "<textarea name='content' class='form-control' style='width:100%; height:100px;'>$rec->{content}</textarea>";
    } else {
        print "<input type='text' name='content' value='$rec->{content}' class='form-control' style='width:100%;'>";
    }
    print '
';

    print '
';
    print 'TTL:
';
    print '<select name="ttl" class="form-control" style="width:150px;">';
    my @ttls = (1, 60, 120, 300, 600, 900, 1800, 3600, 7200, 18000, 43200, 86400);
    my %ttl_labels = (1 => 'Auto', 60 => '1 min', 120 => '2 min', 300 => '5 min', 600 => '10 min',
                      900 => '15 min', 1800 => '30 min', 3600 => '1 hour', 7200 => '2 hours',
                      18000 => '5 hours', 43200 => '12 hours', 86400 => '1 day');
    for my $t (@ttls) {
        my $sel = $rec->{ttl} == $t ? 'selected' : '';
        print "<option value='$t' $sel>$ttl_labels{$t}</option>";
    }
    print '</select>';
    print '
';

    if ($rec->{type} =~ /^(A|AAAA|CNAME)$/) {
        my $checked = $rec->{proxied} ? 'checked' : '';
        print '
';
        print "<input type='checkbox' name='proxied' $checked> Proxied (orange cloud)";
        print '
';
    }

    print '<button type="submit" class="btn btn-success">Save Changes</button>';
    print " Cancel";
    print '</form>';
}

# NEW: Save edited record
sub save_record {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my $record_id = $cgi->param('record_id');
    my $type = $cgi->param('type');
    my $name = $cgi->param('name');
    my $content = $cgi->param('content');
    my $ttl = $cgi->param('ttl') || 1;
    my $proxied = $cgi->param('proxied') ? \1 : \0;

    my $data = {
        type => $type,
        name => $name,
        content => $content,
        ttl => int($ttl),
        proxied => $proxied
    };

    my $result = cf_api($token, 'PUT', "/zones/$zone_id/dns_records/$record_id", $data);

    if ($result->{success}) {
        print '
Record updated successfully.
';
    } else {
        my $err = $result->{errors}[0]{message} || 'Unknown error';
        print "
Failed: $err
";
    }

    print "
Return to DNS Records
";
}

# NEW: Mass delete records
sub mass_delete {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my @record_ids = $cgi->multi_param('record_ids');

    print "
Mass Delete Results
";

    if (!@record_ids) {
        print '
No records selected.
';
        print "
Back to DNS Records
";
        return;
    }

    print '
';
    print '

Record ID	Status

';
    print '
';

    my $success = 0;
    my $failed = 0;

    for my $record_id (@record_ids) {
        my $result = cf_api($token, 'DELETE', "/zones/$zone_id/dns_records/$record_id");

        if ($result->{success}) {
            print "
$record_id	Deleted
";
            $success++;
        } else {
            print "
$record_id	Failed
";
            $failed++;
        }
    }

    print '

';
    print "
Deleted: $success, Failed: $failed
";
    print "
Return to DNS Records
";
}

sub add_record {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my $type = $cgi->param('type');
    my $name = $cgi->param('name');
    my $content = $cgi->param('content');
    my $proxied = $cgi->param('proxied') ? \1 : \0;

    my $data = {
        type => $type,
        name => $name,
        content => $content,
        ttl => 1,
        proxied => $proxied
    };

    my $result = cf_api($token, 'POST', "/zones/$zone_id/dns_records", $data);

    if ($result->{success}) {
        print '
Record added successfully.
';
    } else {
        my $err = $result->{errors}[0]{message} || 'Unknown error';
        print "
Failed: $err
";
    }

    print "
Return to DNS Records
";
}

sub delete_record {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my $record_id = $cgi->param('record_id');

    my $result = cf_api($token, 'DELETE', "/zones/$zone_id/dns_records/$record_id");

    if ($result->{success}) {
        print '
Record deleted.
';
    } else {
        print '
Failed to delete record.
';
    }

    print "
Return to DNS Records
";
}

sub add_m365 {
    my ($cgi, $token) = @_;

    my $zone_id = $cgi->param('zone_id');
    my $domain = $cgi->param('domain');
    my $tenant = $cgi->param('tenant');

    if (!$tenant) {
        print "
Add M365 Records: $domain
";
        print '
← Back
';

        print '<form method="post" action="?action=add_m365" style="max-width: 500px;">';
        print "<input type='hidden' name='zone_id' value='$zone_id'>";
        print "<input type='hidden' name='domain' value='$domain'>";
        print '
';
        print 'Microsoft 365 Tenant Name
';
        print '<input type="text" name="tenant" class="form-control" placeholder="e.g., contoso (from contoso.onmicrosoft.com)">';
        print '
Enter the prefix of your .onmicrosoft.com domain
';
        print '
';
        print '
Records that will be added:
';
        print '
';
        print '
MX → mail.protection.outlook.com
';
        print '
SPF (TXT) → include:spf.protection.outlook.com
';
        print '
DMARC (TXT) → v=DMARC1; p=reject
';
        print '
DKIM selector1 & selector2 (CNAME)
';
        print '
Autodiscover (CNAME)
';
        print '
';
        print '<button type="submit" class="btn btn-primary">Add M365 Records</button>';
        print '</form>';
        return;
    }

    print "
Adding M365 Records: $domain
";

    my $domain_clean = $domain;
    $domain_clean =~ s/\./-/g;

    my $tenant_clean = lc($tenant);
    $tenant_clean =~ s/\.//g;

    my @records = (
        { type => 'MX', name => $domain, content => "$domain.mail.protection.outlook.com", priority => 0 },
        { type => 'TXT', name => $domain, content => "v=spf1 include:spf.protection.outlook.com -all" },
        { type => 'TXT', name => "_dmarc.$domain", content => "v=DMARC1; p=reject; rua=mailto:admin\@$domain" },
        { type => 'CNAME', name => "selector1._domainkey.$domain", content => "selector1-${domain_clean}._domainkey.${tenant_clean}.onmicrosoft.com" },
        { type => 'CNAME', name => "selector2._domainkey.$domain", content => "selector2-${domain_clean}._domainkey.${tenant_clean}.onmicrosoft.com" },
        { type => 'CNAME', name => "autodiscover.$domain", content => 'autodiscover.outlook.com' },
    );

    print '
';
    print '

Record	Status

';
    print '
';

    for my $rec (@records) {
        my $result = cf_api($token, 'POST', "/zones/$zone_id/dns_records", $rec);
        my $status = $result->{success} ? 'Added' : ($result->{errors}[0]{message} || 'Failed');
        my $class = $result->{success} ? 'success' : 'warning';
        print "
$rec->{type} $rec->{name}	$status
";
    }

    print '

';
    print '
Remember to enable DKIM signing in Microsoft 365 Defender portal.
';
    print "
View DNS Records
";
}

sub import_zone {
    my ($cgi, $token) = @_;

    my $domain = $cgi->param('domain');

    if (!$domain) {
        print '
Import Zone to Cloudflare
';
        print '
← Back
';

        print '<form method="post" action="?action=import_zone" style="max-width: 400px;">';
        print '
';
        print 'Domain Name
';
        print '<input type="text" name="domain" class="form-control" placeholder="example.com">';
        print '
';
        print '<button type="submit" class="btn btn-primary">Import Zone</button>';
        print '</form>';
        return;
    }

    print "
Importing: $domain
";

    my $result = cf_api($token, 'POST', '/zones', { name => $domain, jump_start => \1 });

    if ($result->{success}) {
        my $zone = $result->{result};
        print '
Zone added successfully!
';
        print "
Zone ID: $zone->{id}
";
        print "
Status: $zone->{status}
";
        print '
Update Nameservers
';
        print '
Change nameservers at your registrar to:
';
        print '
';
        print "
$_
" for @{$zone->{name_servers}};
        print '
';
    } else {
        my $err = $result->{errors}[0]{message} || 'Unknown error';
        print "
Failed: $err
";
    }

    print '
View All Zones
';
}

# ============ HTML Templates ============

sub print_html_header {
    print <<'HTML';
<!DOCTYPE html>

    <title>Cloudflare DNS Manager</title>

HTML
}

sub print_html_footer {
    print <<'HTML';

Cloudflare DNS Manager v1.1 | AZ Computer Guru

HTML
}

+elsif ($action eq 'do_zonefile_import') {

+    do_zonefile_import($cgi, $api_token);


Now let me update the home page to show the new import options:


print '
';
    print 'Cloudflare Zones ';
    print 'WHM Zones ';
    print 'Import New Zone ';
    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
    print '
';

print '
';
    print 'Cloudflare Zones ';
    print 'WHM Zones ';
    print 'Import New Zone ';
    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
    print '
';

    # Onboarding options
    print '
Onboard External Domain
';
    print '
';
    print 'Import from Zone File ';
    print 'Scan External DNS';
    print '
';
    print '
Use these to import DNS records from domains hosted elsewhere (GoDaddy, Namecheap, etc.)
';


395→        print '
';
   396→        print 'Setup Required: Please configure your Cloudflare API token.';
   397→        print '
';
   398→        show_token_form($config);
   399→        return;
   400→    }
   401→
   402→    my $verify = cf_api($token, 'GET', '/user/tokens/verify');
   403→    if (!$verify->{success}) {
   404→        print '
API Token is invalid or expired.
';
   405→        show_token_form($config);
   406→        return;
   407→    }
   408→
   409→    print '
✓ Connected to Cloudflare
';
   410→
   411→    print '
';
   412→    print 'Cloudflare Zones ';
   413→    print 'WHM Zones ';
   414→    print 'Import New Zone ';
   415→    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
   416→    print '
';
   417→
   418→    print '
';
   419→    show_token_form($config);
   420→    print '
';
   421→
   422→    # Quick stats
   423→    my $cf_zones = cf_api($token, 'GET', '/zones?per_page=50');
   424→    if ($cf_zones->{success}) {

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

print '
';
        print 'Setup Required: Please configure your Cloudflare API token.';
        print '
';
        show_token_form($config);
        return;
    }

    my $verify = cf_api($token, 'GET', '/user/tokens/verify');
    if (!$verify->{success}) {
        print '
API Token is invalid or expired.
';
        show_token_form($config);
        return;
    }

    print '
✓ Connected to Cloudflare
';

    print '
';
    print 'Cloudflare Zones ';
    print 'WHM Zones ';
    print 'Import New Zone ';
    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
    print '
';

    print '
';
    show_token_form($config);
    print '
';

    # Quick stats
    my $cf_zones = cf_api($token, 'GET', '/zones?per_page=50');
    if ($cf_zones->{success}) {


print '
';
    print 'Cloudflare Zones ';
    print 'WHM Zones ';
    print 'Import New Zone ';
    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
    print '
';

    print '
';
    show_token_form($config);
    print '
';

    # Quick stats

print '
';
    print 'Cloudflare Zones ';
    print 'WHM Zones ';
    print 'Import New Zone ';
    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
    print '
';

    # Onboarding options
    print '
Onboard External Domain
';
    print '
';
    print 'Import from Zone File ';
    print 'Scan External DNS';
    print '
';
    print '
Use these to import DNS records from domains hosted elsewhere (GoDaddy, Namecheap, etc.)
';

    print '
';
    show_token_form($config);
    print '
';

    # Quick stats