ssh root@ix.azcomputerguru.com "cat /var/cpanel/apps/imunify360.conf"

Check imunify360 config format


# The name of the plugin
name=imunify360

# Service that will run the plugin/app
service=whostmgr

# User to run the plugin as (root)
user=root

# URL where the plugin program will be stored/called from (relative to /usr/local/cpanel/docroot/)
url=/cgi/imunify

# Required ACL's
acls=software-imunify360

# Display name within the WHM (Plugins section)
displayname=Imunify360

# URL to show in WHM (again, relative to where the plugin/app is installed)
entryurl=imunify/handlers/index.cgi

# Icon that will show up in WHM => Plugins section
icon=imunify360-logo.png

# The targeted browser window in which to open the application link
target=_self

# Plugin search keyword
searchtext="firewall security"


Bash

ssh root@ix.azcomputerguru.com 'cat > /var/cpanel/apps/cloudflare_dns.conf << "EOF"
# The name of the plugin
name=cloudflare_dns

# Service that will run the plugin/app
service=whostmgr

# User to run the plugin as (root)
user=root

# URL where the plugin program will be stored/called from
url=/cgi

# Required ACLs
acls=all

# Display name within WHM (Plugins section)
displayname=Cloudflare DNS Manager

# URL to show in WHM
entryurl=addon_cloudflareDNS.cgi

# Icon that will show up in WHM => Plugins section
icon=icon_cloudflare_dns.svg

# The targeted browser window
target=_self

# Plugin search keywords
searchtext=cloudflare dns
EOF'

Update config to match imunify360 format exactly


Rebuild WHM chrome again


Error: Exit code 127
bash: line 1: C:/Program: No such file or directory


Rebuild WHM chrome


Something's off with the SSH command. Let me try a different approach:


ssh root@ix.azcomputerguru.com "bash -c '/usr/local/cpanel/scripts/rebuild_whm_chrome'"

Run rebuild with explicit bash


ssh root@ix.azcomputerguru.com "echo done"

Test basic SSH


done


ssh root@ix.azcomputerguru.com "sh -c '/usr/local/cpanel/scripts/rebuild_whm_chrome && echo rebuilt'"

Rebuild with sh


rebuilt


Done. Try refreshing WHM now - look in the **Plugins** section or search for "cloudflare".

If it still doesn't appear, you can also access it directly at:
```
https://ix.azcomputerguru.com:2087/cgi/addon_cloudflareDNS.cgi
```


CGI error - let me check the error log and fix the script:


ssh root@ix.azcomputerguru.com "tail -50 /usr/local/cpanel/logs/error_log | grep -i cloudflare"

Check cPanel error log


[2025-12-19 15:23:51 -0700] invalid [cpsrvd] /var/cpanel/apps/cloudflare_dns.conf is an invalid application configuration file. It must specify 'service' and 'url'
Can't locate CGI.pm in @INC (you may need to install the CGI module) (@INC contains: /var/cpanel/plugins/cloudflare_dns/lib /usr/local/cpanel/3rdparty/perl/514/lib64/perl5/cpanel_lib /usr/local/lib64/perl5/5.32 /usr/local/share/perl5/5.32 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5) at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
BEGIN failed--compilation aborted at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
[2025-12-19 15:28:42 -0700] warn [whostmgrd] The subprocess (/usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi) exited with an error (ID 577660834257589): The subprocess reported error number 2 when it ended.
[2025-12-19 15:28:42 -0700] info [whostmgrd] Internal Server Error: "GET /cpsess6605694970/cgi/addon_cloudflareDNS.cgi HTTP/1.1" 500 Internal Error (ID 577660834257589)
Can't locate CGI.pm in @INC (you may need to install the CGI module) (@INC contains: /var/cpanel/plugins/cloudflare_dns/lib /usr/local/cpanel/3rdparty/perl/514/lib64/perl5/cpanel_lib /usr/local/lib64/perl5/5.32 /usr/local/share/perl5/5.32 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5) at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
BEGIN failed--compilation aborted at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
[2025-12-19 15:28:47 -0700] warn [whostmgrd] The subprocess (/usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi) exited with an error (ID 929320677955733): The subprocess reported error number 2 when it ended.
[2025-12-19 15:28:47 -0700] info [whostmgrd] Internal Server Error: "GET /cpsess6605694970/cgi/addon_cloudflareDNS.cgi HTTP/1.1" 500 Internal Error (ID 929320677955733)
Can't locate CGI.pm in @INC (you may need to install the CGI module) (@INC contains: /var/cpanel/plugins/cloudflare_dns/lib /usr/local/cpanel/3rdparty/perl/514/lib64/perl5/cpanel_lib /usr/local/lib64/perl5/5.32 /usr/local/share/perl5/5.32 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5) at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
BEGIN failed--compilation aborted at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
[2025-12-19 15:31:11 -0700] warn [whostmgrd] The subprocess (/usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi) exited with an error (ID 916059621217759): The subprocess reported error number 2 when it ended.
[2025-12-19 15:31:11 -0700] info [whostmgrd] Internal Server Error: "GET /cpsess6605694970/cgi/addon_cloudflareDNS.cgi HTTP/1.1" 500 Internal Error (ID 916059621217759)
Can't locate CGI.pm in @INC (you may need to install the CGI module) (@INC contains: /var/cpanel/plugins/cloudflare_dns/lib /usr/local/cpanel/3rdparty/perl/514/lib64/perl5/cpanel_lib /usr/local/lib64/perl5/5.32 /usr/local/share/perl5/5.32 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5) at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
BEGIN failed--compilation aborted at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
[2025-12-19 15:31:18 -0700] warn [whostmgrd] The subprocess (/usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi) exited with an error (ID 891989475655468): The subprocess reported error number 2 when it ended.
[2025-12-19 15:31:18 -0700] info [whostmgrd] Internal Server Error: "GET /cpsess6605694970/cgi/addon_cloudflareDNS.cgi HTTP/1.1" 500 Internal Error (ID 891989475655468)
Can't locate CGI.pm in @INC (you may need to install the CGI module) (@INC contains: /var/cpanel/plugins/cloudflare_dns/lib /usr/local/cpanel/3rdparty/perl/514/lib64/perl5/cpanel_lib /usr/local/lib64/perl5/5.32 /usr/local/share/perl5/5.32 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5) at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
BEGIN failed--compilation aborted at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
[2025-12-19 15:32:06 -0700] warn [whostmgrd] The subprocess (/usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi) exited with an error (ID 525192074085684): The subprocess reported error number 2 when it ended.
[2025-12-19 15:32:06 -0700] info [whostmgrd] Internal Server Error: "GET /cpsess6605694970/cgi/addon_cloudflareDNS.cgi HTTP/1.1" 500 Internal Error (ID 525192074085684)
Can't locate CGI.pm in @INC (you may need to install the CGI module) (@INC contains: /var/cpanel/plugins/cloudflare_dns/lib /usr/local/cpanel/3rdparty/perl/514/lib64/perl5/cpanel_lib /usr/local/lib64/perl5/5.32 /usr/local/share/perl5/5.32 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5) at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
BEGIN failed--compilation aborted at /usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi line 12.
[2025-12-19 15:32:14 -0700] warn [whostmgrd] The subprocess (/usr/local/cpanel/whostmgr/docroot/cgi/addon_cloudflareDNS.cgi) exited with an error (ID 689735180306347): The subprocess reported error number 2 when it ended.
[2025-12-19 15:32:14 -0700] info [whostmgrd] Internal Server Error: "GET /cpsess6605694970/cgi/addon_cloudflareDNS.cgi HTTP/1.1" 500 Internal Error (ID 689735180306347)


Found it - **CGI.pm is not in the cPanel Perl path**. cPanel uses its own Perl. Let me fix the script to use cPanel's Perl properly:


ssh root@ix.azcomputerguru.com "find /usr/local/cpanel -name 'CGI.pm' 2>/dev/null | head -3"

Find CGI.pm location


/usr/local/cpanel/3rdparty/perl/542/cpanel-lib/HTTP/Server/Simple/CGI.pm
/usr/local/cpanel/3rdparty/perl/542/cpanel-lib/HTTP/Response/CGI.pm
/usr/local/cpanel/3rdparty/perl/542/cpanel-lib/CGI.pm


cPanel uses Perl 5.42 with its own CGI.pm. Let me rewrite the CGI script to use cPanel's Perl interpreter:


Internal Server Error

500

Internal Error (ID 257220076470734)

cpsrvd Server at ix.azcomputerguru.com


Write

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


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
     4→# Uses cPanel's Perl and libraries
     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→# Main
    68→my $cgi = CGI->new;
    69→my $action = $cgi->param('action') || 'home';
    70→
    71→# Print header
    72→print $cgi->header('text/html');
    73→
    74→# Load config
    75→my $config = load_config();
    76→my $api_token = $config->{api_token} || '';
    77→
    78→# HTML header
    79→print_html_header();
    80→
    81→# Route actions
    82→if ($action eq 'home') {
    83→    show_home($cgi, $config, $api_token);
    84→}
    85→elsif ($action eq 'save_token') {
    86→    save_token($cgi, $config);
    87→}
    88→elsif ($action eq 'list_zones') {
    89→    list_zones($api_token);
    90→}
    91→elsif ($action eq 'view_zone') {
    92→    view_zone($cgi, $api_token);
    93→}
    94→elsif ($action eq 'add_record') {
    95→    add_record($cgi, $api_token);
    96→}
    97→elsif ($action eq 'delete_record') {
    98→    delete_record($cgi, $api_token);
    99→}
   100→elsif ($action eq 'add_m365') {
   101→    add_m365($cgi, $api_token);
   102→}
   103→elsif ($action eq 'import_zone') {
   104→    import_zone($cgi, $api_token);
   105→}
   106→else {
   107→    print "
Unknown action: $action
";
   108→}
   109→
   110→print_html_footer();
   111→
   112→# ============ Actions ============
   113→
   114→sub show_home {
   115→    my ($cgi, $config, $token) = @_;
   116→
   117→    print '
Cloudflare DNS Manager
';
   118→
   119→    # Check if token is configured
   120→    if (!$token) {
   121→        print '
';
   122→        print 'Setup Required: Please configure your Cloudflare API token.';
   123→        print '
';
   124→        show_token_form($config);
   125→        return;
   126→    }
   127→
   128→    # Verify token
   129→    my $verify = cf_api($token, 'GET', '/user/tokens/verify');
   130→    if (!$verify->{success}) {
   131→        print '
API Token is invalid or expired.
';
   132→        show_token_form($config);
   133→        return;
   134→    }
   135→
   136→    print '
✓ Connected to Cloudflare
';
   137→
   138→    # Show navigation
   139→    print '
';
   140→    print 'View All Zones ';
   141→    print 'Import New Zone ';
   142→    print '<button onclick="document.getElementById(\'token-form\').style.display=\'block\'" class="btn btn-secondary">Update API Token</button>';
   143→    print '
';
   144→    print '
';
   145→    show_token_form($config);
   146→    print '
';
   147→}
   148→
   149→sub show_token_form {
   150→    my ($config) = @_;
   151→    my $current = $config->{api_token} ? '********' . substr($config->{api_token}, -4) : 'Not set';
   152→
   153→    print '<form method="post" action="?action=save_token" style="margin: 20px 0; padding: 20px; background: #f5f5f5; border-radius: 5px;">';
   154→    print '
Cloudflare API Token
';
   155→    print "
Current: $current
";
   156→    print '
';
   157→    print 'API Token:
';
   158→    print '<input type="password" name="api_token" class="form-control" style="width: 400px;" placeholder="Enter Cloudflare API token">';
   159→    print '
';
   160→    print '<button type="submit" class="btn btn-success">Save Token</button>';
   161→    print '
Required permissions: Zone:Read, Zone:Edit, DNS:Read, DNS:Edit
';
   162→    print '</form>';
   163→}
   164→
   165→sub save_token {
   166→    my ($cgi, $config) = @_;
   167→    my $token = $cgi->param('api_token');
   168→
   169→    if ($token) {
   170→        $config->{api_token} = $token;
   171→        save_config($config);
   172→        print '
API Token saved successfully.
';
   173→        print '
Return to Home
';
   174→    } else {
   175→        print '
No token provided.
';
   176→    }
   177→}
   178→
   179→sub list_zones {
   180→    my ($token) = @_;
   181→
   182→    print '
Cloudflare Zones
';
   183→    print '
← Back
';
   184→
   185→    my $result = cf_api($token, 'GET', '/zones?per_page=50');
   186→
   187→    if (!$result->{success}) {
   188→        print '
Failed to load zones
';
   189→        return;
   190→    }
   191→
   192→    print '
';
   193→    print '

Domain	Status	Plan	Actions

';
   194→    print '
';
   195→
   196→    for my $zone (@{$result->{result}}) {
   197→        my $status_class = $zone->{status} eq 'active' ? 'success' : 'warning';
   198→        print "
";
   199→        print "$zone->{name}	";
   200→        print "$zone->{status}	";
   201→        print "$zone->{plan}{name}	";
   202→        print "";
   203→        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-primary'>DNS Records ";
   204→        print "{id}&domain=$zone->{name}' class='btn btn-sm btn-info'>+ M365";
   205→        print "	";
   206→        print "
";
   207→    }
   208→
   209→    print '

';
   210→}
   211→
   212→sub view_zone {
   213→    my ($cgi, $token) = @_;
   214→
   215→    my $zone_id = $cgi->param('zone_id');
   216→    my $domain = $cgi->param('domain');
   217→
   218→    print "
DNS Records: $domain
";
   219→    print '
← Back to Zones
';
   220→
   221→    # Add record form
   222→    print '
';
   223→    print '
Add Record
';
   224→    print '<form method="post" action="?action=add_record">';
   225→    print "<input type='hidden' name='zone_id' value='$zone_id'>";
   226→    print "<input type='hidden' name='domain' value='$domain'>";
   227→    print '<select name="type" class="form-control" style="width:100px; display:inline-block;">';
   228→    print '<option>A</option><option>AAAA</option><option>CNAME</option><option>TXT</option><option>MX</option>';
   229→    print '</select> ';
   230→    print '<input type="text" name="name" placeholder="Name" class="form-control" style="width: 150px; display:inline-block;"> ';
   231→    print '<input type="text" name="content" placeholder="Content" class="form-control" style="width: 300px; display:inline-block;"> ';
   232→    print '<input type="checkbox" name="proxied"> Proxied ';
   233→    print '<button type="submit" class="btn btn-success">Add</button>';
   234→    print '</form>
';
   235→
   236→    my $result = cf_api($token, 'GET', "/zones/$zone_id/dns_records?per_page=100");
   237→
   238→    if (!$result->{success}) {
   239→        print '
Failed to load records
';
   240→        return;
   241→    }
   242→
   243→    print '
';
   244→    print '

Type	Name	Content	TTL	Proxied	Actions

';
   245→    print '
';
   246→
   247→    for my $rec (sort { $a->{type} cmp $b->{type} || $a->{name} cmp $b->{name} } @{$result->{result}}) {
   248→        my $content = length($rec->{content}) > 50 ? substr($rec->{content}, 0, 47) . '...' : $rec->{content};
   249→        my $ttl = $rec->{ttl} == 1 ? 'Auto' : $rec->{ttl};
   250→        my $proxied = $rec->{proxied} ? '✓' : '';
   251→
   252→        print "
";
   253→        print "$rec->{type}	";
   254→        print "$rec->{name}	";
   255→        print "{content}'>$content	";
   256→        print "$ttl	";
   257→        print "$proxied	";
   258→        print "{id}' class='btn btn-sm btn-danger' onclick='return confirm(\"Delete this record?\")'>Delete	";
   259→        print "
";
   260→    }
   261→
   262→    print '

';
   263→}
   264→
   265→sub add_record {
   266→    my ($cgi, $token) = @_;
   267→
   268→    my $zone_id = $cgi->param('zone_id');
   269→    my $domain = $cgi->param('domain');
   270→    my $type = $cgi->param('type');
   271→    my $name = $cgi->param('name');
   272→    my $content = $cgi->param('content');
   273→    my $proxied = $cgi->param('proxied') ? \1 : \0;
   274→
   275→    my $data = {
   276→        type => $type,
   277→        name => $name,
   278→        content => $content,
   279→        ttl => 1,
   280→        proxied => $proxied
   281→    };
   282→
   283→    my $result = cf_api($token, 'POST', "/zones/$zone_id/dns_records", $data);
   284→
   285→    if ($result->{success}) {
   286→        print '
Record added successfully.
';
   287→    } else {
   288→        my $err = $result->{errors}[0]{message} || 'Unknown error';
   289→        print "
Failed: $err
";
   290→    }
   291→
   292→    print "
Return to DNS Records
";
   293→}
   294→
   295→sub delete_record {
   296→    my ($cgi, $token) = @_;
   297→
   298→    my $zone_id = $cgi->param('zone_id');
   299→    my $domain = $cgi->param('domain');
   300→    my $record_id = $cgi->param('record_id');
   301→
   302→    my $result = cf_api($token, 'DELETE', "/zones/$zone_id/dns_records/$record_id");
   303→
   304→    if ($result->{success}) {
   305→        print '
Record deleted.
';
   306→    } else {
   307→        print '
Failed to delete record.
';
   308→    }
   309→
   310→    print "
Return to DNS Records
";
   311→}
   312→
   313→sub add_m365 {
   314→    my ($cgi, $token) = @_;
   315→
   316→    my $zone_id = $cgi->param('zone_id');
   317→    my $domain = $cgi->param('domain');
   318→    my $tenant = $cgi->param('tenant');
   319→
   320→    if (!$tenant) {
   321→        # Show form
   322→        print "
Add M365 Records: $domain
";
   323→        print '
← Back
';
   324→
   325→        print '<form method="post" action="?action=add_m365" style="max-width: 500px;">';
   326→        print "<input type='hidden' name='zone_id' value='$zone_id'>";
   327→        print "<input type='hidden' name='domain' value='$domain'>";
   328→        print '
';
   329→        print 'Microsoft 365 Tenant Name
';
   330→        print '<input type="text" name="tenant" class="form-control" placeholder="e.g., contoso (from contoso.onmicrosoft.com)">';
   331→        print '
Enter the prefix of your .onmicrosoft.com domain
';
   332→        print '
';
   333→        print '
Records that will be added:
';
   334→        print '
';
   335→        print '
MX → mail.protection.outlook.com
';
   336→        print '
SPF (TXT) → include:spf.protection.outlook.com
';
   337→        print '
DMARC (TXT) → v=DMARC1; p=reject
';
   338→        print '
DKIM selector1 (CNAME)
';
   339→        print '
DKIM selector2 (CNAME)
';
   340→        print '
Autodiscover (CNAME)
';
   341→        print '
';
   342→        print '<button type="submit" class="btn btn-primary">Add M365 Records</button>';
   343→        print '</form>';
   344→        return;
   345→    }
   346→
   347→    # Process
   348→    print "
Adding M365 Records: $domain
";
   349→
   350→    # Clean names for DKIM
   351→    my $domain_clean = $domain;
   352→    $domain_clean =~ s/\./-/g;
   353→
   354→    my $tenant_clean = lc($tenant);
   355→    $tenant_clean =~ s/\.//g;
   356→
   357→    my @records = (
   358→        { type => 'MX', name => $domain, content => "$domain.mail.protection.outlook.com", priority => 0 },
   359→        { type => 'TXT', name => $domain, content => "v=spf1 include:spf.protection.outlook.com -all" },
   360→        { type => 'TXT', name => "_dmarc.$domain", content => "v=DMARC1; p=reject; rua=mailto:admin\@$domain" },
   361→        { type => 'CNAME', name => "selector1._domainkey.$domain", content => "selector1-${domain_clean}._domainkey.${tenant_clean}.onmicrosoft.com" },
   362→        { type => 'CNAME', name => "selector2._domainkey.$domain", content => "selector2-${domain_clean}._domainkey.${tenant_clean}.onmicrosoft.com" },
   363→        { type => 'CNAME', name => "autodiscover.$domain", content => 'autodiscover.outlook.com' },
   364→    );
   365→
   366→    print '
';
   367→    print '

Record	Status

';
   368→    print '
';
   369→
   370→    for my $rec (@records) {
   371→        my $result = cf_api($token, 'POST', "/zones/$zone_id/dns_records", $rec);
   372→        my $status = $result->{success} ? 'Added' : ($result->{errors}[0]{message} || 'Failed');
   373→        my $class = $result->{success} ? 'success' : 'warning';
   374→        print "
$rec->{type} $rec->{name}	$status
";
   375→    }
   376→
   377→    print '

';
   378→    print '
Remember to enable DKIM signing in Microsoft 365 Defender portal.
';
   379→    print "
View DNS Records
";
   380→}
   381→
   382→sub import_zone {
   383→    my ($cgi, $token) = @_;
   384→
   385→    my $domain = $cgi->param('domain');
   386→
   387→    if (!$domain) {
   388→        print '
Import Zone to Cloudflare
';
   389→        print '
← Back
';
   390→
   391→        print '<form method="post" action="?action=import_zone" style="max-width: 400px;">';
   392→        print '
';
   393→        print 'Domain Name
';
   394→        print '<input type="text" name="domain" class="form-control" placeholder="example.com">';
   395→        print '
';
   396→        print '<button type="submit" class="btn btn-primary">Import Zone</button>';
   397→        print '</form>';
   398→        return;
   399→    }
   400→
   401→    print "
Importing: $domain
";
   402→
   403→    my $result = cf_api($token, 'POST', '/zones', { name => $domain, jump_start => \1 });
   404→
   405→    if ($result->{success}) {
   406→        my $zone = $result->{result};
   407→        print '
Zone added successfully!
';
   408→        print "
Zone ID: $zone->{id}
";
   409→        print "
Status: $zone->{status}
";
   410→        print '
Update Nameservers
';
   411→        print '
Change nameservers at your registrar to:
';
   412→        print '
';
   413→        for my $ns (@{$zone->{name_servers}}) {
   414→            print "
$ns
";
   415→        }
   416→        print '
';
   417→    } else {
   418→        my $err = $result->{errors}[0]{message} || 'Unknown error';
   419→        print "
Failed: $err
";
   420→    }
   421→
   422→    print '
View All Zones
';
   423→}
   424→
   425→# ============ HTML Templates ============
   426→
   427→sub print_html_header {
   428→    print <<'HTML';
   429→<!DOCTYPE html>
   430→
   431→
   432→    <title>Cloudflare DNS Manager</title>
   433→
   469→
   470→
   471→HTML
   472→}
   473→
   474→sub print_html_footer {
   475→    print <<'HTML';
   476→

   477→
Cloudflare DNS Manager v1.0 | AZ Computer Guru

   478→
   479→
   480→HTML
   481→}
   482→

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

+    unshift @INC, '/usr/local/cpanel/3rdparty/perl/542/cpanel-lib';

+    unshift @INC, '/var/cpanel/plugins/cloudflare_dns/lib';

my $CONFIG_FILE = '/var/cpanel/plugins/cloudflare_dns/config.json';

+        return eval { decode_json($json) } || {};

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

+        $req->content(encode_json($data));

+        return decode_json($res->content);

+        return { success => 0, errors => [{ message => $res->status_line }] };

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

my $api_token = $config->{api_token} || '';

-my $cf = CloudflareDNS->new(api_token => $api_token);

+    show_home($cgi, $config, $api_token);

-    Setup Required: Please configure your Cloudflare API token.

+        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 '
✓ Connected to Cloudflare
';

+    print '
✓ Connected to Cloudflare
';

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

-    <button onclick="toggleTokenForm()" class="btn btn-secondary">Update API Token</button>

+    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 '<div id="token-form" style="display:none;">';

-    my $current = $config->{api_token} ? '********' . substr($config->{api_token}, -4) : '';

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

-<form method="post" action="?action=save_token" class="form-inline" style="margin: 20px 0; padding: 20px; background: #f5f5f5; border-radius: 5px;">

-        <input type="password" name="api_token" class="form-control" style="width: 400px;" placeholder="Enter Cloudflare API token">

-    <button type="submit" class="btn btn-success">Save Token</button>

-    <p style="margin-top: 10px; color: #666;">

-        Required permissions: Zone:Read, Zone:Edit, DNS:Read, DNS:Edit

+    print '<form method="post" action="?action=save_token" style="margin: 20px 0; padding: 20px; background: #f5f5f5; border-radius: 5px;">';

+    print '<input type="password" name="api_token" class="form-control" style="width: 400px;" placeholder="Enter Cloudflare API token">';

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

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

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

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

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

-<div style="background: #f5f5f5; padding: 15px; margin: 20px 0; border-radius: 5px;">

-    <form method="post" action="?action=add_record" class="form-inline">

-        <input type="hidden" name="zone_id" value="$zone_id">

-        <input type="hidden" name="domain" value="$domain">

-        <select name="type" class="form-control">

-        <input type="text" name="name" placeholder="Name (e.g., www)" class="form-control" style="width: 150px;">

-        <input type="text" name="content" placeholder="Content" class="form-control" style="width: 300px;">

-        <label><input type="checkbox" name="proxied"> Proxied</label>

-        <button type="submit" class="btn btn-success">Add</button>

+    print '<div style="background: #f5f5f5; padding: 15px; margin: 20px 0; border-radius: 5px;">';

+    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 '<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 '<label style="margin: 0 10px;"><input type="checkbox" name="proxied"> Proxied</label> ';

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

-    my $result = $cf->list_records($zone_id);

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

my $ttl = $rec->{ttl} == 1 ? 'Auto' : $rec->{ttl};

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

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

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

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

-    my $result = $cf->add_record($zone_id,

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

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

-    my $result = $cf->delete_record($zone_id, $record_id);

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

-<form method="post" action="?action=add_m365" style="max-width: 500px;">

-    <input type="hidden" name="zone_id" value="$zone_id">

-    <input type="hidden" name="domain" value="$domain">

-        <label>Microsoft 365 Tenant Name</label>

-        <input type="text" name="tenant" class="form-control" placeholder="e.g., contoso (from contoso.onmicrosoft.com)">

-
Enter the prefix of your .onmicrosoft.com domain

-
SPF (TXT) → include:spf.protection.outlook.com

-    <button type="submit" class="btn btn-primary">Add M365 Records</button>

+        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 '<div class="form-group">';

+        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 '
Records that will be added:
';

+        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 '<button type="submit" class="btn btn-primary">Add M365 Records</button>';

-    my $result = $cf->add_m365_records($domain, $tenant);

-        for my $r (@{$result->{results}}) {

-            my $class = $r->{success} ? 'success' : 'warning';

-            print "
$r->{record}	$r->{message}
";

-        print '
Remember to enable DKIM signing in Microsoft 365 Defender portal.
';

-        print '
Failed to add 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' },

+        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 '
Remember to enable DKIM signing in Microsoft 365 Defender portal.
';

-<form method="post" action="?action=import_zone" style="max-width: 400px;">

-        <input type="text" name="domain" class="form-control" placeholder="example.com">

-    <button type="submit" class="btn btn-primary">Import Zone</button>

+        print '<form method="post" action="?action=import_zone" style="max-width: 400px;">';

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

+        print '<input type="text" name="domain" class="form-control" placeholder="example.com">';

+        print '<button type="submit" class="btn btn-primary">Import Zone</button>';

-    my $result = $cf->create_zone($domain);

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

-    <link rel="stylesheet" href="/themes/starter/fonts/roboto/roboto.min.css">

-    <link rel="stylesheet" href="/themes/starter/libraries/bootstrap/css/bootstrap.min.css">

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

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

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

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

-        .alert-success { background: #dff0d8; color: #3c763d; }

-        .alert-warning { background: #fcf8e3; color: #8a6d3b; }

-        .alert-danger { background: #f2dede; color: #a94442; }

-        .alert-info { background: #d9edf7; color: #31708f; }

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

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

-        .table th { background: #f5f5f5; }

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

+        .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 { 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; }

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

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

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

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

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

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

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

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

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

-        .form-control { padding: 8px; border: 1px solid #ccc; border-radius: 4px; margin: 5px; }

-        .form-group { margin: 10px 0; }

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

+        .form-group { margin: 15px 0; }

.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }

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

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

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

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

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

-            var form = document.getElementById('token-form');

-            form.style.display = form.style.display === 'none' ? 'block' : 'none';

Cloudflare DNS Manager v1.0 | AZ Computer Guru

#!/usr/bin/perl
#
# Cloudflare DNS Manager - WHM Plugin
# Main CGI interface
#

use strict;
use warnings;
use lib '/usr/local/cpanel/3rdparty/perl/514/lib64/perl5/cpanel_lib';
use lib '/var/cpanel/plugins/cloudflare_dns/lib';

use CGI;
use JSON;
use Cpanel::Template;
use CloudflareDNS;

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

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

# Initialize API client
my $cf = CloudflareDNS->new(api_token => $api_token);

# HTML header
print_html_header();

# Route actions
if ($action eq 'home') {
    show_home($cgi, $cf, $config);
}
elsif ($action eq 'save_token') {
    save_token($cgi, $config);
}
elsif ($action eq 'list_zones') {
    list_zones($cgi, $cf);
}
elsif ($action eq 'view_zone') {
    view_zone($cgi, $cf);
}
elsif ($action eq 'add_record') {
    add_record($cgi, $cf);
}
elsif ($action eq 'delete_record') {
    delete_record($cgi, $cf);
}
elsif ($action eq 'add_m365') {
    add_m365($cgi, $cf);
}
elsif ($action eq 'import_zone') {
    import_zone($cgi, $cf);
}
else {
    print "
Unknown action: $action
";
}

print_html_footer();

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

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

    print '
Cloudflare DNS Manager
';

    # Check if token is configured
    if (!$api_token) {
        print <<'HTML';

    Setup Required: Please configure your Cloudflare API token.

HTML
        show_token_form($config);
        return;
    }

    # Verify token
    my $verify = $cf->verify_token();
    if (!$verify->{success}) {
        print '
API Token is invalid or expired.
';
        show_token_form($config);
        return;
    }

    print '
✓ Connected to Cloudflare
';

    # Show navigation
    print <<'HTML';

    View All Zones
    Import New Zone
    <button onclick="toggleTokenForm()" class="btn btn-secondary">Update API Token</button>

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

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

    print <<"HTML";
<form method="post" action="?action=save_token" class="form-inline" style="margin: 20px 0; padding: 20px; background: #f5f5f5; border-radius: 5px;">

Cloudflare API Token

Current: $current

        API Token:
        <input type="password" name="api_token" class="form-control" style="width: 400px;" placeholder="Enter Cloudflare API token">

    <button type="submit" class="btn btn-success">Save Token</button>

        Required permissions: Zone:Read, Zone:Edit, DNS:Read, DNS:Edit

</form>
HTML
}

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 ($cgi, $cf) = @_;

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

    my $result = $cf->list_zones();

    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 Records";
        print "	";
        print "
";
    }

    print '

';
}

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

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

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

    # Add record form
    print <<"HTML";

Add Record

    <form method="post" action="?action=add_record" class="form-inline">
        <input type="hidden" name="zone_id" value="$zone_id">
        <input type="hidden" name="domain" value="$domain">
        <select name="type" class="form-control">
            <option>A</option>
            <option>AAAA</option>
            <option>CNAME</option>
            <option>TXT</option>
            <option>MX</option>
        </select>
        <input type="text" name="name" placeholder="Name (e.g., www)" class="form-control" style="width: 150px;">
        <input type="text" name="content" placeholder="Content" class="form-control" style="width: 300px;">
        <input type="checkbox" name="proxied"> Proxied
        <button type="submit" class="btn btn-success">Add</button>
    </form>

HTML

    my $result = $cf->list_records($zone_id);

    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, $cf) = @_;

    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 $result = $cf->add_record($zone_id,
        type => $type,
        name => $name,
        content => $content,
        proxied => $proxied
    );

    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, $cf) = @_;

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

    my $result = $cf->delete_record($zone_id, $record_id);

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

    print "
Return to DNS Records
";
}

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

    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 <<"HTML";
<form method="post" action="?action=add_m365" style="max-width: 500px;">
    <input type="hidden" name="zone_id" value="$zone_id">
    <input type="hidden" name="domain" value="$domain">

        Microsoft 365 Tenant Name
        <input type="text" name="tenant" class="form-control" placeholder="e.g., contoso (from contoso.onmicrosoft.com)">

Enter the prefix of your .onmicrosoft.com domain

Records that will be added:

MX → mail.protection.outlook.com

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

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

DKIM selector1 (CNAME)

DKIM selector2 (CNAME)

Autodiscover (CNAME)

    <button type="submit" class="btn btn-primary">Add M365 Records</button>
</form>
HTML
        return;
    }

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

    my $result = $cf->add_m365_records($domain, $tenant);

    if ($result->{success}) {
        print '
';
        print '

Record	Status

';
        print '
';
        for my $r (@{$result->{results}}) {
            my $class = $r->{success} ? 'success' : 'warning';
            print "
$r->{record}	$r->{message}
";
        }
        print '

';
        print '
Remember to enable DKIM signing in Microsoft 365 Defender portal.
';
    } else {
        print '
Failed to add records.
';
    }

    print "
View DNS Records
";
}

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

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

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

        print <<'HTML';
<form method="post" action="?action=import_zone" style="max-width: 400px;">

        Domain Name
        <input type="text" name="domain" class="form-control" placeholder="example.com">

    <button type="submit" class="btn btn-primary">Import Zone</button>
</form>
HTML
        return;
    }

    print "
Importing: $domain
";

    my $result = $cf->create_zone($domain);

    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>
    <link rel="stylesheet" href="/themes/starter/fonts/roboto/roboto.min.css">
    <link rel="stylesheet" href="/themes/starter/libraries/bootstrap/css/bootstrap.min.css">

HTML
}

sub print_html_footer {
    print <<'HTML';

Cloudflare DNS Manager v1.0 | AZ Computer Guru

HTML
}


Now upload the fixed script:


Bash

Upload fixed CGI script


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

Check perl syntax


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


Syntax OK. Now try refreshing the page in WHM.


Let's see what the limits of this plugin are.  Can we transfer a domain in an automatic fashion, like from WHM to cloudflare that removes the zone from WHM, or leave a marker or indicator to look in the plugin/cloudflare for the zone?  In the zone editor, is there an option to have checkboxes for mass deleting of bad records?  And can we add an edit button for existing records?


Great feature ideas! Let me summarize what you're asking for and then implement them:

## Feature Requests

1. **Zone Transfer from WHM → Cloudflare**
   - Export zone from WHM DNS
   - Import to Cloudflare
   - Either remove from WHM or leave a marker (e.g., TXT record saying "DNS managed by Cloudflare")

2. **Mass Delete with Checkboxes**
   - Select multiple records
   - Delete all selected at once

3. **Edit Button for Existing Records**
   - Modify content, TTL, proxy status without delete/recreate

Let me update the plugin with these features:


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.