ZonesRecordsController
class ZonesRecordsController extends PublicApiController (View source)
/api/v2/zones/{id}/records: lists, creates, updates and deletes single records in a zone.
Constants
| protected MAX_PAGE_SIZE |
|
| CHANGE_REQUEST_REQUIRED |
Refusal for a direct write when the caller's changes to the zone go through review |
| CHANGE_APPROVAL_DISABLED |
Every change request endpoint answers this while approval.enabled is off |
| private RECORD_SORT_FIELDS |
Fields accepted by the |
Properties
| protected PDO | $db | from BaseController | |
| protected array | $requestData | from BaseController | |
| protected Request | $httpRequest | from BaseController | |
| protected MessageService | $messageService | from BaseController | |
| protected ConfigurationInterface | $config | from BaseController | |
| protected LoggerInterface | $logger | from BaseController | |
| protected Request | $request | from AbstractApiController | |
| protected array | $pathParameters | from PublicApiController | |
| protected int | $authenticatedUserId | from PublicApiController | |
| protected ApiKeyScope|null | $apiKeyScope | Permission scope of the API key used for this request. Null when the request authenticated via HTTP Basic auth or no key scope could be resolved, in which case {self::getApiKeyScope()} returns an unrestricted scope. |
from PublicApiController |
Methods
Handle zone records requests
PublicApiController constructor
API requests authenticate per call with an API key or HTTP Basic credentials, so there is no session-bound form token to check.
Get a module config value with legacy fallback.
Renders a template with the given parameters.
API requests carry no session, so the PowerDNS version is fetched on first use and kept for this request only. Callers ask lazily (a zone create with a catalog kind), so ordinary requests never pay for the lookup.
The server info the page chrome reads for zone type labels. Never calls PowerDNS (it renders on login pages too); the dashboard refreshes the cache, and an expired entry still beats flipping labels to Master/Slave.
Capabilities to use when filtering selectable record types, or null to skip version filtering entirely.
Trigger a session-cached refresh of PowerDNS version + capabilities.
Redirects to a specified URL with optional arguments.
Sets a message to be displayed for a specific script.
Checks a condition and displays an error message if the condition is true.
Ends the request after its response was written; see RequestHalted.
Sends a prepared response and ends the request; the API controllers answer every request this way.
Lazily builds the shared service factory so per-request memoized instances (backend provider, permission cache) span all accessors.
The same factory narrowed to the module SDK contract; module controllers use this instead of services() so their core dependencies stay declared.
The request's backend as a capability probe; controllers ask it what the backend can do instead of re-reading dns.backend from the configuration.
Backend identity, for the few places that report it rather than act on a capability (the is_api_backend template variable, the API error banner).
RRsets of the zone that PowerDNS Auth 5.1.x would alter on the next API write to them; always empty on the SQL backend, which writes content as entered.
Resolves the page size for a listing: the request value wins, then the user's stored preference, then interface.rows_per_page with $fallback when unset.
Renders a pagination widget for a paginated listing.
The pagination (pre-rendered string, kept for theme forks) and pagination_items
(for _partials/pagination.html) template variables for a paginated listing.
Check if the logged-in user has the given permission
The logged-in user's zone view level: "all", "own" or "none"
Whether the logged-in user may view any zone; flashes the refusal when not, so a zone lookup skipped on "none" still explains itself on the page.
Check if the logged-in user owns the given zone directly or via group membership
Stops the request unless the current user may open the zone (showError exits).
The zone service the API uses, told what the connected server supports.
The zone creation flow of the add-zone forms, told what the connected server supports.
What the enabled modules offer for a capability (wizard actions, lookup links, export formats), honouring each module's admin restriction.
Change-approval answers for one user and zone; callers pass the user id, usually getCurrentUserId(). One instance per request.
For a write that lands in a zone other than the one the request was checked against, such as the PTR of an A record: refused where the user must file a change request.
The users a new zone may be given to: everyone when the create path lets the caller assign other owners and they may see other users, otherwise only the current user.
The owner an add-zone form shows again after a failed submit: the posted id when the picker can offer it, '' for an explicit "no user owner", otherwise the current user.
Flash the outcome of a zone write to a page: the given text on success, the result's reason on failure.
The request's session, shared by every collaborator this controller builds.
Sets the current page identifier used for navigation highlighting.
Checks if the user has a specific permission and displays an error message if not.
Reads a positive integer request parameter (an id) or ends the request with an error page.
Answers with the standard 404 page, for routes whose feature is switched off.
Displays an error message.
Gets a safe value from the request data.
Sets validation constraints for the request data.
Displays the first validation error.
Adds a system-wide message that will be displayed on any page
The first query parameter sent as an array (?type[]=A), or null when every
value is a scalar. InputBag::get() throws on such a value, so subclasses refuse
the request before any handler reads the query. The key is scrubbed to valid
UTF-8 because it is echoed in a JSON error.
Get the JSON request body validated as a non-empty array.
Override to record every public API response in the audit log
Wrapped API response: {success, data, message?} plus any additional top-level fields.
Wrapped API error: {success: false, data, message} plus any additional top-level fields.
Return a 405 Method Not Allowed error with the RFC 9110 Allow header listing the methods the endpoint supports.
Return an error response
Extract a string value from input array.
Extract an integer value from input array.
Extract a boolean value from input array.
Extract an integer value that also accepts JSON booleans and "true"/"false" strings.
Extract a template ID from input. Accepts string IDs, "none", and JSON integers.
Authenticate the API request using API key or HTTP Basic auth
Answer 400 when a query parameter arrives as an array; see arrayQueryParameter().
Check if the current API endpoint requires authentication Can be overridden in child classes for public endpoints
Pick the right "cannot edit this zone's records" message: read-only zones (Secondary, Consumer) replicate from a primary and are rejected for a different reason than a missing edit permission. Keeps the public error contract accurate for both cases.
The 403 a direct write gets when the caller's changes to the zone are routed through change requests, or null when the write may proceed.
Get the permission scope of the API key for this request. Requests without a key scope (Basic auth, or an unresolvable key) are treated as unrestricted.
Reject the request with 403 when the API key's read-only/operation scope does not permit the HTTP method. This is a request-global gate.
Whether an API key restricted to specific zones may call this endpoint.
Operations the current request performs, all of which the API key must permit. Defaults to the single operation implied by the HTTP method.
Send a 403 for an operation the API key may not perform, and stop.
Guard a zone-scoped endpoint against the API key's zone restriction.
The key's zone scope as a check for a write that lands outside the requested zone, such as the PTR of an A record; null for a key with no zone restriction.
The opt-in paging parameters of a list request, see ListPaging.
Whether the request sets any of these query parameters to a non-empty value; an empty filter keeps every row, the same as an absent one.
The non-empty query parameters among these, for a repository pre-filter.
Filter, sort and page a list that is already loaded. Each filter maps a query parameter to the values of a row it searches as a case-insensitive substring.
Apply V2 record-content formatting.
The API wording for a refused record write: a duplicate and a backend fault keep the contract strings, a backend verdict appends its reason to them, every other refusal carries the manager's reason.
Whether a record fetched by id belongs to the zone addressed in the path.
Record names are stored as punycode and always carry the zone suffix.
Strip quotes from single-string TXT records for V2 API responses
Handle exception and return JSON error response
Details
void
run()
Handle zone records requests
__construct(array $request, array $pathParameters = [])
PublicApiController constructor
static void
bindContext(BootContext $context)
Hands the booted context to every controller the router builds.
Controllers are constructed by class name with request data only, so the router cannot pass it through their constructors.
static protected BootContext
requestContext()
No description
protected bool
requiresCsrfValidation()
API requests authenticate per call with an API key or HTTP Basic credentials, so there is no session-bound form token to check.
bool
isPost()
Checks if the current request is a POST request.
ConfigurationInterface
getConfig()
Gets the application configuration.
protected mixed
getModuleConfig(string $module, string $key, mixed $default = null)
Get a module config value with legacy fallback.
Checks modules.
void
render(string $template, array $params)
Renders a template with the given parameters.
protected PdnsCapabilities
getPdnsCapabilities()
API requests carry no session, so the PowerDNS version is fetched on first use and kept for this request only. Callers ask lazily (a zone create with a catalog kind), so ordinary requests never pay for the lookup.
protected array|null
getCachedPdnsServerInfo(bool $allowExpired = true)
The server info the page chrome reads for zone type labels. Never calls PowerDNS (it renders on login pages too); the dashboard refreshes the cache, and an expired entry still beats flipping labels to Master/Slave.
The dashboard passes false so its version badge shows only a fresh value.
protected PdnsCapabilities|null
getRecordTypeCapabilities()
Capabilities to use when filtering selectable record types, or null to skip version filtering entirely.
Only API backends can report their PowerDNS version, so only there can we safely hide types a newer-than-server feature would need. On SQL backends the version is never known, and strict-unknown filtering would wrongly and permanently drop valid types (HTTPS, SVCB, ZONEMD, ...) from every form. Returning null there leaves the configured/default type list intact.
protected void
refreshPdnsCapabilities()
Trigger a session-cached refresh of PowerDNS version + capabilities.
Runs in both API and SQL backend modes: the version display is useful in
either mode whenever pdns_api is configured, even though capability
gates only matter for API mode. getPdnsCapabilities() calls this itself
once the cached entry expires; call it directly only where a fresh
version matters more than the cached one - e.g. the dashboard.
void
validateCsrfToken()
Validates the CSRF token from the request.
void
redirect(string $url, array $args = [])
Redirects to a specified URL with optional arguments.
Automatically prepends base_url_prefix for subfolder deployments.
void
setMessage(string $script, string $type, string $content)
Sets a message to be displayed for a specific script.
array|null
getMessages(string $script)
Gets messages for a specific script.
void
checkCondition(bool $condition, string $errorMessage)
Checks a condition and displays an error message if the condition is true.
protected never
halt(string $kind, string $target)
Ends the request after its response was written; see RequestHalted.
protected never
sendAndHalt(Response $response)
Sends a prepared response and ends the request; the API controllers answer every request this way.
protected ControllerServiceFactory
services()
Lazily builds the shared service factory so per-request memoized instances (backend provider, permission cache) span all accessors.
protected ModuleServices
moduleServices()
The same factory narrowed to the module SDK contract; module controllers use this instead of services() so their core dependencies stay declared.
protected BackendCapabilitiesInterface
backendCapabilities()
The request's backend as a capability probe; controllers ask it what the backend can do instead of re-reading dns.backend from the configuration.
protected bool
isApiBackend()
Backend identity, for the few places that report it rather than act on a capability (the is_api_backend template variable, the API error banner).
protected array
apiEscapeRiskRrsets(int $zoneId, string $zoneName)
RRsets of the zone that PowerDNS Auth 5.1.x would alter on the next API write to them; always empty on the SQL backend, which writes content as entered.
protected int
resolveRowsPerPage(int $fallback = PaginationService::DEFAULT_ROWS_PER_PAGE)
Resolves the page size for a listing: the request value wins, then the user's stored preference, then interface.rows_per_page with $fallback when unset.
protected string
presentPagination(int $totalItems, int $itemsPerPage, string $path, array $queryParams = [])
Renders a pagination widget for a paginated listing.
$path is the route with the {PageNumber} placeholder already in place
(e.g. '/zones/forward?start={PageNumber}'). $queryParams are extra
key => value pairs appended urlencoded, in order, skipping absent and blank values.
protected array
paginationVariables(int $totalItems, int $itemsPerPage, string $path, array $queryParams = [])
The pagination (pre-rendered string, kept for theme forks) and pagination_items
(for _partials/pagination.html) template variables for a paginated listing.
protected ZoneSortingService
createZoneSortingService()
No description
protected bool
hasPermission(string $permission)
Check if the logged-in user has the given permission
protected string
getViewPermissionLevel()
The logged-in user's zone view level: "all", "own" or "none"
protected bool
canViewZones()
Whether the logged-in user may view any zone; flashes the refusal when not, so a zone lookup skipped on "none" still explains itself on the page.
protected bool
isZoneOwner(int $zoneId)
Check if the logged-in user owns the given zone directly or via group membership
protected void
requireZoneView(int $zoneId)
Stops the request unless the current user may open the zone (showError exits).
protected ZoneManagementService
createZoneManagementService()
The zone service the API uses, told what the connected server supports.
protected ZoneCreateService
createZoneCreateService()
The zone creation flow of the add-zone forms, told what the connected server supports.
protected ModuleRegistry
moduleRegistry()
The request's loaded module registry.
protected array
moduleCapabilityData(string $capability, array $context = [])
What the enabled modules offer for a capability (wizard actions, lookup links, export formats), honouring each module's admin restriction.
protected bool
moduleProvides(string $capability)
No description
protected ChangeApprovalContext
changeApproval()
Change-approval answers for one user and zone; callers pass the user id, usually getCurrentUserId(). One instance per request.
protected string
changeApprovalModeForZone(int $zoneId)
No description
protected Closure
directWriteZoneGate()
For a write that lands in a zone other than the one the request was checked against, such as the PTR of an A record: refused where the user must file a change request.
protected int|null
getCurrentUserId()
Get current user ID
protected array
assignableOwners(array $users)
The users a new zone may be given to: everyone when the create path lets the caller assign other owners and they may see other users, otherwise only the current user.
protected int|string
preservedOwnerChoice(array $assignableOwners, mixed $ownerInput)
The owner an add-zone form shows again after a failed submit: the posted id when the picker can offer it, '' for an explicit "no user owner", otherwise the current user.
protected void
reportZoneWrite(string $script, ZoneWriteResult $result, string $successMessage)
Flash the outcome of a zone write to a page: the given text on success, the result's reason on failure.
protected SessionInterface
session()
The request's session, shared by every collaborator this controller builds.
protected UserContextService
getUserContextService()
Get the user context service
protected void
setCurrentPage(string $page)
Sets the current page identifier used for navigation highlighting.
protected void
setPageTitle(string $title)
Sets the page title displayed in the header.
void
checkPermission(string $permission, string $errorMessage)
Checks if the user has a specific permission and displays an error message if not.
protected int
requireNumericParam(string $name, string|null $error = null)
Reads a positive integer request parameter (an id) or ends the request with an error page.
protected void
renderNotFound()
Answers with the standard 404 page, for routes whose feature is switched off.
void
showError(string $error, string|null $recordName = null)
Displays an error message.
array
getRequest()
Gets the request data.
string
getSafeRequestValue(string $key)
Gets a safe value from the request data.
void
setValidationConstraints(array $constraints)
Sets validation constraints for the request data.
bool
doValidateRequest(array|null $data = null)
Validates the request data.
void
showFirstValidationError(array|null $data = null)
Displays the first validation error.
void
addSystemMessage(string $type, string $content)
Adds a system-wide message that will be displayed on any page
protected string|null
arrayQueryParameter()
The first query parameter sent as an array (?type[]=A), or null when every
value is a scalar. InputBag::get() throws on such a value, so subclasses refuse
the request before any handler reads the query. The key is scrubbed to valid
UTF-8 because it is echoed in a JSON error.
protected bool
isJsonRequest()
Checks if the current request is a JSON request
protected array|null
getJsonInput()
Get JSON input from request body
protected array|null
getValidatedJsonBody()
Get the JSON request body validated as a non-empty array.
Same sources as {\Poweradmin\Application\Controller\Api\self::getJsonInput()} (JSON body with a form-data
fallback), but scalar bodies like "abc" or 42 - which decode to
non-array values - are rejected as well, so callers can safely use array
access on the result. Returns null for anything but a non-empty array.
protected JsonResponse
returnJsonResponse(mixed $data, int $status = 200, array $headers = [])
Override to record every public API response in the audit log
protected JsonResponse
returnApiResponse(mixed $data, bool $success = true, string|null $message = null, int $status = 200, array $additionalFields = [], array $headers = [])
Wrapped API response: {success, data, message?} plus any additional top-level fields.
protected JsonResponse
returnApiError(string $message, int $status = 400, mixed $data = null, array $additionalFields = [], array $headers = [])
Wrapped API error: {success: false, data, message} plus any additional top-level fields.
protected JsonResponse
methodNotAllowed(array $allowedMethods)
Return a 405 Method Not Allowed error with the RFC 9110 Allow header listing the methods the endpoint supports.
protected JsonResponse
returnErrorResponse(string $message, int $status = 400, string|null $code = null)
Return an error response
protected string|null
inputString(array $input, string $key, string|null $default = null)
Extract a string value from input array.
Returns default only when key is absent; returns null for present non-string values so callers can distinguish missing from malformed.
protected int|null
inputInt(array $input, string $key, int|null $default = null)
Extract an integer value from input array.
Accepts integers and integer-shaped strings. Returns default when key is absent; returns null for present non-integer values.
protected bool|null
inputBool(array $input, string $key, bool|null $default = null)
Extract a boolean value from input array.
Accepts native booleans, int 1/0, and strings "1"/"0"/"true"/"false" for form-encoded compatibility. Returns default when key is absent; returns null for present invalid values.
protected int|null
inputIntFromBool(array $input, string $key, int|null $default = 0)
Extract an integer value that also accepts JSON booleans and "true"/"false" strings.
Use for fields like 'disabled' where the API documents boolean but stores as int. Returns default when key is absent; returns null for present invalid values.
protected string
inputTemplate(array $input)
Extract a template ID from input. Accepts string IDs, "none", and JSON integers.
Rejects booleans, arrays, and non-numeric strings.
protected void
authenticateApiRequest()
Authenticate the API request using API key or HTTP Basic auth
protected void
refuseArrayQueryParameters()
Answer 400 when a query parameter arrives as an array; see arrayQueryParameter().
protected string|null
getApiKeyFromRequest()
Get API key from request headers
protected bool
requiresAuthentication()
Check if the current API endpoint requires authentication Can be overridden in child classes for public endpoints
protected string
zoneEditDeniedMessage(string|null $zoneType)
Pick the right "cannot edit this zone's records" message: read-only zones (Secondary, Consumer) replicate from a primary and are rejected for a different reason than a missing edit permission. Keeps the public error contract accurate for both cases.
protected JsonResponse|null
refuseWhenChangeRequestRequired(ApiPermissionService $permissions, int $userId, int $zoneId)
The 403 a direct write gets when the caller's changes to the zone are routed through change requests, or null when the write may proceed.
protected int
getAuthenticatedUserId()
Get the authenticated user ID (stateless)
protected string
getAuthenticatedUsername()
No description
protected ApiKeyScope
getApiKeyScope()
Get the permission scope of the API key for this request. Requests without a key scope (Basic auth, or an unresolvable key) are treated as unrestricted.
protected void
enforceApiKeyMethodScope()
Reject the request with 403 when the API key's read-only/operation scope does not permit the HTTP method. This is a request-global gate.
protected bool
acceptsZoneRestrictedKey()
Whether an API key restricted to specific zones may call this endpoint.
Endpoints not tied to any zone override this to refuse such keys.
protected array
requiredApiKeyOperations()
Operations the current request performs, all of which the API key must permit. Defaults to the single operation implied by the HTTP method.
Override for endpoints where the method is not a 1:1 operation mapping: return the exact set (e.g. [create, update] for an upsert), or [] to skip the central check and enforce the operation scope inside the handler.
protected never
sendApiKeyOperationForbidden()
Send a 403 for an operation the API key may not perform, and stop.
protected JsonResponse|null
enforceApiKeyZoneScope(int $zoneId)
Guard a zone-scoped endpoint against the API key's zone restriction.
Returns a 403 response when the zone is out of scope, or null when allowed.
Callers return the response directly: if (($r = $this->enforceApiKeyZoneScope($id)) !== null) { return $r; }
protected Closure|null
apiKeyZoneGate()
The key's zone scope as a check for a write that lands outside the requested zone, such as the PTR of an A record; null for a key with no zone restriction.
protected array
pagingParameters()
The opt-in paging parameters of a list request, see ListPaging.
protected bool
anyQueryParameter(array $parameters)
Whether the request sets any of these query parameters to a non-empty value; an empty filter keeps every row, the same as an absent one.
protected array
queryNeedles(array $parameters)
The non-empty query parameters among these, for a repository pre-filter.
protected array
filterSortAndPage(array $rows, ListSort $sort, array $filters)
Filter, sort and page a list that is already loaded. Each filter maps a query parameter to the values of a row it searches as a case-insensitive substring.
protected string
formatV2RecordContent(string $type, string $content)
Apply V2 record-content formatting.
V2 always quotes single-string TXT records, even when dns.txt_auto_quote is off, so records round-trip: create quotes, read strips, update must re-quote. Used by create and update paths alike so stored content stays consistent.
protected string
recordWriteErrorMessage(RecordWriteResult $result, string $backendFailureText)
The API wording for a refused record write: a duplicate and a backend fault keep the contract strings, a backend verdict appends its reason to them, every other refusal carries the manager's reason.
protected bool
recordBelongsToZone(array $record, int $zoneId, string|null $zoneName)
Whether a record fetched by id belongs to the zone addressed in the path.
A zone moved from SQL to API backend mode can be addressed by its zones row id while its records carry the canonical id, so the zone's own canonical id (resolved by name, the way the record lookup resolves it) also matches.
protected string
normalizeV2RecordName(string $name, string $zoneName)
Record names are stored as punycode and always carry the zone suffix.
protected string
stripTxtQuotes(string $content, string $type)
Strip quotes from single-string TXT records for V2 API responses
V2 responses present single-string TXT content unquoted regardless of stored form (zone records are force-quoted on write, template records only when dns.txt_auto_quote is on). Multi-string TXT records (e.g., "part1" "part2") are preserved as-is since they represent long values split across multiple strings.
protected JsonResponse
handleException(Throwable $e, string $context, string $userMessage = 'An error occurred', int $statusCode = 500)
Handle exception and return JSON error response
Catches all throwables (Exception, TypeError, Error, etc.) and returns a proper JSON error response instead of letting PHP display HTML errors. Logs detailed error information for debugging.