Native Development with Direct Database Access
A developer guide for building server-side tools for Watch My Domains SED using the application's own internal classes and direct database access.
This guide is intended for development teams within an organization that manage their own WMD SED installation and need to build internal tools with native, direct access to the application database.
It assumes the team has full administrative control of the server, understands the security implications of direct database access, and is building tools for internal operational use — not for end users or external integrations. If you are building tools for external use, or want to work within WMD's user and permission model, use the REST API instead.
Security Warning
Direct-access tools bypass all application-level permission controls and operate with unrestricted access to the WMD database.
This is equivalent to full administrative access to your domain data.
This is fundamentally different from tools built against the REST API, which operate within WMD's session, user, and column-level permission model. A direct-access tool loaded via the bootstrap has the same database privileges as the application itself — it can read, write, and delete any data regardless of which WMD user invokes it.
Before building or deploying a direct-access tool, understand the following:
- Server access implies database access. Anyone with the ability to place files on the server already has access to the database credentials stored in WMD's configuration files. Direct-access tools do not introduce a new attack surface — but they do make that access easier to exercise.
- In browser mode, WMD's login session is the only access control. If a user is logged in to WMD, they can invoke any direct-access tool placed in
user-tools/regardless of their WMD role or permissions. There is no automatic role check. - In CLI mode, there is no access control at all. Any OS user with shell access and read permission on the tool file can run it.
- Accidental exposure is common. A tool placed in
user-tools/may be reachable via URL without the developer realising it. If browser access is not explicitly disabled, the tool is reachable over HTTP. - These tools should only be built and deployed by administrators who have full server access and understand the implications. They must never be placed in a location accessible to untrusted users.
- WMD user access rights are completely ignored. WMD's permission model — role assignments, category restrictions, column-level access controls — exists entirely within the application layer. Direct database access operates below that layer. A read-only WMD user with access to a browser-accessible tool has exactly the same database access as a full administrator.
- Write operations are permanent.
updateTable()and delete operations have no undo. Errors in tool logic can corrupt or destroy data across the entire domain portfolio. Bulk operations can affect thousands of records in a single run with no confirmation step. - If exposed without session validation, the tool becomes a public database endpoint. Anyone who can access the URL — including unauthenticated users, bots, or scanners — can execute read, write, and delete operations across your entire domain database.
- Treat these tools with the same care as direct database access. Because that is exactly what they are.
Table of Contents
- Security Warning
- Overview
- Two Approaches to Extending WMD SED
- Where to Place Your Tool
- Bootstrapping: The Loader
- Dual Mode: CLI and Browser
- Authentication and Database Access
- Querying the Domain Table
- Handling CLI Flags and Browser Parameters
- Output Helpers
- Error Handling
- Debug Mode
- Full Example: Domain Expiry Notifier
- Checklist
Overview
WMD SED exposes two ways to interact with it programmatically:
- The REST API (
api.php) — session-based, JSON responses, suitable for browser-based tools and external integrations. Documented atlearn.domainpunch.com/wmdsed/api/. - Direct database access — server-side PHP scripts that load WMD's own internal classes and talk to the database directly. Suitable for CLI automation, cron jobs, and tasks that need to run outside a browser session.
This guide covers the second approach: direct database access via internal classes.
Two Approaches to Extending WMD SED
API-Based Tools
Use the REST API when:
- Your tool is browser-based (HTML + JavaScript)
- You need to integrate from an external server or domain
- You want to work within WMD's session and permission model
- You don't have shell access to the server
Direct Access Tools
Use direct database access when:
- Your tool runs as a cron job or scheduled task
- You need to run from the CLI without a browser session
- You want to enrich or update domain data in bulk
- You are calling third-party APIs and writing results back to the domain table
- You need access to internal WMD utility classes for output, logging, or configuration
Where to Place Your Tool
Place all custom tools in the user-tools/ folder inside your WMD SED installation root:
wmdsed/
user-tools/
my-tool.php
domain-expiry-notifier.php
another-tool.php
Do not place tools in wmdsed/tools/ — that folder is reserved for tools distributed with the application and may be overwritten during updates.
The user-tools/ folder keeps your custom code clearly separated and safe across upgrades.
Bootstrapping: The Loader
Every direct-access tool must start by loading WMD's bootstrap file. From the user-tools/ folder the path is always:
require '../lib/php/loader.php';
The loader initialises the WMD framework, sets up autoloading for all internal classes, starts the session (for browser mode), and connects to the database configuration. Nothing else is needed to get access to the full WMD class library.
This single line replaces everything that would otherwise need manual setup: database credentials, class autoloading, session handling, and environment detection.
Dual Mode: CLI and Browser
WMD tools can run in both CLI and browser contexts from the same file. The key utility method is:
UTIL::is_cli()
Use this to branch behaviour where the two modes differ.
CLI only by default. Avoid browser mode.
Direct-access tools are designed for CLI execution — cron jobs, scheduled tasks, and administrative scripts run by someone with full server access. Browser mode is technically possible but carries serious risk: any authenticated WMD user who can reach the URL can trigger unrestricted database operations, regardless of their role or permissions within WMD.
Consequences of uncontrolled browser access include:
- Any authenticated WMD user can read every domain record in the portfolio — including columns and categories their WMD account has no permission to see
- Any authenticated WMD user can trigger write or delete operations across the entire domain table — WMD's column-level and category-level access controls are completely bypassed
- Data can be corrupted or permanently deleted with no undo — there is no audit trail specific to direct-access tool operations
- Bulk operations can affect thousands of records in a single run with no confirmation step
User access rights are completely ignored. WMD's permission model — role assignments, category restrictions, column-level access — exists entirely within the application layer. Direct database access operates below that layer entirely. A read-only WMD user hitting this tool in a browser has the same database access as a full administrator.
The recommended practice is to exit immediately if not running from CLI:
if (!UTIL::is_cli()) {
UTIL::print("This tool can only be run from the command line.");
exit;
}
Put this check at the very top of your tool, before any other logic. This one guard eliminates the entire browser attack surface. If you find yourself wanting browser access to a direct-access tool, consider whether the task should instead be built against the REST API, which operates within WMD's proper permission model.
Only bypass this guard if you have a specific, well-understood operational reason to allow browser execution — and you fully accept the security implications documented in the Security Warning above.
CLI Mode
When run from the command line, parse arguments before doing anything else:
if (UTIL::is_cli()) {
UTIL::parse_request_data_in_cli();
}
$rd = UTIL::get_unsafe_request_data_array();
parse_request_data_in_cli() maps --key=value style arguments into the request data array, making them available via $rd in the same way $_GET / $_POST would be in browser mode.
Important: bare flags without a value (e.g. --debug, --dry-run) are not populated into $rd by parse_request_data_in_cli(). Detect these by scanning $argv directly:
if (UTIL::is_cli()) {
foreach (($GLOBALS['argv'] ?? []) as $arg) {
$argLower = strtolower(ltrim($arg, '-'));
if ($argLower === 'debug' || strpos($argLower, 'debug=') === 0) {
$debug = true;
}
}
}
Browser Mode
In browser mode the loader starts the WMD session, but the tool must still explicitly validate that session before doing anything that touches the database. Browser access should only be allowed when you have a specific, well-understood operational reason to permit it.
Query string parameters are available via $rd after calling UTIL::get_unsafe_request_data_array().
Output
UTIL::print() handles output in both modes natively. In CLI it writes to stdout. In browser it wraps content appropriately for HTML display. Use it for all output rather than echo directly:
UTIL::print("Processing complete.");
Authentication and Database Access
All direct-access tools obtain the database handle through the Auth class, which reads WMD's own database configuration. You never need to hardcode credentials:
$auth = new \CodePunch\Config\Auth(\CodePunch\DB\Database::READONLY_TABLES);
if (!$auth) {
throw new Exception("Failed to initialise Auth — check database configuration.");
}
Session Validation in Browser Mode
In CLI mode there is no session to check. In browser mode however, the tool must explicitly verify that the user is logged in before doing anything else. The loader does not enforce this automatically — without the check, an unauthenticated request will reach the database.
Browser access to direct-access tools is strongly discouraged. Any user with a valid WMD login session can invoke the tool regardless of their role or permissions within WMD. If you do not have a specific, well-understood reason to expose the tool via a browser, disable browser access entirely by exiting immediately when not in CLI mode:
if (!UTIL::is_cli()) {
UTIL::print("This tool can only be run from the command line.");
exit;
}
If you allow browser access and forget to call validateSession(false, false), the tool will execute with full database access for any HTTP request — authenticated or not. There is no fallback protection. An unauthenticated visitor, a search engine crawler, or anyone who discovers the URL will have unrestricted read and write access to your entire domain database. This is not a theoretical risk — it will happen silently, with no error and no log entry in WMD.
If you choose to allow browser access: treating session validation as optional or something to add later is not acceptable. It must be the first thing that runs after Auth is instantiated, every single time, with no exceptions.
If you choose to allow browser access and fully understand what you are doing, validate the session before obtaining the database handle. Call validateSession(false, false) — the two false arguments are critical:
- The first argument is the page to show after validation. Passing
falsesuppresses any page rendering by Auth. - The second argument controls the swipe-in behaviour. Passing
falsedisables it. - Do not call
validateSession()without arguments. Auth will attempt to render the login page without full UI initialisation, producing a broken page.
On failure, redirect to the login page manually rather than letting Auth handle it:
if (!UTIL::is_cli()) {
// RECOMMENDED: disable browser access entirely instead:
// UTIL::print("This tool can only be run from the command line.");
// exit;
$sessionStatus = $auth->validateSession(false, false);
if ($sessionStatus !== \CodePunch\Config\Auth::VALID) {
header("Location: ../login.php");
exit;
}
}
validateSession(false, false) returns one of the following constants defined on the Auth class:
| Constant | Value | Meaning |
|---|---|---|
Auth::VALID | 0 | Session is active and authenticated — proceed |
Auth::TIMEDOUT | 1 | Session has expired |
Auth::INVALID | -1 | Session is not authenticated |
Auth::UNKNOWN | -2 | Session state cannot be determined |
Auth::ERROR | -3 | An error occurred during validation |
Only Auth::VALID should be allowed to proceed. Any other status redirects to login.
After session validation, obtain the database handle as normal:
$db = $auth->getDatabase();
if (!$db) {
throw new Exception("Failed to obtain a database handle — check connection settings.");
}
Getting the Domain Table Name
Never hardcode the domain table name. Always retrieve it via:
$table = $db->getDomainTableName();
This returns the correct table name for your installation (typically wmdsed_domains but may differ).
Querying the Domain Table
The primary method for reading domain data is getFromTable():
$rows = $db->getFromTable(
$columns, // comma-separated column names string
$table, // from getDomainTableName()
$where, // WHERE clause with ? placeholders
$params, // array of bound parameter values
$orderBy, // sort column name
$direction, // 'asc' or 'desc'
$limit // maximum rows to return
);
This method uses PDO prepared statements internally. Always use ? placeholders in your WHERE clause and pass values in the $params array — never interpolate user input directly into SQL.
The return value is an array of associative arrays, each keyed by column name exactly as specified in $columns. Returns false on query failure — always check for this:
if ($rows === false) {
throw new Exception("Database query failed.");
}
An empty result set returns an empty array [], not false.
Common Domain Table Columns
| Column | Description |
|---|---|
sid | Internal domain ID (primary key) |
domain | Domain name |
registry_expiry | Expiry date from registry WHOIS |
registrar_expiry | Expiry date from registrar WHOIS |
status | EPP status codes (comma-separated string) |
availability | Domain availability status e.g. Not Available, available, possibly available |
primary_whois_checked_at | Timestamp of last registry WHOIS lookup |
secondary_whois_checked_at | Timestamp of last registrar WHOIS lookup |
Custom columns defined in your WMD installation are also queryable by their column name. Examples may include client_email, customer_name, or any other fields added via the custom columns feature.
Writing Data Back
To update domain records use updateTable():
$result = $db->updateTable(
$table, // table name
$dataArray, // associative array of column => value to update
"sid=?", // WHERE clause
[$sid] // bound parameter values
);
if ($result === false) {
throw new Exception("Update failed for sid {$sid}.");
}
Handling CLI Flags and Browser Parameters
Design tools to accept the same parameters in both modes:
| Mode | Key=value parameter | Bare flag |
|---|---|---|
| CLI | --days=60 → available in $rd['days'] |
--debug → must scan $argv |
| Browser | ?days=60 → available in $rd['days'] |
?debug=1 → available in $rd['debug'] |
A robust pattern that handles both:
$days = 30; // default
$debug = false;
// key=value — works identically in both modes via $rd
if (isset($rd['days'])) {
$parsed = (int) $rd['days'];
if ($parsed > 0) $days = $parsed;
}
// bare flags — CLI needs $argv scan, browser uses $rd
if (UTIL::is_cli()) {
foreach (($GLOBALS['argv'] ?? []) as $arg) {
$lower = strtolower(ltrim($arg, '-'));
if ($lower === 'debug' || strpos($lower, 'debug=') === 0) $debug = true;
}
} else {
if (isset($rd['debug'])) $debug = true;
}
Output Helpers
Define simple wrapper functions around UTIL::print() to give your output consistent structure:
function out_info(string $msg): void { UTIL::print("[INFO] " . $msg); }
function out_warn(string $msg): void { UTIL::print("[WARN] " . $msg); }
function out_divider(): void { UTIL::print(str_repeat('=', 72)); }
function out_preformatted(string $text): void { UTIL::print($text); }
UTIL::print() handles both CLI (stdout) and browser (HTML) rendering automatically. Use it for all output — normal results, warnings, and errors alike. Do not use echo directly.
Layout — Optional Page Rendering
\CodePunch\UI\Layout->show() is an optional alternative that renders a complete WMD page with application chrome — navigation, header, and footer. Use it when you want your tool's output to appear as a proper WMD UI page rather than raw printed output.
To use it, collect everything you want to display into a heading string and a body string, then pass them together:
$layout = new \CodePunch\UI\Layout();
$layout->show([
'heading' => '<h3>My Tool</h3>',
'body' => $collectedOutput,
]);
Important: because Layout->show() renders a full page, you must not call UTIL::print() before it in the same request. If you use Layout, collect all output into a string first, then pass it to show() in one call.
For most simple tools, UTIL::print() throughout is perfectly sufficient — including for error output. Layout is only worth using when you specifically want the WMD page chrome around your output.
Error Handling
Wrap all tool logic in a try/catch block. Log the error via CPLogger and print it with UTIL::print() — this works correctly in both CLI and browser with no mode branching needed:
try {
$auth = new \CodePunch\Config\Auth(\CodePunch\DB\Database::READONLY_TABLES);
if (!$auth) {
throw new Exception("Failed to initialise Auth.");
}
$db = $auth->getDatabase();
if (!$db) {
throw new Exception("Failed to obtain database handle.");
}
// ... tool logic ...
}
catch (Exception $e) {
$logger = new \CodePunch\Base\CPLogger();
$logger->error($e->getMessage());
UTIL::print($e->getMessage() . ' - ' . $e->getFile() . ' - ' . $e->getLine());
exit;
}
Key points:
- Always throw explicitly when
$author$dbreturn falsy — do not use silentif ($auth) { ... }nesting, which swallows failures. - Always check
getFromTable()for=== falsebefore checkingempty(). CPLoggerwrites to WMD's own log, making errors visible in the WMD admin log viewer.- There is no need to branch on
UTIL::is_cli()in the catch block —UTIL::print()handles both modes correctly on its own.
Debug Mode
Adding a debug mode to your tool is good practice. A --debug flag in CLI or ?debug=1 in the browser gives you a controlled way to inspect what the tool is doing without modifying the code.
Useful things to expose in debug mode include the SQL being executed, the bound parameter values, and a sample of the raw data returned from the database. This makes it significantly faster to diagnose problems with query logic, column name mismatches, or unexpected data.
Debug output should always be clearly labelled and easy to distinguish from normal tool output.
Full Example: Domain Expiry Notifier
The following is a complete annotated skeleton showing all the patterns above working together.
File Header and Configuration
<?php require '../lib/php/loader.php'; const DEFAULT_DAYS = 30; const CLIENT_EMAIL_COLUMN = 'client_email'; const UNASSIGNED_KEY = '__unassigned__'; use CodePunch\Base\Util as UTIL; use CodePunch\Base\Text as TEXT; use CodePunch\DB\Audit as AUDIT;
Bootstrap
if (UTIL::is_cli()) {
UTIL::parse_request_data_in_cli();
}
$rd = UTIL::get_unsafe_request_data_array();
Flag Resolution
$days = DEFAULT_DAYS;
$debug = $dryRun = false;
if (UTIL::is_cli()) {
foreach (($GLOBALS['argv'] ?? []) as $arg) {
$a = strtolower(ltrim($arg, '-'));
if ($a === 'debug' || strpos($a, 'debug=') === 0) $debug = true;
if ($a === 'dry-run' || strpos($a, 'dry-run=') === 0) $dryRun = true;
}
} else {
if (isset($rd['debug'])) $debug = true;
if (isset($rd['dry-run'])) $dryRun = true;
}
if (isset($rd['days']) && (int)$rd['days'] > 0) {
$days = (int)$rd['days'];
}
Main Try/Catch Block
try {
$auth = new \CodePunch\Config\Auth(\CodePunch\DB\Database::READONLY_TABLES);
if (!$auth) throw new Exception("Auth init failed.");
if (!UTIL::is_cli()) {
// RECOMMENDED: disable browser access entirely instead:
// UTIL::print("This tool can only be run from the command line.");
// exit;
$status = $auth->validateSession(false, false);
if ($status !== \CodePunch\Config\Auth::VALID) {
header("Location: ../login.php");
exit;
}
}
$db = $auth->getDatabase();
if (!$db) throw new Exception("Database handle failed.");
$table = $db->getDomainTableName();
$today = date('Y-m-d');
$rows = $db->getFromTable($columns, $table, $where, $params, 'domain', 'asc', 10000);
if ($rows === false) throw new Exception("Query failed.");
foreach ($rows as $row) {
// process rows...
}
}
catch (Exception $e) {
$logger = new \CodePunch\Base\CPLogger();
$logger->error($e->getMessage());
UTIL::print($e->getMessage() . ' - ' . $e->getFile() . ' - ' . $e->getLine());
exit;
}
Checklist
- Unless browser access is explicitly required, exit immediately when not in CLI mode — place
if (!UTIL::is_cli()) { ... exit; }at the top of the tool before any other logic - File is in
wmdsed/user-tools/, notwmdsed/tools/ require '../lib/php/loader.php'is the first line after<?phpUTIL::parse_request_data_in_cli()is called beforeget_unsafe_request_data_array()in CLI mode- Bare CLI flags are detected via
$argvscan, not just$rd AuthandgetDatabase()return values are checked and throw on failure- In browser mode,
$auth->validateSession(false, false)is called immediately afterAuthinstantiation and before any database access — any status other thanAuth::VALIDexits immediately getDomainTableName()is used — table name is never hardcoded- All SQL uses
?placeholders with a$paramsarray getFromTable()return value is checked for=== false- All output goes through
UTIL::print()— including errors in the catch block - If using
Layout->show(), all output is collected into a string first and passed as the body — never mixed withUTIL::print()calls in the same request - Exceptions are caught, logged via
CPLogger, and printed viaUTIL::print() - A debug mode is provided to inspect SQL, parameters, and raw data without modifying the code
- A dry-run mode is provided for tools that write or send data, showing what would happen without doing it
This guide covers direct database access tools only. For the REST API approach see the WMD SED Custom Tools Developer Guide.