DomainPunch Logo

 A Service of Softnik Technologies

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


Overview

WMD SED exposes two ways to interact with it programmatically:

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:

Direct Access Tools

Use direct database access when:


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:

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:

ConstantValueMeaning
Auth::VALID0Session is active and authenticated — proceed
Auth::TIMEDOUT1Session has expired
Auth::INVALID-1Session is not authenticated
Auth::UNKNOWN-2Session state cannot be determined
Auth::ERROR-3An 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
sidInternal domain ID (primary key)
domainDomain name
registry_expiryExpiry date from registry WHOIS
registrar_expiryExpiry date from registrar WHOIS
statusEPP status codes (comma-separated string)
availabilityDomain availability status e.g. Not Available, available, possibly available
primary_whois_checked_atTimestamp of last registry WHOIS lookup
secondary_whois_checked_atTimestamp 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:


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


This guide covers direct database access tools only. For the REST API approach see the WMD SED Custom Tools Developer Guide.

Close