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:
     1→#!/usr/local/cpanel/3rdparty/bin/perl
     2→#
     3→# Cloudflare DNS Manager - WHM Plugin v1.1
     4→# Features: Zone transfer, mass delete, record editing
     5→#
     6→
     7→use strict;
     8→use warnings;
     9→
    10→BEGIN {
    11→    unshift @INC, '/usr/local/cpanel/3rdparty/perl/542/cpanel-lib';
    12→    unshift @INC, '/var/cpanel/plugins/cloudflare_dns/lib';
    13→}
    14→
    15→use CGI;
    16→use JSON;
    17→use LWP::UserAgent;
    18→use HTTP::Request;
    19→
    20→# Config file path
    21→my $CONFIG_FILE = '/var/cpanel/plugins/cloudflare_dns/config.json';
    22→
    23→# Load or initialize config
    24→sub load_config {
    25→    if (-f $CONFIG_FILE) {
    26→        open my $fh, '<', $CONFIG_FILE or return {};
    27→        local $/;
    28→        my $json = <$fh>;
    29→        close $fh;
    30→        return eval { decode_json($json) } || {};
    31→    }
    32→    return {};
    33→}
    34→
    35→sub save_config {
    36→    my ($config) = @_;
    37→    open my $fh, '>', $CONFIG_FILE or die "Cannot write config: $!";
    38→    print $fh encode_json($config);
    39→    close $fh;
    40→    chmod 0600, $CONFIG_FILE;
    41→}
    42→
    43→# Cloudflare API
    44→sub cf_api {
    45→    my ($token, $method, $endpoint, $data) = @_;
    46→
    47→    my $ua = LWP::UserAgent->new(timeout => 30);
    48→    my $url = "https://api.cloudflare.com/client/v4" . $endpoint;
    49→
    50→    my $req = HTTP::Request->new($method => $url);
    51→    $req->header('Authorization' => "Bearer $token");
    52→    $req->header('Content-Type' => 'application/json');
    53→
    54→    if ($data) {
    55→        $req->content(encode_json($data));
    56→    }
    57→
    58→    my $res = $ua->request($req);
    59→
    60→    if ($res->is_success) {
    61→        return decode_json($res->content);
    62→    } else {
    63→        return { success => 0, errors => [{ message => $res->status_line }] };
    64→    }
    65→}
    66→
    67→# WHM API - Get zone from local DNS
    68→sub whm_get_zone {
    69→    my ($domain) = @_;
    70→
    71→    # Use cPanel's DNS functions
    72→    my $zonefile = "/var/named/$domain.db";
    73→    return undef unless -f $zonefile;
    74→
    75→    open my $fh, '<', $zonefile or return undef;
    76→    my @lines = <$fh>;
    77→    close $fh;
    78→
    79→    my @records;
    80→    my $origin = $domain;
    81→
    82→    for my $line (@lines) {
    83→        chomp $line;
    84→        next if $line =~ /^\s*;/;  # Skip comments
    85→        next if $line =~ /^\s*$/;  # Skip empty lines
    86→        next if $line =~ /^\$TTL/; # Skip TTL directive
    87→
    88→        if ($line =~ /^\$ORIGIN\s+(\S+)/) {
    89→            $origin = $1;
    90→            $origin =~ s/\.$//;
    91→            next;
    92→        }
    93→
    94→        # Parse DNS records: name ttl class type content
    95→        if ($line =~ /^(\S+)\s+(\d+)?\s*IN\s+(A|AAAA|CNAME|TXT|MX|SRV|NS)\s+(.+)$/i) {
    96→            my ($name, $ttl, $type, $content) = ($1, $2 || 3600, uc($3), $4);
    97→
    98→            # Normalize name
    99→            $name = $origin if $name eq '@';
   100→            $name = "$name.$origin" unless $name =~ /\.$/;
   101→            $name =~ s/\.$//;
   102→
   103→            # Clean content
   104→            $content =~ s/\s+$//;
   105→            $content =~ s/^"(.*)"$/$1/ if $type eq 'TXT';
   106→
   107→            # Skip NS records for the domain itself (Cloudflare manages these)
   108→            next if $type eq 'NS' && $name eq $domain;
   109→
   110→            push @records, {
   111→                name => $name,
   112→                type => $type,
   113→                content => $content,
   114→                ttl => int($ttl)
   115→            };
   116→        }
   117→    }
   118→
   119→    return \@records;
   120→}
   121→
   122→# WHM API - Remove zone or add marker
   123→sub whm_mark_zone_transferred {
   124→    my ($domain, $remove) = @_;
   125→
   126→    if ($remove) {
   127→        # Use WHM API to remove zone
   128→        my $result = `whmapi1 killdns domain=$domain 2>&1`;
   129→        return $result !~ /error/i;
   130→    } else {
   131→        # Add marker TXT record
   132→        my $marker = "v=cloudflare-dns; managed=true; transferred=" . time();
   133→        system("whmapi1 addzonerecord domain=$domain name=_cloudflare type=TXT txtdata=\"$marker\" 2>&1");
   134→        return 1;
   135→    }
   136→}
   137→
   138→# Get list of WHM zones
   139→sub whm_list_zones {
   140→    my $result = `whmapi1 listzones 2>&1`;
   141→    my @zones;
   142→
   143→    while ($result =~ /domain:\s*(\S+)/g) {
   144→        push @zones, $1;
   145→    }
   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→# Main
   158→my $cgi = CGI->new;
   159→my $action = $cgi->param('action') || 'home';
   160→
   161→# Print header
   162→print $cgi->header('text/html');
   163→
   164→# Load config
   165→my $config = load_config();
   166→my $api_token = $config->{api_token} || '';
   167→
   168→# HTML header
   169→print_html_header();
   170→
   171→# Route actions
   172→if ($action eq 'home') {
   173→    show_home($cgi, $config, $api_token);
   174→}
   175→elsif ($action eq 'save_token') {
   176→    save_token($cgi, $config);
   177→}
   178→elsif ($action eq 'list_zones') {
   179→    list_zones($api_token);
   180→}
   181→elsif ($action eq 'view_zone') {
   182→    view_zone($cgi, $api_token);
   183→}
   184→elsif ($action eq 'add_record') {
   185→    add_record($cgi, $api_token);
   186→}
   187→elsif ($action eq 'edit_record') {
   188→    edit_record($cgi, $api_token);
   189→}
   190→elsif ($action eq 'save_record') {
   191→    save_record($cgi, $api_token);
   192→}
   193→elsif ($action eq 'delete_record') {
   194→    delete_record($cgi, $api_token);
   195→}
   196→elsif ($action eq 'mass_delete') {
   197→    mass_delete($cgi, $api_token);
   198→}
   199→elsif ($action eq 'add_m365') {
   200→    add_m365($cgi, $api_token);
   201→}
   202→elsif ($action eq 'import_zone') {
   203→    import_zone($cgi, $api_token);
   204→}
   205→elsif ($action eq 'transfer_zone') {
   206→    transfer_zone($cgi, $api_token);
   207→}
   208→elsif ($action eq 'do_transfer') {
   209→    do_transfer($cgi, $api_token);
   210→}
   211→elsif ($action eq 'whm_zones') {
   212→    whm_zones($cgi, $api_token);
   213→}
   214→else {
   215→    print "
Unknown action: $action
";
   216→}
   217→
   218→print_html_footer();
   219→
   220→# ============ Actions ============
   221→
   222→sub show_home {
   223→    my ($cgi, $config, $token) = @_;
   224→
   225→    print '
Cloudflare DNS Manager
';
   226→
   227→    if (!$token) {
   228→        print '
';
   229→        print 'Setup Required: Please configure your Cloudflare API token.';
   230→        print '
';
   231→        show_token_form($config);
   232→        return;
   233→    }
   234→
   235→    my $verify = cf_api($token, 'GET', '/user/tokens/verify');
   236→    if (!$verify->{success}) {
   237→        print '
API Token is invalid or expired.
';
   238→        show_token_form($config);
   239→        return;
   240→    }
   241→
   242→    print '
✓ Connected to Cloudflare
';
   243→
   244→    print '
';
   245→    print 'Cloudflare Zones ';
   246→    print 'WHM Zones ';
   247→    print 'Import New Zone ';
   248→    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
   249→    print '
';
   250→
   251→    print '
';
   252→    show_token_form($config);
   253→    print '
';
   254→
   255→    # Quick stats
   256→    my $cf_zones = cf_api($token, 'GET', '/zones?per_page=50');
   257→    if ($cf_zones->{success}) {
   258→        my $active = scalar grep { $_->{status} eq 'active' } @{$cf_zones->{result}};
   259→        my $pending = scalar grep { $_->{status} eq 'pending' } @{$cf_zones->{result}};
   260→        print '
';
   261→        print "
Cloudflare Zones: $active active, $pending pending
";
   262→        print '
';
   263→    }
   264→}
   265→
   266→sub show_token_form {
   267→    my ($config) = @_;
   268→    my $current = $config->{api_token} ? '********' . substr($config->{api_token}, -4) : 'Not set';
   269→
   270→    print '<form method="post" action="?action=save_token" style="margin: 20px 0; padding: 20px; background: #f5f5f5; border-radius: 5px;">';
   271→    print '
Cloudflare API Token
';
   272→    print "
Current: $current
";
   273→    print '
';
   274→    print 'API Token:
';
   275→    print '<input type="password" name="api_token" class="form-control" style="width: 400px;" placeholder="Enter Cloudflare API token">';
   276→    print '
';
   277→    print '<button type="submit" class="btn btn-success">Save Token</button>';
   278→    print '
Required: Zone:Read, Zone:Edit, DNS:Read, DNS:Edit
';
   279→    print '</form>';
   280→}
   281→
   282→sub save_token {
   283→    my ($cgi, $config) = @_;
   284→    my $token = $cgi->param('api_token');
   285→
   286→    if ($token) {
   287→        $config->{api_token} = $token;
   288→        save_config($config);
   289→        print '
API Token saved successfully.
';
   290→        print '
Return to Home
';
   291→    } else {
   292→        print '
No token provided.
';
   293→    }
   294→}
   295→
   296→sub list_zones {
   297→    my ($token) = @_;
   298→
   299→    print '
Cloudflare Zones
';
   300→    print '
← Back
';
   301→
   302→    my $result = cf_api($token, 'GET', '/zones?per_page=50');
   303→
   304→    if (!$result->{success}) {
   305→        print '
Failed to load zones
';
   306→        return;
   307→    }
   308→
   309→    print '
';
   310→    print '

Domain	Status	Plan	Actions

';
   311→    print '
';
   312→
   313→    for my $zone (sort { $a->{name} cmp $b->{name} } @{$result->{result}}) {
   314→        my $status_class = $zone->{status} eq 'active' ? 'success' : 'warning';
   315→        print "
";
   316→        print "$zone->{name}	";
   317→        print "$zone->{status}	";
   318→        print "$zone->{plan}{name}	";
   319→        print "";
   320→        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-primary'>DNS ";
   321→        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-info'>+M365";
   322→        print "	";
   323→        print "
";
   324→    }
   325→
   326→    print '

';
   327→}
   328→
   329→# NEW: WHM Zones list with transfer option
   330→sub whm_zones {
   331→    my ($cgi, $token) = @_;
   332→
   333→    print '
WHM DNS Zones
';
   334→    print '
← Back
';
   335→    print '
Transfer zones from WHM to Cloudflare for centralized DNS management.
';
   336→
   337→    my $zones = whm_list_zones();
   338→
   339→    # Get Cloudflare zones for comparison
   340→    my $cf_result = cf_api($token, 'GET', '/zones?per_page=100');
   341→    my %cf_zones;
   342→    if ($cf_result->{success}) {
   343→        $cf_zones{$_->{name}} = $_->{status} for @{$cf_result->{result}};
   344→    }
   345→
   346→    print '
';
   347→    print '

Domain	WHM Status	Cloudflare Status	Actions

';
   348→    print '
';
   349→
   350→    for my $domain (sort @$zones) {
   351→        my $has_marker = whm_has_cf_marker($domain);
   352→        my $cf_status = $cf_zones{$domain} || 'Not in Cloudflare';
   353→
   354→        my $whm_status = $has_marker ? 'Transferred' : 'Active';
   355→        my $cf_label = $cf_status eq 'active' ? 'Active' :
   356→                       $cf_status eq 'pending' ? 'Pending' :
   357→                       '-';
   358→
   359→        print "
";
   360→        print "$domain	";
   361→        print "$whm_status	";
   362→        print "$cf_label	";
   363→        print "";
   364→
   365→        if (!exists $cf_zones{$domain}) {
   366→            print "Transfer to CF";
   367→        } elsif ($cf_status eq 'active' && !$has_marker) {
   368→            print "Mark Transferred";
   369→        } else {
   370→            print "-";
   371→        }
   372→
   373→        print "	";
   374→        print "
";
   375→    }
   376→
   377→    print '

';
   378→}
   379→
   380→# NEW: Transfer zone from WHM to Cloudflare
   381→sub transfer_zone {
   382→    my ($cgi, $token) = @_;
   383→
   384→    my $domain = $cgi->param('domain');
   385→    my $mark_only = $cgi->param('mark_only');
   386→
   387→    print "
Transfer Zone: $domain
";
   388→    print '
← Back to WHM Zones
';
   389→
   390→    if ($mark_only) {
   391→        # Just mark it, zone already in Cloudflare
   392→        print '
Zone already exists in Cloudflare. Choose how to handle WHM zone:
';
   393→    } else {
   394→        # Preview records to transfer
   395→        my $records = whm_get_zone($domain);
   396→
   397→        if (!$records || !@$records) {
   398→            print '
Could not read zone file or no records found.
';
   399→            return;
   400→        }
   401→
   402→        print '
Records to Transfer
';
   403→        print '
';
   404→        print '

Type	Name	Content	TTL

';
   405→        print '
';
   406→
   407→        for my $rec (@$records) {
   408→            my $content = length($rec->{content}) > 60 ? substr($rec->{content}, 0, 57) . '...' : $rec->{content};
   409→            print "
$rec->{type}	$rec->{name}	$content	$rec->{ttl}
";
   410→        }
   411→
   412→        print '

';
   413→        print "
Total: " . scalar(@$records) . " records
";
   414→    }
   415→
   416→    print '<form method="post" action="?action=do_transfer" style="margin-top: 20px; padding: 20px; background: #f5f5f5; border-radius: 5px;">';
   417→    print "<input type='hidden' name='domain' value='$domain'>";
   418→    print "<input type='hidden' name='mark_only' value='$mark_only'>";
   419→
   420→    print '
After Transfer Options
';
   421→    print '
';
   422→    print '<input type="radio" name="whm_action" value="marker" checked> Add marker record (keep zone in WHM, add _cloudflare TXT)
';
   423→    print '<input type="radio" name="whm_action" value="remove"> Remove zone from WHM (delete local zone file)
';
   424→    print '<input type="radio" name="whm_action" value="none"> Do nothing (keep both)';
   425→    print '
';
   426→
   427→    unless ($mark_only) {
   428→        print '
';
   429→        print 'Important: After transfer, you must update nameservers at the registrar to point to Cloudflare.';
   430→        print '
';
   431→    }
   432→
   433→    print '<button type="submit" class="btn btn-success">Proceed with Transfer</button>';
   434→    print ' Cancel';
   435→    print '</form>';
   436→}
   437→
   438→# NEW: Execute zone transfer
   439→sub do_transfer {
   440→    my ($cgi, $token) = @_;
   441→
   442→    my $domain = $cgi->param('domain');
   443→    my $mark_only = $cgi->param('mark_only');
   444→    my $whm_action = $cgi->param('whm_action') || 'marker';
   445→
   446→    print "
Transferring: $domain
";
   447→
   448→    my $zone_id;
   449→    my @results;
   450→
   451→    unless ($mark_only) {
   452→        # Step 1: Create zone in Cloudflare
   453→        print '
Step 1: Create Zone in Cloudflare
';
   454→        my $create_result = cf_api($token, 'POST', '/zones', { name => $domain, jump_start => \0 });
   455→
   456→        if ($create_result->{success}) {
   457→            $zone_id = $create_result->{result}{id};
   458→            print "
Zone created! ID: $zone_id
";
   459→            print '
Nameservers:

';
   460→            print "
$_
" for @{$create_result->{result}{name_servers}};
   461→            print '
';
   462→        } elsif ($create_result->{errors}[0]{code} == 1061) {
   463→            # Zone already exists
   464→            my $existing = cf_api($token, 'GET', "/zones?name=$domain");
   465→            if ($existing->{success} && @{$existing->{result}}) {
   466→                $zone_id = $existing->{result}[0]{id};
   467→                print "
Zone already exists in Cloudflare. ID: $zone_id
";
   468→            }
   469→        } else {
   470→            print "
Failed: $create_result->{errors}[0]{message}
";
   471→            return;
   472→        }
   473→
   474→        # Step 2: Import records
   475→        print '
Step 2: Import DNS Records
';
   476→        my $records = whm_get_zone($domain);
   477→
   478→        print '
';
   479→        print '

Record	Status

';
   480→        print '
';
   481→
   482→        for my $rec (@$records) {
   483→            my $data = {
   484→                type => $rec->{type},
   485→                name => $rec->{name},
   486→                content => $rec->{content},
   487→                ttl => $rec->{ttl} > 0 ? $rec->{ttl} : 1
   488→            };
   489→
   490→            # Handle MX priority
   491→            if ($rec->{type} eq 'MX' && $rec->{content} =~ /^(\d+)\s+(.+)/) {
   492→                $data->{priority} = int($1);
   493→                $data->{content} = $2;
   494→            }
   495→
   496→            my $result = cf_api($token, 'POST', "/zones/$zone_id/dns_records", $data);
   497→            my $status = $result->{success} ? 'Added' : ($result->{errors}[0]{message} || 'Failed');
   498→            my $class = $result->{success} ? 'success' : 'warning';
   499→
   500→            print "
$rec->{type} $rec->{name}	$status
";
   501→            push @results, { record => "$rec->{type} $rec->{name}", success => $result->{success} };
   502→        }
   503→
   504→        print '

';
   505→    } else {
   506→        print '
Skipped zone creation (already in Cloudflare)
';
   507→    }
   508→
   509→    # Step 3: Handle WHM zone
   510→    print '
Step 3: WHM Zone Action
';
   511→
   512→    if ($whm_action eq 'marker') {
   513→        whm_mark_zone_transferred($domain, 0);
   514→        print '
Added _cloudflare marker record to WHM zone.
';
   515→    } elsif ($whm_action eq 'remove') {
   516→        if (whm_mark_zone_transferred($domain, 1)) {
   517→            print '
Removed zone from WHM.
';
   518→        } else {
   519→            print '
Could not remove zone from WHM (may require manual removal).
';
   520→        }
   521→    } else {
   522→        print '
No changes made to WHM zone.
';
   523→    }
   524→
   525→    print '
';
   526→    print 'Next Step: Update nameservers at your registrar to Cloudflare\'s nameservers.';
   527→    print '
';
   528→
   529→    print '
';
   530→    print 'View Cloudflare Zones ';
   531→    print 'View WHM Zones';
   532→    print '
';
   533→}
   534→
   535→sub view_zone {
   536→    my ($cgi, $token) = @_;
   537→
   538→    my $zone_id = $cgi->param('zone_id');
   539→    my $domain = $cgi->param('domain');
   540→
   541→    print "
DNS Records: $domain
";
   542→    print '
← Back to Zones
';
   543→
   544→    # Add record form
   545→    print '
';
   546→    print '
Add Record
';
   547→    print '<form method="post" action="?action=add_record">';
   548→    print "<input type='hidden' name='zone_id' value='$zone_id'>";
   549→    print "<input type='hidden' name='domain' value='$domain'>";
   550→    print '<select name="type" class="form-control" style="width:100px; display:inline-block;">';
   551→    print '<option>A</option><option>AAAA</option><option>CNAME</option><option>TXT</option><option>MX</option>';
   552→    print '</select> ';
   553→    print '<input type="text" name="name" placeholder="Name" class="form-control" style="width: 150px; display:inline-block;"> ';
   554→    print '<input type="text" name="content" placeholder="Content" class="form-control" style="width: 300px; display:inline-block;"> ';
   555→    print '<input type="checkbox" name="proxied"> Proxied ';
   556→    print '<button type="submit" class="btn btn-success">Add</button>';
   557→    print '</form>
';
   558→
   559→    my $result = cf_api($token, 'GET', "/zones/$zone_id/dns_records?per_page=100");
   560→
   561→    if (!$result->{success}) {
   562→        print '
Failed to load records
';
   563→        return;
   564→    }
   565→
   566→    # Mass delete form wrapper
   567→    print '<form method="post" action="?action=mass_delete" id="mass-delete-form">';
   568→    print "<input type='hidden' name='zone_id' value='$zone_id'>";
   569→    print "<input type='hidden' name='domain' value='$domain'>";
   570→
   571→    print '
';
   572→    print '<button type="button" onclick="toggleAll()" class="btn btn-sm btn-secondary">Select All</button> ';
   573→    print '<button type="submit" class="btn btn-sm btn-danger" onclick="return confirmMassDelete()">Delete Selected</button>';
   574→    print '
';
   575→
   576→    print '
';
   577→    print '

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

';
   578→    print '
';
   579→
   580→    for my $rec (sort { $a->{type} cmp $b->{type} || $a->{name} cmp $b->{name} } @{$result->{result}}) {
   581→        my $content = length($rec->{content}) > 40 ? substr($rec->{content}, 0, 37) . '...' : $rec->{content};
   582→        my $ttl = $rec->{ttl} == 1 ? 'Auto' : $rec->{ttl};
   583→        my $proxied = $rec->{proxied} ? '✓' : '';
   584→
   585→        print "
";
   586→        print "<input type='checkbox' name='record_ids' value='$rec->{id}' class='record-checkbox'>	";
   587→        print "$rec->{type}	";
   588→        print "$rec->{name}	";
   589→        print "{content}' style='font-size:12px;'>$content	";
   590→        print "$ttl	";
   591→        print "$proxied	";
   592→        print "";
   593→        print "{id}' class='btn btn-sm btn-info'>Edit ";
   594→        print "{id}' class='btn btn-sm btn-danger' onclick='return confirm(\"Delete?\")'>Del";
   595→        print "	";
   596→        print "
";
   597→    }
   598→
   599→    print '

';
   600→    print '</form>';
   601→
   602→    # JavaScript for select all
   603→    print <<'JS';
   604→
   621→JS
   622→}
   623→
   624→# NEW: Edit record form
   625→sub edit_record {
   626→    my ($cgi, $token) = @_;
   627→
   628→    my $zone_id = $cgi->param('zone_id');
   629→    my $domain = $cgi->param('domain');
   630→    my $record_id = $cgi->param('record_id');
   631→
   632→    # Get current record
   633→    my $result = cf_api($token, 'GET', "/zones/$zone_id/dns_records/$record_id");
   634→
   635→    if (!$result->{success}) {
   636→        print '
Record not found
';
   637→        return;
   638→    }
   639→
   640→    my $rec = $result->{result};
   641→
   642→    print "
Edit Record
";
   643→    print "
← Back to $domain
";
   644→
   645→    print '<form method="post" action="?action=save_record" style="max-width: 600px;">';
   646→    print "<input type='hidden' name='zone_id' value='$zone_id'>";
   647→    print "<input type='hidden' name='domain' value='$domain'>";
   648→    print "<input type='hidden' name='record_id' value='$record_id'>";
   649→    print "<input type='hidden' name='type' value='$rec->{type}'>";
   650→
   651→    print '
';
   652→    print "Type: $rec->{type}";
   653→    print '
';
   654→
   655→    print '
';
   656→    print 'Name:
';
   657→    print "<input type='text' name='name' value='$rec->{name}' class='form-control' style='width:100%;'>";
   658→    print '
';
   659→
   660→    print '
';
   661→    print 'Content:
';
   662→    if ($rec->{type} eq 'TXT') {
   663→        print "<textarea name='content' class='form-control' style='width:100%; height:100px;'>$rec->{content}</textarea>";
   664→    } else {
   665→        print "<input type='text' name='content' value='$rec->{content}' class='form-control' style='width:100%;'>";
   666→    }
   667→    print '
';
   668→
   669→    print '
';
   670→    print 'TTL:
';
   671→    print '<select name="ttl" class="form-control" style="width:150px;">';
   672→    my @ttls = (1, 60, 120, 300, 600, 900, 1800, 3600, 7200, 18000, 43200, 86400);
   673→    my %ttl_labels = (1 => 'Auto', 60 => '1 min', 120 => '2 min', 300 => '5 min', 600 => '10 min',
   674→                      900 => '15 min', 1800 => '30 min', 3600 => '1 hour', 7200 => '2 hours',
   675→                      18000 => '5 hours', 43200 => '12 hours', 86400 => '1 day');
   676→    for my $t (@ttls) {
   677→        my $sel = $rec->{ttl} == $t ? 'selected' : '';
   678→        print "<option value='$t' $sel>$ttl_labels{$t}</option>";
   679→    }
   680→    print '</select>';
   681→    print '
';
   682→
   683→    if ($rec->{type} =~ /^(A|AAAA|CNAME)$/) {
   684→        my $checked = $rec->{proxied} ? 'checked' : '';
   685→        print '
';
   686→        print "<input type='checkbox' name='proxied' $checked> Proxied (orange cloud)";
   687→        print '
';
   688→    }
   689→
   690→    print '<button type="submit" class="btn btn-success">Save Changes</button>';
   691→    print " Cancel";
   692→    print '</form>';
   693→}
   694→
   695→# NEW: Save edited record
   696→sub save_record {
   697→    my ($cgi, $token) = @_;
   698→
   699→    my $zone_id = $cgi->param('zone_id');
   700→    my $domain = $cgi->param('domain');
   701→    my $record_id = $cgi->param('record_id');
   702→    my $type = $cgi->param('type');
   703→    my $name = $cgi->param('name');
   704→    my $content = $cgi->param('content');
   705→    my $ttl = $cgi->param('ttl') || 1;
   706→    my $proxied = $cgi->param('proxied') ? \1 : \0;
   707→
   708→    my $data = {
   709→        type => $type,
   710→        name => $name,
   711→        content => $content,
   712→        ttl => int($ttl),
   713→        proxied => $proxied
   714→    };
   715→
   716→    my $result = cf_api($token, 'PUT', "/zones/$zone_id/dns_records/$record_id", $data);
   717→
   718→    if ($result->{success}) {
   719→        print '
Record updated successfully.
';
   720→    } else {
   721→        my $err = $result->{errors}[0]{message} || 'Unknown error';
   722→        print "
Failed: $err
";
   723→    }
   724→
   725→    print "
Return to DNS Records
";
   726→}
   727→
   728→# NEW: Mass delete records
   729→sub mass_delete {
   730→    my ($cgi, $token) = @_;
   731→
   732→    my $zone_id = $cgi->param('zone_id');
   733→    my $domain = $cgi->param('domain');
   734→    my @record_ids = $cgi->multi_param('record_ids');
   735→
   736→    print "
Mass Delete Results
";
   737→
   738→    if (!@record_ids) {
   739→        print '
No records selected.
';
   740→        print "
Back to DNS Records
";
   741→        return;
   742→    }
   743→
   744→    print '
';
   745→    print '

Record ID	Status

';
   746→    print '
';
   747→
   748→    my $success = 0;
   749→    my $failed = 0;
   750→
   751→    for my $record_id (@record_ids) {
   752→        my $result = cf_api($token, 'DELETE', "/zones/$zone_id/dns_records/$record_id");
   753→
   754→        if ($result->{success}) {
   755→            print "
$record_id	Deleted
";
   756→            $success++;
   757→        } else {
   758→            print "
$record_id	Failed
";
   759→            $failed++;
   760→        }
   761→    }
   762→
   763→    print '

';
   764→    print "
Deleted: $success, Failed: $failed
";
   765→    print "
Return to DNS Records
";
   766→}
   767→
   768→sub add_record {
   769→    my ($cgi, $token) = @_;
   770→
   771→    my $zone_id = $cgi->param('zone_id');
   772→    my $domain = $cgi->param('domain');
   773→    my $type = $cgi->param('type');
   774→    my $name = $cgi->param('name');
   775→    my $content = $cgi->param('content');
   776→    my $proxied = $cgi->param('proxied') ? \1 : \0;
   777→
   778→    my $data = {
   779→        type => $type,
   780→        name => $name,
   781→        content => $content,
   782→        ttl => 1,
   783→        proxied => $proxied
   784→    };
   785→
   786→    my $result = cf_api($token, 'POST', "/zones/$zone_id/dns_records", $data);
   787→
   788→    if ($result->{success}) {
   789→        print '
Record added successfully.
';
   790→    } else {
   791→        my $err = $result->{errors}[0]{message} || 'Unknown error';
   792→        print "
Failed: $err
";
   793→    }
   794→
   795→    print "
Return to DNS Records
";
   796→}
   797→
   798→sub delete_record {
   799→    my ($cgi, $token) = @_;
   800→
   801→    my $zone_id = $cgi->param('zone_id');
   802→    my $domain = $cgi->param('domain');
   803→    my $record_id = $cgi->param('record_id');
   804→
   805→    my $result = cf_api($token, 'DELETE', "/zones/$zone_id/dns_records/$record_id");
   806→
   807→    if ($result->{success}) {
   808→        print '
Record deleted.
';
   809→    } else {
   810→        print '
Failed to delete record.
';
   811→    }
   812→
   813→    print "
Return to DNS Records
";
   814→}
   815→
   816→sub add_m365 {
   817→    my ($cgi, $token) = @_;
   818→
   819→    my $zone_id = $cgi->param('zone_id');
   820→    my $domain = $cgi->param('domain');
   821→    my $tenant = $cgi->param('tenant');
   822→
   823→    if (!$tenant) {
   824→        print "
Add M365 Records: $domain
";
   825→        print '
← Back
';
   826→
   827→        print '<form method="post" action="?action=add_m365" style="max-width: 500px;">';
   828→        print "<input type='hidden' name='zone_id' value='$zone_id'>";
   829→        print "<input type='hidden' name='domain' value='$domain'>";
   830→        print '
';
   831→        print 'Microsoft 365 Tenant Name
';
   832→        print '<input type="text" name="tenant" class="form-control" placeholder="e.g., contoso (from contoso.onmicrosoft.com)">';
   833→        print '
Enter the prefix of your .onmicrosoft.com domain
';
   834→        print '
';
   835→        print '
Records that will be added:
';
   836→        print '
';
   837→        print '
MX → mail.protection.outlook.com
';
   838→        print '
SPF (TXT) → include:spf.protection.outlook.com
';
   839→        print '
DMARC (TXT) → v=DMARC1; p=reject
';
   840→        print '
DKIM selector1 & selector2 (CNAME)
';
   841→        print '
Autodiscover (CNAME)
';
   842→        print '
';
   843→        print '<button type="submit" class="btn btn-primary">Add M365 Records</button>';
   844→        print '</form>';
   845→        return;
   846→    }
   847→
   848→    print "
Adding M365 Records: $domain
";
   849→
   850→    my $domain_clean = $domain;
   851→    $domain_clean =~ s/\./-/g;
   852→
   853→    my $tenant_clean = lc($tenant);
   854→    $tenant_clean =~ s/\.//g;
   855→
   856→    my @records = (
   857→        { type => 'MX', name => $domain, content => "$domain.mail.protection.outlook.com", priority => 0 },
   858→        { type => 'TXT', name => $domain, content => "v=spf1 include:spf.protection.outlook.com -all" },
   859→        { type => 'TXT', name => "_dmarc.$domain", content => "v=DMARC1; p=reject; rua=mailto:admin\@$domain" },
   860→        { type => 'CNAME', name => "selector1._domainkey.$domain", content => "selector1-${domain_clean}._domainkey.${tenant_clean}.onmicrosoft.com" },
   861→        { type => 'CNAME', name => "selector2._domainkey.$domain", content => "selector2-${domain_clean}._domainkey.${tenant_clean}.onmicrosoft.com" },
   862→        { type => 'CNAME', name => "autodiscover.$domain", content => 'autodiscover.outlook.com' },
   863→    );
   864→
   865→    print '
';
   866→    print '

Record	Status

';
   867→    print '
';
   868→
   869→    for my $rec (@records) {
   870→        my $result = cf_api($token, 'POST', "/zones/$zone_id/dns_records", $rec);
   871→        my $status = $result->{success} ? 'Added' : ($result->{errors}[0]{message} || 'Failed');
   872→        my $class = $result->{success} ? 'success' : 'warning';
   873→        print "
$rec->{type} $rec->{name}	$status
";
   874→    }
   875→
   876→    print '

';
   877→    print '
Remember to enable DKIM signing in Microsoft 365 Defender portal.
';
   878→    print "
View DNS Records
";
   879→}
   880→
   881→sub import_zone {
   882→    my ($cgi, $token) = @_;
   883→
   884→    my $domain = $cgi->param('domain');
   885→
   886→    if (!$domain) {
   887→        print '
Import Zone to Cloudflare
';
   888→        print '
← Back
';
   889→
   890→        print '<form method="post" action="?action=import_zone" style="max-width: 400px;">';
   891→        print '
';
   892→        print 'Domain Name
';
   893→        print '<input type="text" name="domain" class="form-control" placeholder="example.com">';
   894→        print '
';
   895→        print '<button type="submit" class="btn btn-primary">Import Zone</button>';
   896→        print '</form>';
   897→        return;
   898→    }
   899→
   900→    print "
Importing: $domain
";
   901→
   902→    my $result = cf_api($token, 'POST', '/zones', { name => $domain, jump_start => \1 });
   903→
   904→    if ($result->{success}) {
   905→        my $zone = $result->{result};
   906→        print '
Zone added successfully!
';
   907→        print "
Zone ID: $zone->{id}
";
   908→        print "
Status: $zone->{status}
";
   909→        print '
Update Nameservers
';
   910→        print '
Change nameservers at your registrar to:
';
   911→        print '
';
   912→        print "
$_
" for @{$zone->{name_servers}};
   913→        print '
';
   914→    } else {
   915→        my $err = $result->{errors}[0]{message} || 'Unknown error';
   916→        print "
Failed: $err
";
   917→    }
   918→
   919→    print '
View All Zones
';
   920→}
   921→
   922→# ============ HTML Templates ============
   923→
   924→sub print_html_header {
   925→    print <<'HTML';
   926→<!DOCTYPE html>
   927→
   928→
   929→    <title>Cloudflare DNS Manager</title>
   930→
   973→
   974→
   975→HTML
   976→}
   977→
   978→sub print_html_footer {
   979→    print <<'HTML';
   980→

   981→
Cloudflare DNS Manager v1.1 | AZ Computer Guru

   982→
   983→
   984→HTML
   985→}
   986→

#!/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
}

+# Cloudflare DNS Manager - WHM Plugin v1.1

+# Features: Zone transfer, mass delete, record editing

+    my $zonefile = "/var/named/$domain.db";

+    open my $fh, '<', $zonefile or return undef;

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

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

+        next if $line =~ /^\$TTL/; # Skip TTL directive

+        # Parse DNS records: name ttl class type content

+            $name = $origin if $name eq '@';

+            $name = "$name.$origin" unless $name =~ /\.$/;

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

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

+        my $result = `whmapi1 killdns domain=$domain 2>&1`;

+        my $marker = "v=cloudflare-dns; managed=true; transferred=" . time();

+        system("whmapi1 addzonerecord domain=$domain name=_cloudflare type=TXT txtdata=\"$marker\" 2>&1");

+    my $result = `whmapi1 listzones 2>&1`;

+    while ($result =~ /domain:\s*(\S+)/g) {

+    my $result = `dig +short TXT _cloudflare.$domain \@localhost 2>/dev/null`;

+    return $result =~ /cloudflare-dns/;

my $action = $cgi->param('action') || 'home';

print '<div class="alert alert-warning">';

print 'Setup Required: Please configure your Cloudflare API token.';

my $verify = cf_api($token, 'GET', '/user/tokens/verify');

print '
API Token is invalid or expired.
';

print '<div class="btn-group" style="margin: 20px 0;">';

-    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update API Token</button>';

+    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';

print '<div id="token-form" style="display:none;">';

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

+        my $active = scalar grep { $_->{status} eq 'active' } @{$cf_zones->{result}};

+        my $pending = scalar grep { $_->{status} eq 'pending' } @{$cf_zones->{result}};

+        print '<div style="margin-top: 20px;">';

+        print "
Cloudflare Zones: $active active, $pending pending
";

-    print '
Required permissions: Zone:Read, Zone:Edit, DNS:Read, DNS:Edit
';

+    print '
Required: Zone:Read, Zone:Edit, DNS:Read, DNS:Edit
';

-    print '<table class="table table-striped">';

-    for my $zone (@{$result->{result}}) {

+    for my $zone (sort { $a->{name} cmp $b->{name} } @{$result->{result}}) {

my $status_class = $zone->{status} eq 'active' ? 'success' : 'warning';

-        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-primary'>DNS Records ";

-        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-info'>+ M365";

+        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-primary'>DNS ";

+        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-info'>+M365";

+# NEW: WHM Zones list with transfer option

+    print '
Transfer zones from WHM to Cloudflare for centralized DNS management.
';

+    # Get Cloudflare zones for comparison

+    my $cf_result = cf_api($token, 'GET', '/zones?per_page=100');

+        $cf_zones{$_->{name}} = $_->{status} for @{$cf_result->{result}};

+    print '

Domain	WHM Status	Cloudflare Status	Actions

';

+        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' :

+        if (!exists $cf_zones{$domain}) {

+        } elsif ($cf_status eq 'active' && !$has_marker) {

+# NEW: Transfer zone from WHM to Cloudflare

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

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

+        # Just mark it, zone already in Cloudflare

+        print '
Zone already exists in Cloudflare. Choose how to handle WHM zone:
';

+        my $records = whm_get_zone($domain);

+            print '
Could not read zone file or no records found.
';

+        print '

Type	Name	Content	TTL

';

+            my $content = length($rec->{content}) > 60 ? substr($rec->{content}, 0, 57) . '...' : $rec->{content};

+            print "
$rec->{type}	$rec->{name}	$content	$rec->{ttl}
";

+        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 '<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 '<label><input type="radio" name="whm_action" value="none"> Do nothing (keep both)</label>';

+        print '<div class="alert alert-warning" style="margin-top: 15px;">';

+        print 'Important: After transfer, you must update nameservers at the registrar to point to Cloudflare.';

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

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

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

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

+        # 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 "<li>$_</li>" for @{$create_result->{result}{name_servers}};

+        } elsif ($create_result->{errors}[0]{code} == 1061) {

+            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
";

+            print "
Failed: $create_result->{errors}[0]{message}
";

+        print '
Step 2: Import DNS Records
';

+        my $records = whm_get_zone($domain);

+                content => $rec->{content},

+                $data->{priority} = int($1);

+            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 '
Skipped zone creation (already in Cloudflare)
';

+        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.
';

+            print '
Could not remove zone from WHM (may require manual removal).
';

+        print '
No changes made to WHM zone.
';

+    print '<div class="alert alert-warning" style="margin-top: 20px;">';

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

+    print '<p style="margin-top: 20px;">';

-    print '<table class="table table-striped">';

-    print '

Type	Name	Content	TTL	Proxied	Actions

';

+    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 '<div style="margin-bottom: 10px;">';

+    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 '

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

';

-        my $content = length($rec->{content}) > 50 ? substr($rec->{content}, 0, 47) . '...' : $rec->{content};

+        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 "<input type='checkbox' name='record_ids' value='$rec->{id}' class='record-checkbox'>	";

+        print "{content}' style='font-size:12px;'>$content	";

-        print "{id}' class='btn btn-sm btn-danger' onclick='return confirm(\"Delete this record?\")'>Delete	";

+        print "{id}' class='btn btn-sm btn-info'>Edit ";

+        print "{id}' class='btn btn-sm btn-danger' onclick='return confirm(\"Delete?\")'>Del";

+    var checkboxes = document.querySelectorAll('.record-checkbox');

+    var selectAll = document.getElementById('select-all');

+    for (var i = 0; i < checkboxes.length; i++) {

+        checkboxes[i].checked = selectAll.checked;

+    var checked = document.querySelectorAll('.record-checkbox:checked');

+    return confirm('Delete ' + checked.length + ' selected records?');

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

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

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

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

+    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 "<input type='text' name='name' value='$rec->{name}' class='form-control' style='width:100%;'>";

+        print "<textarea name='content' class='form-control' style='width:100%; height:100px;'>$rec->{content}</textarea>";

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

+    print '<select name="ttl" class="form-control" style="width:150px;">';

+        my $sel = $rec->{ttl} == $t ? 'selected' : '';

+        print "<option value='$t' $sel>$ttl_labels{$t}</option>";

+    if ($rec->{type} =~ /^(A|AAAA|CNAME)$/) {

+        my $checked = $rec->{proxied} ? 'checked' : '';

+        print '<div class="form-group">';

+        print "<label><input type='checkbox' name='proxied' $checked> Proxied (orange cloud)</label>";

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

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

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

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

+    my $content = $cgi->param('content');

+    my $proxied = $cgi->param('proxied') ? \1 : \0;

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

+        print '
Record updated successfully.
';

+        my $err = $result->{errors}[0]{message} || 'Unknown error';

+    my @record_ids = $cgi->multi_param('record_ids');

+        print '
No records selected.
';

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

+            print "
$record_id	Deleted
";

+            print "
$record_id	Failed
";

+    print "
Deleted: $success, Failed: $failed
";

print '
MX → mail.protection.outlook.com
';

print '
SPF (TXT) → include:spf.protection.outlook.com
';

print '
DMARC (TXT) → v=DMARC1; p=reject
';

-        print '
DKIM selector1 (CNAME)
';

-        print '
DKIM selector2 (CNAME)
';

+        print '
DKIM selector1 & selector2 (CNAME)
';

print '
Change nameservers at your registrar to:
';

-        for my $ns (@{$zone->{name_servers}}) {

+        print "
$_
" for @{$zone->{name_servers}};

my $err = $result->{errors}[0]{message} || 'Unknown error';

body { font-family: 'Roboto', Arial, sans-serif; padding: 20px; background: #fff; }

h1 { color: #333; border-bottom: 2px solid #f38020; padding-bottom: 10px; }

+        h3 { color: #555; margin-top: 20px; }

.alert { padding: 15px; margin: 15px 0; border-radius: 4px; }

.alert-success { background: #dff0d8; color: #3c763d; border: 1px solid #d6e9c6; }

.alert-warning { background: #fcf8e3; color: #8a6d3b; border: 1px solid #faebcc; }

.alert-danger { background: #f2dede; color: #a94442; border: 1px solid #ebccd1; }

.alert-info { background: #d9edf7; color: #31708f; border: 1px solid #bce8f1; }

.table { width: 100%; margin: 20px 0; border-collapse: collapse; }

-        .table th, .table td { padding: 10px; border: 1px solid #ddd; text-align: left; }

+        .table th, .table td { padding: 8px 10px; border: 1px solid #ddd; text-align: left; }

.table th { background: #f5f5f5; font-weight: bold; }

.table tr:nth-child(even) { background: #fafafa; }

.table tr.success { background: #dff0d8; }

.table tr.warning { background: #fcf8e3; }

-        .btn { padding: 8px 15px; border-radius: 4px; text-decoration: none; display: inline-block; margin: 2px; cursor: pointer; border: none; font-size: 14px; }

-        .btn-primary { background: #337ab7; color: white; }

+        .table tr.danger { background: #f2dede; }

+        .btn { padding: 8px 15px; border-radius: 4px; text-decoration: none; display: inline-block; margin: 2px; cursor: pointer; border: none; font-size: 14px; color: white; }

+        .btn-primary { background: #337ab7; }

.btn-primary:hover { background: #286090; }

-        .btn-success { background: #5cb85c; color: white; }

+        .btn-success { background: #5cb85c; }

.btn-success:hover { background: #449d44; }

-        .btn-danger { background: #d9534f; color: white; }

+        .btn-danger { background: #d9534f; }

.btn-danger:hover { background: #c9302c; }

-        .btn-info { background: #5bc0de; color: white; }

+        .btn-info { background: #5bc0de; }

.btn-info:hover { background: #31b0d5; }

-        .btn-secondary { background: #777; color: white; }

+        .btn-warning { background: #f0ad4e; }

+        .btn-warning:hover { background: #ec971f; }

+        .btn-secondary { background: #777; }

.btn-sm { padding: 4px 10px; font-size: 12px; }

.form-control { padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }

.label { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }

.label-success { background: #5cb85c; color: white; }

.label-warning { background: #f0ad4e; color: white; }

+        .label-info { background: #5bc0de; color: white; }

+        .label-default { background: #999; color: white; }

code { background: #f5f5f5; padding: 2px 5px; border-radius: 3px; font-family: monospace; }

.help-block { color: #777; font-size: 12px; margin-top: 5px; }

+        textarea { font-family: monospace; }

-
Cloudflare DNS Manager v1.0 | AZ Computer Guru

+
Cloudflare DNS Manager v1.1 | AZ Computer Guru

#!/usr/local/cpanel/3rdparty/bin/perl
#
# Cloudflare DNS Manager - WHM Plugin
# Uses cPanel's Perl and libraries
#

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 }] };
    }
}

# 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 'delete_record') {
    delete_record($cgi, $api_token);
}
elsif ($action eq 'add_m365') {
    add_m365($cgi, $api_token);
}
elsif ($action eq 'import_zone') {
    import_zone($cgi, $api_token);
}
else {
    print "
Unknown action: $action
";
}

print_html_footer();

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

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

    print '
Cloudflare DNS Manager
';

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

    # Verify token
    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
';

    # Show navigation
    print '
';
    print 'View All Zones ';
    print 'Import New Zone ';
    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update API Token</button>';
    print '
';
    print '
';
    show_token_form($config);
    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 permissions: 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 (@{$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 Records ";
        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-info'>+ M365";
        print "	";
        print "
";
    }

    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;
    }

    print '
';
    print '

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}) > 50 ? substr($rec->{content}, 0, 47) . '...' : $rec->{content};
        my $ttl = $rec->{ttl} == 1 ? 'Auto' : $rec->{ttl};
        my $proxied = $rec->{proxied} ? '✓' : '';

        print "
";
        print "$rec->{type}	";
        print "$rec->{name}	";
        print "{content}'>$content	";
        print "$ttl	";
        print "$proxied	";
        print "{id}' class='btn btn-sm btn-danger' onclick='return confirm(\"Delete this record?\")'>Delete	";
        print "
";
    }

    print '

';
}

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) {
        # Show form
        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 (CNAME)
';
        print '
DKIM selector2 (CNAME)
';
        print '
Autodiscover (CNAME)
';
        print '
';
        print '<button type="submit" class="btn btn-primary">Add M365 Records</button>';
        print '</form>';
        return;
    }

    # Process
    print "
Adding M365 Records: $domain
";

    # Clean names for DKIM
    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 '
';
        for my $ns (@{$zone->{name_servers}}) {
            print "
$ns
";
        }
        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.0 | AZ Computer Guru

HTML
}


Yes, but as I think about it, we may need to wait for DNS propagation when we change Glue records.  So perhaps we have a button in the plugin that allows us to remove the WHM zone after the fact.


Good point - the transfer shouldn't auto-remove the WHM zone since DNS propagation can take up to 48 hours. Let me upload this version and then we can test it. The current version already has the "marker" option as default (keeps the zone, just adds a TXT record), and the remove option is there but requires explicit selection.

Let me upload and verify:


Bash

Upload updated plugin v1.1


** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html


ssh root@ix.azcomputerguru.com "chmod 755 /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi && /usr/local/cpanel/3rdparty/bin/perl -c /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi 2>&1"

Verify syntax and permissions


/usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi syntax OK


Uploaded and syntax verified.

## Plugin v1.1 Features

### New Capabilities:

| Feature | Description |
|---------|-------------|
| **WHM Zones View** | Lists all WHM zones, shows transfer status, and Cloudflare status |
| **Zone Transfer** | Reads WHM zone file, previews records, imports to Cloudflare |
| **Post-Transfer Options** | Add marker TXT, remove zone, or do nothing (marker is default) |
| **Mass Delete** | Checkboxes on all records, "Select All" button, bulk delete |
| **Edit Records** | Edit button opens form to modify content, TTL, proxy status |

### Transfer Workflow:

1. **WHM Zones** → Shows all local zones
2. **Transfer to CF** → Preview records to import
3. **Options:**
   - Add `_cloudflare` TXT marker (default, keeps zone active)
   - Remove zone (for later, after propagation)
   - Do nothing
4. **After nameserver propagation** → Return to mark zone or remove it

Try refreshing WHM and accessing the plugin now. The home screen has a new **"WHM Zones"** button.


Also when we onboard a domain that is hosted either in house (whm) or externally, can we or are we cloning the original zone records?


Yes, the plugin does clone/import records in both scenarios:

## Current Record Import Behavior

| Source | How It Works |
|--------|--------------|
| **WHM (in-house)** | Reads `/var/named/domain.db` zone file, parses all A, AAAA, CNAME, TXT, MX, SRV records, imports to Cloudflare |
| **External (GoDaddy, etc.)** | Uses Cloudflare's `jump_start: true` which auto-scans existing DNS and imports what it finds |

### The Problem with External Domains

Cloudflare's `jump_start` scan is **not reliable** - it often misses records, especially:
- TXT records (SPF, DKIM, DMARC)
- SRV records
- Less common CNAMEs

That's why you had to manually provide the GoDaddy zone export for bgbuildersllc.com earlier.

## Better Approach: Zone File Import

I can add a feature to **paste a zone file export** (like the one you provided from GoDaddy) and parse/import all records. This works for any external registrar.

Want me to add:

1. **"Import from Zone File"** - Paste BIND-format zone export, parses and imports all records
2. **"Scan External DNS"** - Query the domain's current nameservers via dig and import discovered records (more reliable than Cloudflare's jump_start)

Both would give you a preview of records before importing, just like the WHM transfer does.

Which would be more useful, or both?


Both


Let me add both features to the plugin:


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→# Main
   158→my $cgi = CGI->new;
   159→my $action = $cgi->param('action') || 'home';
   160→
   161→# Print header
   162→print $cgi->header('text/html');
   163→
   164→# Load config
   165→my $config = load_config();
   166→my $api_token = $config->{api_token} || '';
   167→
   168→# HTML header
   169→print_html_header();
   170→
   171→# Route actions
   172→if ($action eq 'home') {
   173→    show_home($cgi, $config, $api_token);
   174→}
   175→elsif ($action eq 'save_token') {
   176→    save_token($cgi, $config);
   177→}
   178→elsif ($action eq 'list_zones') {
   179→    list_zones($api_token);
   180→}
   181→elsif ($action eq 'view_zone') {
   182→    view_zone($cgi, $api_token);
   183→}
   184→elsif ($action eq 'add_record') {
   185→    add_record($cgi, $api_token);
   186→}
   187→elsif ($action eq 'edit_record') {
   188→    edit_record($cgi, $api_token);
   189→}
   190→elsif ($action eq 'save_record') {
   191→    save_record($cgi, $api_token);
   192→}
   193→elsif ($action eq 'delete_record') {
   194→    delete_record($cgi, $api_token);
   195→}
   196→elsif ($action eq 'mass_delete') {
   197→    mass_delete($cgi, $api_token);
   198→}
   199→elsif ($action eq 'add_m365') {
   200→    add_m365($cgi, $api_token);
   201→}
   202→elsif ($action eq 'import_zone') {
   203→    import_zone($cgi, $api_token);
   204→}
   205→elsif ($action eq 'transfer_zone') {
   206→    transfer_zone($cgi, $api_token);
   207→}
   208→elsif ($action eq 'do_transfer') {
   209→    do_transfer($cgi, $api_token);
   210→}
   211→elsif ($action eq 'whm_zones') {
   212→    whm_zones($cgi, $api_token);
   213→}
   214→else {
   215→    print "
Unknown action: $action
";
   216→}
   217→
   218→print_html_footer();
   219→
   220→# ============ Actions ============
   221→
   222→sub show_home {
   223→    my ($cgi, $config, $token) = @_;
   224→
   225→    print '
Cloudflare DNS Manager
';
   226→
   227→    if (!$token) {
   228→        print '
';
   229→        print 'Setup Required: Please configure your Cloudflare API token.';
   230→        print '
';
   231→        show_token_form($config);
   232→        return;
   233→    }
   234→
   235→    my $verify = cf_api($token, 'GET', '/user/tokens/verify');
   236→    if (!$verify->{success}) {
   237→        print '
API Token is invalid or expired.
';
   238→        show_token_form($config);
   239→        return;
   240→    }
   241→
   242→    print '
✓ Connected to Cloudflare
';
   243→
   244→    print '
';
   245→    print 'Cloudflare Zones ';
   246→    print 'WHM Zones ';
   247→    print 'Import New Zone ';
   248→    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update Token</button>';
   249→    print '
';

<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>

# 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 '
';


I'll add both features. Let me update the plugin with:
1. **Import from Zone File** - Paste BIND format zone export
2. **Scan External DNS** - Query current nameservers via dig