Files
novacpx/panel/lib/CloudflareManager.php
T
myron 6fdccc6dbd feat: items #9-13 — password change, webmail SSO, DKIM live, file manager security, cache busting
#9  auth.php: add self-service change-password action (current+new+confirm)
    accounts.php: fix admin change-password — accept account_id, fetch username
    for chpasswd (was using int ID), add Auth::require('admin') guard
    user.js: add Change Password page + navItem + submitChangePassword()

#10 EmailManager: store AES-256-CBC enc_password alongside SHA512-CRYPT hash
    webmail.php: rewrite login-url to use webmail_sso_tokens table
    novacpx-sso.php: Roundcube SSO bridge (validate token, decrypt, autosubmit)
    Migration 005: add enc_password column + webmail_sso_tokens table

#11 opendkim: installed, configured (/etc/opendkim.conf, signing.table,
    key.table, trusted.hosts), socket at /var/spool/postfix/opendkim/,
    Postfix milter wired, service enabled+running, key generation verified

#12 files.php: fix safe_path() for non-existent paths (write/mkdir),
    add safe_path_new() helper using parent-dir realpath check,
    fix delete guard (block deleting account root dirs),
    fix rename destination, clamp chmod to 0777

#13 nova.js: api() handles network errors, 429 rate-limit with retry-after,
    non-JSON responses (PHP fatal pages) — graceful error instead of throw
    admin/user/reseller index.php: filemtime-based cache-busting on all assets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 01:19:33 +00:00

151 lines
7.7 KiB
PHP

<?php
class CloudflareManager {
private const API = 'https://api.cloudflare.com/client/v4/';
private PDO $db;
public function __construct() {
$this->db = Database::getInstance()->getPDO();
}
// ── Credential management ─────────────────────────────────────────────────
public function saveCredentials(int $accountId, string $apiKey, string $email): bool {
$stmt = $this->db->prepare("UPDATE accounts SET cf_api_key=?, cf_api_email=? WHERE id=?");
return $stmt->execute([$apiKey, $email, $accountId]);
}
public function testCredentials(string $apiKey, string $email): bool {
$r = $this->req('GET', 'user/tokens/verify', [], $apiKey, $email);
return $r['success'] ?? false;
}
public function getCredentials(int $accountId): ?array {
$stmt = $this->db->prepare("SELECT cf_api_key, cf_api_email, cf_zone_id FROM accounts WHERE id=?");
$stmt->execute([$accountId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return ($row && $row['cf_api_key']) ? $row : null;
}
// ── Zones ─────────────────────────────────────────────────────────────────
public function listZones(string $apiKey, string $email): array {
$r = $this->req('GET', 'zones?per_page=200&status=active', [], $apiKey, $email);
return $r['result'] ?? [];
}
public function getZoneId(string $domain, string $apiKey, string $email): ?string {
$r = $this->req('GET', 'zones?name=' . urlencode($domain), [], $apiKey, $email);
return $r['result'][0]['id'] ?? null;
}
// ── DNS records ───────────────────────────────────────────────────────────
public function listRecords(string $zoneId, string $apiKey, string $email): array {
$r = $this->req('GET', "zones/{$zoneId}/dns_records?per_page=200", [], $apiKey, $email);
return $r['result'] ?? [];
}
public function createRecord(string $zoneId, array $record, string $apiKey, string $email): array {
return $this->req('POST', "zones/{$zoneId}/dns_records", $record, $apiKey, $email);
}
public function updateRecord(string $zoneId, string $recordId, array $record, string $apiKey, string $email): array {
return $this->req('PUT', "zones/{$zoneId}/dns_records/{$recordId}", $record, $apiKey, $email);
}
public function deleteRecord(string $zoneId, string $recordId, string $apiKey, string $email): bool {
$r = $this->req('DELETE', "zones/{$zoneId}/dns_records/{$recordId}", [], $apiKey, $email);
return $r['success'] ?? false;
}
public function toggleProxy(string $zoneId, string $recordId, bool $proxied, string $apiKey, string $email): array {
// Fetch existing record first
$r = $this->req('GET', "zones/{$zoneId}/dns_records/{$recordId}", [], $apiKey, $email);
$rec = $r['result'] ?? [];
$rec['proxied'] = $proxied;
unset($rec['id'], $rec['zone_id'], $rec['zone_name'], $rec['created_on'], $rec['modified_on'], $rec['meta']);
return $this->req('PUT', "zones/{$zoneId}/dns_records/{$recordId}", $rec, $apiKey, $email);
}
// ── Sync: push local DNS records to Cloudflare ────────────────────────────
public function syncToCloudflare(string $domain, string $zoneId, string $apiKey, string $email): array {
$localRecords = $this->getLocalRecords($domain);
$cfRecords = $this->listRecords($zoneId, $apiKey, $email);
$cfIndex = [];
foreach ($cfRecords as $r) $cfIndex[$r['type'] . '_' . $r['name']] = $r;
$created = 0; $updated = 0;
foreach ($localRecords as $local) {
$key = $local['type'] . '_' . $local['name'] . '.' . $domain . '.';
if (isset($cfIndex[$key])) {
$this->updateRecord($zoneId, $cfIndex[$key]['id'], [
'type' => $local['type'],
'name' => $local['name'],
'content' => $local['content'],
'ttl' => (int)($local['ttl'] ?? 1),
'proxied' => in_array($local['type'], ['A','AAAA','CNAME']),
], $apiKey, $email);
$updated++;
} else {
$this->createRecord($zoneId, [
'type' => $local['type'],
'name' => $local['name'],
'content' => $local['content'],
'ttl' => (int)($local['ttl'] ?? 1),
'proxied' => in_array($local['type'], ['A','AAAA','CNAME']),
], $apiKey, $email);
$created++;
}
}
return ['created' => $created, 'updated' => $updated];
}
// ── Sync: pull Cloudflare records into local DB ───────────────────────────
public function syncFromCloudflare(string $domain, string $zoneId, string $apiKey, string $email): int {
$cfRecords = $this->listRecords($zoneId, $apiKey, $email);
$count = 0;
foreach ($cfRecords as $rec) {
$name = rtrim(str_replace('.' . $domain, '', $rec['name']), '.');
$this->db->prepare("INSERT INTO dns_records (domain, name, type, content, ttl, priority)
VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE content=VALUES(content), ttl=VALUES(ttl)")
->execute([$domain, $name ?: '@', $rec['type'], $rec['content'], $rec['ttl'] ?? 300, $rec['priority'] ?? 0]);
$count++;
}
// Store zone ID on domain
$this->db->prepare("UPDATE dns_zones SET cf_zone_id=? WHERE domain=?")->execute([$zoneId, $domain]);
return $count;
}
// ── Purge cache ───────────────────────────────────────────────────────────
public function purgeCache(string $zoneId, string $apiKey, string $email): bool {
$r = $this->req('POST', "zones/{$zoneId}/purge_cache", ['purge_everything' => true], $apiKey, $email);
return $r['success'] ?? false;
}
// ── HTTP helper ───────────────────────────────────────────────────────────
private function req(string $method, string $path, array $body, string $apiKey, string $email): array {
$ch = curl_init(self::API . ltrim($path, '/'));
$headers = [
"X-Auth-Email: {$email}",
"X-Auth-Key: {$apiKey}",
"Content-Type: application/json",
];
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_SSL_VERIFYPEER => true,
]);
if ($body && in_array($method, ['POST','PUT','PATCH'])) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true) ?? ['success' => false, 'errors' => ['curl failed']];
}
private function getLocalRecords(string $domain): array {
$stmt = $this->db->prepare("SELECT * FROM dns_records WHERE domain=?");
$stmt->execute([$domain]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}