v6.5.4 Addon System

Addon Developer Guide

Complete reference for building addons for SNetworks PHP Classifieds. WordPress-style hooks, per-theme templates, admin pages, and full API documentation.

20
Sections
19
Hook Points
Full
API Reference
SNetworks addon system architecture diagram showing core platform with hook points connecting addon modules
1

Overview

The SNetworks addon system provides a WordPress-style architecture for extending the classifieds platform without modifying core files.

The SNetworks addon system provides a WordPress-style architecture for extending the classifieds platform without modifying core files. Addons can:

  • Hook into template rendering at predefined points
  • Register CSS and JavaScript assets
  • Add admin panel pages accessible from the sidebar
  • Create and manage custom database tables
  • Provide theme-specific templates for each supported theme
  • Be installed via ZIP upload or manual folder placement

The system consists of three core components:

  • addon-hooks.inc.php — The hook/action/filter engine
  • addon-manager.cls.php — Discovery, lifecycle management, ZIP upload
  • admin/addons.php — Admin UI for managing addons
2

Getting Started

Minimum requirements, quick-start walkthrough, and your first working addon in five steps.

Minimum Requirements

  • SNetworks PHP Classifieds v6.5.2 or later
  • PHP 7.4+ (8.3 recommended)
  • Write access to the addons/ directory

Quick Start

  1. Create a directory inside addons/ with your addon ID as the folder name
  2. Create a manifest file addon.json with id, name, version, description, author, themes
  3. Create the entry point main.php with an action hook
  4. Go to Admin > Addons > Manage Addons and click Activate
  5. Visit any page to see your addon in action
addon.json
{
    "id": "my-first-addon",
    "name": "My First Addon",
    "version": "1.0.0",
    "description": "A simple addon to learn the system",
    "author": "Your Name",
    "themes": ["backpage", "snet-phoenix"]
}
main.php
<?php
add_action('content_before', function() {
    echo '<div style="background:#ffe;padding:8px;text-align:center;">Hello from My First Addon!</div>';
});
3

Addon Directory Structure

Required and optional files, folder conventions, and how the addon loader discovers your code.

addons/
  my-addon/
    addon.json              # Required — manifest file
    main.php                # Required — entry point, loaded when addon is active
    install.php             # Optional — runs on first activation
    update.php              # Optional — runs on version upgrade (v6.5.4+)
    uninstall.php           # Optional — runs when addon is uninstalled
    admin/                  # Optional — admin page files
      settings.php          #   Wrapped inside admin chrome automatically
      reports.php
    templates/              # Optional — per-theme template parts
      backpage/             #   Templates for the backpage theme
        my-widget.php
        sidebar.php
      snet-phoenix/         #   Templates for the snet-phoenix theme
        my-widget.php
        sidebar.php
    assets/                 # Optional — static files (publicly accessible)
      css/
        styles.css
      js/
        app.js
      images/
        icon.png

Important Notes

  • The folder name MUST match the id field in addon.json
  • Only files inside assets/ are directly accessible via HTTP
  • All PHP files are protected from direct access by addons/.htaccess
  • Templates must be organized by theme name (matching the directory name in themes/)
4

The Manifest File (addon.json)

Every addon requires an addon.json in its root directory. This section documents the full schema and validation rules.

Full Schema

addon.json
{
    "id": "my-addon",
    "name": "Human-Readable Name",
    "version": "1.0.0",
    "description": "A one-line description of what this addon does.",
    "author": "Author Name",
    "author_url": "https://example.com",
    "min_app_version": "6.5.2",
    "themes": ["backpage", "snet-phoenix"],
    "admin_pages": [
        {
            "slug": "settings",
            "title": "My Addon Settings",
            "file": "admin/settings.php"
        }
    ],
    "on_install": "install.php",
    "on_update": "update.php",
    "on_uninstall": "uninstall.php"
}

Field Reference

FieldRequiredTypeDescription
idYesstringUnique identifier. Must match folder name. Allowed characters: lowercase a-z, digits 0-9, hyphens -. Cannot start with a hyphen.
nameYesstringDisplay name shown in the admin panel.
versionYesstringSemantic version (e.g., "1.0.0", "2.1.3").
descriptionNostringShort description shown in admin panel.
authorNostringAuthor name displayed in addon listing.
author_urlNostringURL to author's website. If provided, author name becomes a link.
min_app_versionNostringMinimum SNetworks version required (informational).
themesNoarrayList of supported theme directory names (e.g., ["backpage", "snet-phoenix"]). Displayed in admin listing.
admin_pagesNoarrayArray of admin page definitions (see Admin Pages section).
on_installNostringPath to install script relative to addon root. Defaults to install.php.
on_updateNostringPath to update script relative to addon root. Defaults to update.php. Runs on version upgrade (v6.5.4+).
on_uninstallNostringPath to uninstall script relative to addon root. Defaults to uninstall.php.

ID Naming Rules

The addon ID is used as the directory name inside addons/, the database identifier in clf_addons, and the first parameter of addon_template(), addon_enqueue_css(), and addon_enqueue_js().

Validation regex: /^[a-z0-9][-a-z0-9]*$/

  • Valid: my-addon, email-blocker, ad-navigation, stats2
  • Invalid: My_Addon, my addon, -starts-with-hyphen, UPPERCASE
5

The Entry Point (main.php)

When an addon is activated, main.php is included once during the bootstrap phase. This is where you register hooks, enqueue assets, and define callbacks.

Execution Timing

main.php runs at a specific point in the bootstrap sequence. All global variables are available and you can safely register hooks and enqueue assets.

  • After database connection is established
  • After $xtheme (active theme) is set
  • After session initialization
  • After core globals ($xview, $xadid, $xsubcatid, etc.) are populated
  • Before page output begins

Typical main.php Structure

main.php
<?php
/**
 * My Addon — main entry point
 */

// 1. Enqueue assets (must be done here, before head_styles/footer_scripts fire)
addon_enqueue_css('my-addon', 'css/styles.css');
addon_enqueue_js('my-addon', 'js/app.js');

// 2. Register action hooks
add_action('header_nav_end', 'myaddon_header_button');
add_action('showad_after_title', 'myaddon_show_widget');
add_action('footer_links', 'myaddon_footer_link');

// 3. Register filter hooks (if any)
// add_filter('some_filter', 'myaddon_filter_value');

// 4. Define callback functions
function myaddon_header_button()
{
    echo '<li><a href="/my-page">My Feature</a></li>';
}

function myaddon_show_widget()
{
    global $xadid, $xsubcatid;
    if (!$xadid) return;

    // Use per-theme templates for proper styling
    addon_template('my-addon', 'widget', array(
        'ad_id' => $xadid,
        'data'  => 'some value',
    ));
}

function myaddon_footer_link()
{
    echo '<span class="sep">&middot;</span> <a href="/my-page">My Addon</a>';
}

Do's and Don'ts

Do:

  • Prefix all function names with your addon ID to avoid collisions (e.g., myaddon_render)
  • Check context before rendering (e.g., if ($xview !== 'showad') return;)
  • Use addon_template() for HTML output to support multiple themes
  • Enqueue assets at the top level, not inside callbacks

Don't:

  • Output HTML directly at the top level of main.php (it will appear on every page)
  • Use generic function names that could collide (e.g., render(), init())
  • Modify core global variables
  • Include files from outside your addon directory
6

Action Hooks

Actions allow your addon to execute code at specific points in the page lifecycle. They are "fire and forget" — your callback runs and any output is echoed in place.

Registering an Action

add_action($hook_name, $callback, $priority);
ParameterTypeDefaultDescription
$hook_namestringThe hook to attach to (e.g., 'showad_after_title')
$callbackcallableFunction name (string), closure, or [object, 'method']
$priorityint10Execution order. Lower numbers run first.

Priority System

When multiple callbacks are registered for the same hook, they execute in priority order:

add_action('content_before', 'run_second', 20);
add_action('content_before', 'run_first', 5);
add_action('content_before', 'run_also_second', 20);  // Same priority = order of registration

Execution order: run_first (5) → run_second (20) → run_also_second (20)

Callback Types

// Named function
add_action('header_nav_end', 'my_function');

// Anonymous function (closure)
add_action('footer_links', function() {
    echo '<span class="sep">&middot;</span> <a href="#">Link</a>';
});

// Object method
add_action('content_before', array($myObject, 'renderBanner'));

Checking if a Hook Has Callbacks

if (has_action('showad_after_title'))
{
    // At least one addon is hooked into showad_after_title
}
7

Filter Hooks

Filters allow your addon to modify a value as it passes through the system. Unlike actions, filters receive a value, transform it, and must return the result.

Registering a Filter

add_filter($hook_name, $callback, $priority);

Applying a Filter

For theme or core developers who want to make a value filterable:

$title = apply_filters('ad_title', $original_title);
$price = apply_filters('ad_price_display', $price, $currency, $post);

Writing a Filter Callback

Your callback receives the current value as the first parameter and must return a value:

add_filter('ad_title', function($title) {
    return strtoupper($title);  // Must return the modified value
});

// With additional arguments
add_filter('ad_price_display', function($price, $currency, $post) {
    if ($price == 0) return 'Free';
    return $currency . number_format($price, 2);
}, 10);

Checking if a Filter Has Callbacks

if (has_filter('ad_title'))
{
    $title = apply_filters('ad_title', $title);
}

Note

The core system currently uses action hooks at template hook points. Filter hooks are available for addon-to-addon communication or for future core integration.

8

Available Hook Points

All 19 do_action() hook points available in the template system, organized by page area. They are identical in both themes unless noted.

Layout Hooks (_layout.php)

These fire on every page and are the most commonly used hooks.

HookLocationTypical Use
head_stylesAfter theme CSS <link> tags, before </head>Addon CSS (use addon_enqueue_css() instead)
head_scriptsAfter theme JS <script> tags, before </head>Addon head JS
header_beforeBefore the header includeAnnouncement bars, notices
header_afterAfter the header includeAccount bars, sub-navigation
content_beforeBefore <?php echo $content; ?>Page-wide banners, alerts
content_afterAfter <?php echo $content; ?>Page-wide widgets, CTAs
footer_beforeBefore the footer includePre-footer widgets
footer_scriptsBefore </body>Addon JS (use addon_enqueue_js() instead)

Header Hooks (parts/header.php)

HookLocationTypical Use
header_nav_endAfter the last navigation item in the header menuAccount links, extra nav buttons

Backpage context: Inside .bp-header-actions, after the "My Account" button. Phoenix context: Inside .nav.navbar-nav.navbar-right, after the location dropdown <li>.

Footer Hooks (parts/footer.php)

HookLocationTypical Use
footer_linksAfter Terms/Privacy links, inside the footer links areaExtra footer links

Homepage Hooks (main.php)

HookLocationTypical Use
main_before_categoriesBefore the categories gridFeatured banners, announcements
main_after_categoriesAfter the categories gridPromotional content, ads

Ad Detail Page Hooks (showad.php)

HookLocationTypical Use
showad_before_titleBefore the ad title headerBadges, status indicators
showad_after_titleAfter the title and subtitleNext/prev navigation, breadcrumbs
showad_before_detailsBefore the ad description textWarnings, disclaimers
showad_after_detailsAfter the ad description textRelated ads, recommendations
showad_after_contactAfter the contact form sectionAdditional contact options, maps

Tip

The $post argument is passed in the backpage theme. In snet-phoenix, $post is available as a global instead. Always use global $post; in your callback for cross-theme compatibility.

Ad Listing Page Hooks (ads.php)

HookLocationTypical Use
ads_before_listingBefore the ad listing gridFilters, sorting controls
ads_after_listingAfter the listing and paginationLoad more buttons, suggestions

Cross-Theme Compatibility Note

When writing callbacks for showad_* hooks, always access data via globals rather than relying on hook arguments:

function my_showad_callback()
{
    // GOOD: Works in both themes
    global $xadid, $xsubcatid, $xad, $post;

    // AVOID: Only works in backpage (passes $post as argument)
    // function my_showad_callback($post) { ... }
}
9

Per-Theme Templates

Since each theme has different HTML structure and CSS classes, addons must provide separate templates for each supported theme.

Directory Layout

addons/my-addon/templates/
  backpage/
    widget.php
    sidebar.php
  snet-phoenix/
    widget.php
    sidebar.php

Rendering a Template

From your hook callback:

addon_template('my-addon', 'widget', array(
    'items'    => $items,
    'show_all' => true,
    'ad_id'    => $xadid,
));

The function addon_template($addon_id, $template_name, $vars):

  1. Looks for addons/{addon_id}/templates/{current_theme}/{template_name}.php
  2. If not found, falls back to the first available theme directory
  3. Extracts $vars into the template's local scope
  4. Makes all global variables available in the template scope
  5. Includes the template file

Writing Template Files

Templates have access to all variables passed in the $vars array (extracted into local scope), all global variables, and all output helper functions (out(), outlang(), etc.).

addons/my-addon/templates/backpage/widget.php
<div class="bp-my-widget">
    <h3>My Widget</h3>
    <?php if ($show_all): ?>
        <ul>
        <?php foreach ($items as $item): ?>
            <li><a href="<?php out($item['url']); ?>"><?php out($item['title']); ?></a></li>
        <?php endforeach; ?>
        </ul>
    <?php endif; ?>
</div>
addons/my-addon/templates/snet-phoenix/widget.php
<div class="panel panel-default">
    <div class="panel-heading">My Widget</div>
    <div class="panel-body">
        <?php if ($show_all): ?>
            <div class="list-group">
            <?php foreach ($items as $item): ?>
                <a href="<?php out($item['url']); ?>" class="list-group-item">
                    <?php out($item['title']); ?>
                </a>
            <?php endforeach; ?>
            </div>
        <?php endif; ?>
    </div>
</div>

Theme Detection

If you need to know the current theme in your callback (e.g., to output theme-specific inline markup):

global $xtheme;
if ($xtheme === 'backpage') {
    // Backpage-specific logic
}
10

Enqueueing CSS and JavaScript

Register CSS and JavaScript assets so they are automatically included in the page head and footer respectively.

CSS

Call addon_enqueue_css() at the top level of main.php (not inside a callback):

addon_enqueue_css('my-addon', 'css/styles.css');

This registers a <link> tag on the head_styles hook. The path is relative to your addon's assets/ directory: addons/my-addon/assets/css/styles.css

The generated HTML:

<link rel="stylesheet" href="https://yoursite.com/addons/my-addon/assets/css/styles.css"/>

JavaScript

Call addon_enqueue_js() at the top level of main.php:

addon_enqueue_js('my-addon', 'js/app.js');

This registers a <script> tag on the footer_scripts hook (before </body>). The path is relative to assets/: addons/my-addon/assets/js/app.js

Multiple Assets

addon_enqueue_css('my-addon', 'css/styles.css');
addon_enqueue_css('my-addon', 'css/responsive.css');
addon_enqueue_js('my-addon', 'js/vendor/library.min.js');
addon_enqueue_js('my-addon', 'js/app.js');

Inline Scripts

For small amounts of inline JavaScript, use the footer_scripts hook directly:

add_action('footer_scripts', function() {
    echo '<script>console.log("My addon loaded");</script>' . "\n";
});

Conditional Loading

To load assets only on certain pages:

add_action('head_styles', function() {
    global $xview;
    if ($xview === 'showad') {
        $url = absurl('addons/my-addon/assets/css/showad-only.css');
        echo '<link rel="stylesheet" href="' . htmlspecialchars($url) . '"/>' . "\n";
    }
});
11

Admin Pages

Addons can register their own admin pages that appear in the admin sidebar under the "Addons" section.

yoursite.com/admin/addons.php
SNetworks admin panel addon management page showing ZIP upload panel and installed addons list with status badges and action buttons
The Manage Addons admin page — upload addons via ZIP or activate/deactivate installed addons

Registering Admin Pages

In addon.json:

addon.json (excerpt)
{
    "admin_pages": [
        {
            "slug": "settings",
            "title": "My Addon Settings",
            "file": "admin/settings.php"
        },
        {
            "slug": "reports",
            "title": "Reports",
            "file": "admin/reports.php"
        }
    ]
}
FieldDescription
slugURL identifier for the page (used in ?page= parameter)
titleDisplay text in the admin sidebar menu
filePath to the PHP file, relative to addon root

How Admin Pages Work

When someone clicks your admin page link in the sidebar:

  1. The URL is admin/addon-page.php?addon=my-addon&page=settings
  2. addon-page.php validates admin authentication
  3. It verifies the addon and page slug exist
  4. It includes aheader.inc.php (header + sidebar), your page file, then afooter.inc.php

Your page file is already wrapped in the admin layout — you just output your content.

Writing an Admin Page

addons/my-addon/admin/settings.php
<?php
global $addon_manager, $t_addons;

$addon_id = 'my-addon';

// Handle form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST')
{
    $setting_value = $_POST['my_setting'] ?? '';
    $escaped_value = mysql_real_escape_string($setting_value);
    $escaped_id = mysql_real_escape_string($addon_id);

    // Store settings in the addon's DB row
    $settings = json_encode(array('my_setting' => $setting_value));
    $escaped_settings = mysql_real_escape_string($settings);
    execute("UPDATE $t_addons SET settings = '$escaped_settings' WHERE addon_id = '$escaped_id'");

    echo '<div class="msg">Settings saved.</div>';
}

// Load current settings
$db_info = $addon_manager->get_db_info($addon_id);
$settings = json_decode($db_info['settings'] ?? '{}', true);
$current_value = $settings['my_setting'] ?? '';
?>

<h2>My Addon Settings</h2>

<form method="post">
    <table class="datatable" width="100%">
        <tr>
            <td width="200"><b>My Setting:</b></td>
            <td>
                <input type="text" name="my_setting"
                    value="<?php echo htmlspecialchars($current_value); ?>"
                    size="40" />
            </td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td><button type="submit">Save Settings</button></td>
        </tr>
    </table>
</form>

Admin Page Styling

Your admin pages inherit the core admin CSS. Use these existing classes:

  • class="datatable" — Standard data table
  • class="box" — Boxed container
  • class="msg" — Success message (green)
  • class="err" — Error message (red)
  • class="tip" — Info/tip message
  • class="head" — Table header row
  • class="row1" / class="row2" — Alternating table rows
12

Install, Update, and Uninstall Hooks

Lifecycle scripts that run on first activation, on version upgrade, and on uninstall. Use them to create, migrate, or tear down database tables and seed data.

Install Hook (on_install)

The install file runs once — the first time the addon is activated. Use it to create database tables, seed default data, etc.

addons/my-addon/install.php
<?php
/**
 * My Addon — Installation
 * Runs on first activation only.
 */

// Create custom table
$sql = "CREATE TABLE IF NOT EXISTS clf_myaddon_data (
    id INT AUTO_INCREMENT PRIMARY KEY,
    ad_id INT NOT NULL,
    data TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_ad_id (ad_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8";
execute($sql);

Note

The install file runs in the global scope with all core functions available. After the first activation, it will not run again (the installed flag is set in the database). To re-run it, uninstall and re-activate the addon.

Update Hook (on_update) — v6.5.4+

The update file runs when an already-installed addon is activated with a newer version than what is stored in the database. Use it to run database migrations, add new columns, update default settings, etc.

Two variables are available inside the update script:

  • $old_version — The version previously stored in the database
  • $new_version — The version from the current addon.json
addons/my-addon/update.php
<?php
/**
 * My Addon — Update
 * Runs when addon is activated with a newer version.
 * Available variables: $old_version, $new_version
 */

// v1.0.0 → v1.1.0: Add status column
if (version_compare($old_version, '1.1.0', '<'))
{
    execute("ALTER TABLE clf_myaddon_data ADD COLUMN status VARCHAR(20) DEFAULT 'active'");
}

// v1.1.0 → v1.2.0: Add index for performance
if (version_compare($old_version, '1.2.0', '<'))
{
    execute("ALTER TABLE clf_myaddon_data ADD INDEX idx_status (status)");
}

Tip

Use version_compare($old_version, 'x.y.z', '<') guards so that each migration only runs once and migrations can be skipped when jumping multiple versions (e.g., v1.0.0 straight to v1.3.0 will run all three blocks in order).

Note

The update hook does NOT run if the version is the same or lower. To force a re-run, bump the version in addon.json.

Uninstall Hook (on_uninstall)

The uninstall file runs when an admin clicks "Uninstall" on the addon management page. Use it to clean up database tables and data.

addons/my-addon/uninstall.php
<?php
/**
 * My Addon — Uninstallation
 * Clean up all addon data.
 */

// Drop custom tables
execute("DROP TABLE IF EXISTS clf_myaddon_data");

Note

Uninstalling removes the database record from clf_addons but does NOT delete the addon's files from the addons/ directory. The addon can be re-discovered and re-activated. To fully remove it, manually delete the folder.

Deactivation vs Uninstallation

ActionWhat HappensData Preserved?Files Preserved?
DeactivateSets status to inactive in DB. Addon stops loading.YesYes
UpdateRuns on_update when re-activated with a newer version.YesYes (overwritten by new ZIP)
UninstallRuns on_uninstall, deletes DB rowNo (if uninstall script drops tables)Yes
Manual deleteRemove folder from addons/DependsNo
13

Database Access

The addon system uses the same database functions available throughout the codebase. All tables use the clf_ prefix.

Query Functions

// SELECT — returns array of associative arrays
$rows = query("SELECT * FROM clf_myaddon_data WHERE ad_id = 123");

// SELECT — returns array keyed by specified field
$rows = query("SELECT * FROM clf_myaddon_data", 'id');

// SELECT — returns first row only (single record)
$row = query_record("SELECT * FROM clf_myaddon_data WHERE id = 1");

// SELECT — returns first column of first row (single value)
$count = query_value("SELECT COUNT(*) FROM clf_myaddon_data");

// INSERT / UPDATE / DELETE — returns affected row count
$affected = execute("INSERT INTO clf_myaddon_data (ad_id, data) VALUES (123, 'hello')");

Escaping Input

Always escape user input before using it in queries:

$user_input = mysql_real_escape_string($_POST['value']);
$sql = "INSERT INTO clf_myaddon_data (data) VALUES ('$user_input')";
execute($sql);

Table Naming Convention

Prefix your addon's tables with clf_ followed by your addon ID (with hyphens replaced by underscores):

Addon ID: my-addon
Table names: clf_myaddon_data, clf_myaddon_settings, clf_myaddon_logs

Storing Addon Settings

The clf_addons table has a settings TEXT column for each addon. Use it for simple key-value settings:

// Save settings
global $t_addons;
$settings = json_encode(array('option1' => 'value1', 'option2' => true));
$escaped = mysql_real_escape_string($settings);
execute("UPDATE $t_addons SET settings = '$escaped' WHERE addon_id = 'my-addon'");

// Load settings
global $addon_manager;
$db_info = $addon_manager->get_db_info('my-addon');
$settings = json_decode($db_info['settings'] ?? '{}', true);
$option1 = $settings['option1'] ?? 'default';
14

Available Global Variables

These globals are available in your main.php, hook callbacks, and templates. They provide access to the current page state, location, category, ad data, and site configuration.

Core State

VariableTypeDescription
$xviewstringCurrent page view: 'main', 'ads', 'showad', 'showevent', 'post', 'edit', 'imgs', etc.
$xthemestringActive theme directory name (e.g., 'backpage', 'snet-phoenix')
$xzAppDataPreloaded data object (see below)
$themeThemeTheme engine instance
$addon_managerAddonManagerThe addon manager instance

Location Context

VariableTypeDescription
$xcityidintCurrent city ID (0 = all cities, -1 = all regions)
$xcitynamestringCurrent city name
$xcountryidintCurrent country/region ID
$xcountrynamestringCurrent country/region name

Category Context (set on ads/showad pages)

VariableTypeDescription
$xcatidintCurrent category ID
$xcatnamestringCurrent category name
$xsubcatidintCurrent subcategory ID
$xsubcatnamestringCurrent subcategory name

Ad Context (set on showad page only)

VariableTypeDescription
$xadidintCurrent ad ID
$xadarrayRaw ad record from database
$postarrayProcessed ad record with computed fields (set in showad.php view file)
$xadtypestring'A' for ads, 'E' for events

Site Configuration

VariableTypeDescription
$site_namestringSite name from settings
$site_emailstringSite email from settings
$script_urlstringBase URL of the site
$admin_loggedboolWhether an admin is logged in
$demoboolWhether the site is in demo mode
$debugboolWhether debug mode is on

Database Tables

VariableTable
$t_adsclf_ads
$t_eventsclf_events
$t_catsclf_categories
$t_subcatsclf_subcategories
$t_citiesclf_cities
$t_countriesclf_countries
$t_adxfieldsclf_adxfields
$t_addonsclf_addons
$tprefixclf_ (the table prefix)

The $xz Object

$xz is an AppData instance with preloaded arrays:

$xz->cats        // All categories, keyed by catid
$xz->subcats     // All subcategories, keyed by subcatid
$xz->regions     // All regions/countries, keyed by countryid
$xz->cities      // All cities, keyed by cityid
$xz->areas       // All areas
$xz->params      // Request parameters
$xz->settings    // Settings instance

Example usage:

$cat_name = $xz->cats[$catid]['catname'];
$city_name = $xz->cities[$cityid]['cityname'];
$region_name = $xz->regions[$countryid]['countryname'];
15

Available Helper Functions

Commonly used utility functions for output escaping, URL generation, database queries, formatting, and validation.

Output

FunctionDescription
out($value)Echo HTML-escaped value
out($value, HTML)Echo raw HTML (no escaping)
outlang($key)Echo a language phrase from $lang
outlang($key, $params)Echo language phrase with parameter substitution
outlang($key, $params, HTML)Echo language phrase as raw HTML

URLs

FunctionDescription
absurl($path)Get absolute URL to a path (e.g., absurl('images/logo.png'))
buildURL($type, $params)Build a SEF or query-string URL

buildURL() types and parameters:

// Homepage
buildURL('main', array($cityid));

// Ad listing
buildURL('ads', array($cityid, $catid, $catname, $subcatid, $subcatname));

// Show ad
buildURL('showad', array($cityid, $catid, $catname, $subcatid, $subcatname, $adid, $adtitle));

// Show event
buildURL('showevent', array($cityid, $date, $adid, $adtitle));

Database

FunctionDescription
query($sql)Execute SELECT, return array of rows
query($sql, $id_field)Execute SELECT, return rows keyed by $id_field
query_record($sql)Return first row of SELECT result
query_value($sql)Return first column of first row
execute($sql)Execute INSERT/UPDATE/DELETE, return affected rows
mysql_real_escape_string($str)Escape string for SQL (uses bridge)

Formatting

FunctionDescription
price($amount)Format a price with currency symbol
price_detailed($amount)Format price with more detail
QuickDate($timestamp, $hide_time)Format a date from Unix timestamp

Validation

FunctionDescription
ValidateEmail($email)Check if email is valid
check_numeric($value)Validate numeric input (dies on failure)
numerize($value)Coerce value to number safely
16

Distribution (ZIP Packaging)

How to package your addon as a ZIP file for distribution and how the upload and validation process works.

Creating a ZIP Package

Structure your ZIP file so that all addon files are inside a single root directory:

my-addon/
  addon.json
  main.php
  install.php
  update.php
  uninstall.php
  admin/
    settings.php
  templates/
    backpage/
      widget.php
    snet-phoenix/
      widget.php
  assets/
    css/
      styles.css
    js/
      app.js

Create the ZIP:

cd addons/
zip -r my-addon.zip my-addon/

Upload Process

  1. Admin navigates to Addons > Manage Addons
  2. Selects the ZIP file and clicks Upload & Install
  3. The system validates the ZIP structure, locates and parses addon.json, validates the addon ID format, extracts to addons/ directory, renames the directory to match the addon ID if needed, and re-discovers all addons
  4. The addon appears in the listing as Inactive
  5. Admin clicks Activate to enable it

Upgrading an Addon

When uploading a ZIP for an addon that already exists on the system:

  1. The system detects the existing addon and displays an "updated" message with the version change (e.g., "v1.0.0 → v1.1.0")
  2. Files are overwritten with the new version. Settings and database data are preserved.
  3. On the next Activate (or re-activate), if the new addon.json version is higher than the stored version, the on_update script runs
  4. Include an update.php in your addon with version-gated migrations (see Install, Update, and Uninstall Hooks section)

ZIP Validation Rules

The upload will be rejected if:

  • The file is not a valid ZIP archive
  • No addon.json is found (must be at root or one level deep)
  • addon.json is missing or has no id field
  • The addon ID contains invalid characters
17

Security Considerations

Best practices for PHP file protection, input escaping, admin authentication, and file system safety.

PHP File Protection

The addons/.htaccess file prevents direct HTTP access to PHP files:

addons/.htaccess
<FilesMatch "\.php$">
    Order Deny,Allow
    Deny from all
</FilesMatch>

Only static assets (CSS, JS, images, fonts) can be served directly. All addon PHP code runs through the core include chain.

Admin Page Authentication

Addon admin pages are always wrapped by admin/addon-page.php, which requires aauth.inc.php. This means admin authentication is automatically enforced — you don't need to add auth checks in your admin pages.

Input Escaping

Always escape user input in SQL queries:

$safe = mysql_real_escape_string($_POST['value']);
execute("UPDATE my_table SET col = '$safe' WHERE id = 1");

Always escape output in templates:

<?php out($user_data); ?>              <!-- HTML-escaped (safe) -->
<?php echo $user_data; ?>              <!-- UNSAFE — avoid this -->
<?php out($trusted_html, HTML); ?>     <!-- Raw HTML — only for trusted content -->

File System Safety

  • Never write files outside your addon directory
  • Never dynamically include files based on user-supplied paths
  • Validate file uploads thoroughly
  • Use the existing absurl() function for generating URLs
18

Complete Example: Building an Addon from Scratch

A full walkthrough building an "Email Block" addon that lets admins block specific email domains from posting ads, covering all nine steps from directory structure to ZIP packaging.

Let's build an "Email Block" addon that lets admins block specific email domains from posting ads.

Step 1: Create Directory Structure

addons/email-block/
  addon.json
  main.php
  install.php
  uninstall.php
  admin/
    settings.php
  templates/
    backpage/
      blocked-notice.php
    snet-phoenix/
      blocked-notice.php
  assets/
    css/
      styles.css

Step 2: Manifest (addon.json)

addon.json
{
    "id": "email-block",
    "name": "Email Domain Blocker",
    "version": "1.0.0",
    "description": "Block ad posts from specific email domains",
    "author": "SNetworks",
    "min_app_version": "6.5.2",
    "themes": ["backpage", "snet-phoenix"],
    "admin_pages": [
        {
            "slug": "blocked-domains",
            "title": "Blocked Domains",
            "file": "admin/settings.php"
        }
    ],
    "on_install": "install.php",
    "on_uninstall": "uninstall.php"
}

Step 3: Install Script (install.php)

install.php
<?php
$sql = "CREATE TABLE IF NOT EXISTS clf_emailblock_domains (
    id INT AUTO_INCREMENT PRIMARY KEY,
    domain VARCHAR(255) NOT NULL,
    added_on DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY unique_domain (domain)
) ENGINE=InnoDB DEFAULT CHARSET=utf8";
execute($sql);

Step 4: Uninstall Script (uninstall.php)

uninstall.php
<?php
execute("DROP TABLE IF EXISTS clf_emailblock_domains");

Step 5: Entry Point (main.php)

main.php
<?php
/**
 * Email Domain Blocker
 * Blocks ad submissions from blacklisted email domains.
 */

addon_enqueue_css('email-block', 'css/styles.css');

// Hook into the ad posting process
add_action('content_before', 'emailblock_check_post');

function emailblock_check_post()
{
    global $xview;

    // Only run on the post/edit views
    if ($xview !== 'post' && $xview !== 'edit') return;
    if (empty($_POST['email'])) return;

    $email = $_POST['email'];
    $domain = strtolower(substr(strrchr($email, '@'), 1));

    if (!$domain) return;

    $escaped_domain = mysql_real_escape_string($domain);
    $blocked = query_value(
        "SELECT COUNT(*) FROM clf_emailblock_domains WHERE domain = '$escaped_domain'"
    );

    if ($blocked > 0)
    {
        addon_template('email-block', 'blocked-notice', array(
            'domain' => $domain,
        ));
    }
}

Step 6: Templates

templates/backpage/blocked-notice.php
<div class="bp-email-blocked-notice">
    <i class="fa fa-ban"></i>
    <strong>Posting blocked:</strong> The email domain
    <em><?php out($domain); ?></em> is not allowed.
</div>
templates/snet-phoenix/blocked-notice.php
<div class="alert alert-danger">
    <i class="fa fa-ban"></i>
    <strong>Posting blocked:</strong> The email domain
    <em><?php out($domain); ?></em> is not allowed.
</div>

Step 7: Admin Page (admin/settings.php)

admin/settings.php
<?php
// Handle adding a domain
if (($_POST['action'] ?? '') === 'add' && !empty($_POST['domain']))
{
    $domain = strtolower(trim($_POST['domain']));
    $escaped = mysql_real_escape_string($domain);
    execute("INSERT IGNORE INTO clf_emailblock_domains (domain) VALUES ('$escaped')");
    echo '<div class="msg">Domain added.</div>';
}

// Handle removing a domain
if (($_GET['action'] ?? '') === 'remove' && !empty($_GET['id']))
{
    $id = intval($_GET['id']);
    execute("DELETE FROM clf_emailblock_domains WHERE id = $id");
    echo '<div class="msg">Domain removed.</div>';
}

// Load current domains
$domains = query("SELECT * FROM clf_emailblock_domains ORDER BY domain");
$page_url = "addon-page.php?addon=email-block&page=blocked-domains";
?>

<h2>Blocked Email Domains</h2>

<form method="post" style="margin-bottom: 20px;">
    <input type="hidden" name="action" value="add" />
    <input type="text" name="domain" placeholder="example.com" size="30" />
    <button type="submit">Add Domain</button>
</form>

<?php if (!empty($domains)): ?>
<table class="datatable" width="100%">
    <tr class="head">
        <td>Domain</td>
        <td>Added On</td>
        <td width="80">Actions</td>
    </tr>
    <?php $i = 0; foreach ($domains as $d): $i++; ?>
    <tr class="<?php echo ($i % 2) ? 'row1' : 'row2'; ?>">
        <td><?php echo htmlspecialchars($d['domain']); ?></td>
        <td><?php echo htmlspecialchars($d['added_on']); ?></td>
        <td>
            <a href="<?php echo $page_url; ?>&action=remove&id=<?php echo $d['id']; ?>"
               onclick="return confirm('Remove this domain?');">Remove</a>
        </td>
    </tr>
    <?php endforeach; ?>
</table>
<?php else: ?>
<p>No domains blocked yet.</p>
<?php endif; ?>

Step 8: CSS (assets/css/styles.css)

assets/css/styles.css
.bp-email-blocked-notice {
    background: #ffe0e0;
    border: 1px solid #d00;
    color: #900;
    padding: 12px 16px;
    border-radius: 4px;
    margin-bottom: 16px;
}

.bp-email-blocked-notice i {
    margin-right: 6px;
}

Step 9: Package and Distribute

cd addons/
zip -r email-block.zip email-block/
19

API Reference

Complete reference of all hook functions, template/asset helpers, AddonManager methods, and a quick-reference table of all 19 hook points.

Hook Functions

FunctionSignatureDescription
add_actionadd_action(string $hook, callable $callback, int $priority = 10): voidRegister callback for an action hook
do_actiondo_action(string $hook, mixed ...$args): voidFire all callbacks for an action hook
has_actionhas_action(string $hook): boolCheck if callbacks exist for a hook
add_filteradd_filter(string $hook, callable $callback, int $priority = 10): voidRegister callback for a filter hook
apply_filtersapply_filters(string $hook, mixed $value, mixed ...$args): mixedRun value through all filter callbacks
has_filterhas_filter(string $hook): boolCheck if filters exist for a hook

Template/Asset Functions

FunctionSignatureDescription
addon_templateaddon_template(string $addon_id, string $template_name, array $vars = []): voidRender a per-theme template
addon_enqueue_cssaddon_enqueue_css(string $addon_id, string $file): voidQueue a CSS file from assets/
addon_enqueue_jsaddon_enqueue_js(string $addon_id, string $file): voidQueue a JS file from assets/

AddonManager Methods

MethodReturnsDescription
discover()arrayScan addons/ for manifests
load_active_list()voidLoad active addon IDs from DB
load_active_addons()voidInclude main.php for each active addon
activate($addon_id)arrayActivate addon (runs install on first time)
deactivate($addon_id)arrayDeactivate addon
uninstall($addon_id)arrayRun uninstall hook, remove DB record
upload_zip($file)arrayExtract and validate ZIP upload
get_all()arrayGet all discovered addon manifests
is_active($addon_id)boolCheck if addon is active
get_loaded()arrayGet manifests of loaded (active) addons
get_admin_pages()arrayGet all admin pages from active addons
get_db_info($addon_id)array|nullGet addon's database record

Hook Points Quick Reference

HookFires OnLocation
head_stylesAll pages<head>, after theme CSS
head_scriptsAll pages<head>, after theme JS
header_beforeAll pagesBefore header
header_afterAll pagesAfter header
header_nav_endAll pagesEnd of header navigation
content_beforeAll pagesBefore page content
content_afterAll pagesAfter page content
footer_beforeAll pagesBefore footer
footer_linksAll pagesInside footer links
footer_scriptsAll pagesBefore </body>
main_before_categoriesHomepageBefore categories grid
main_after_categoriesHomepageAfter categories grid
ads_before_listingListing pageBefore ad listing
ads_after_listingListing pageAfter ad listing
showad_before_titleAd detailBefore ad title
showad_after_titleAd detailAfter ad title/subtitle
showad_before_detailsAd detailBefore description
showad_after_detailsAd detailAfter description
showad_after_contactAd detailAfter contact section
20

Troubleshooting

Common issues and their solutions, from addon discovery problems to ZIP upload failures.

Addon doesn't appear in admin listing

  • Verify addon.json exists in the addon's root directory
  • Verify addon.json is valid JSON (use a JSON validator)
  • Verify the id field is present and matches the folder name
  • Check that the addons/ directory is readable by the web server

Addon activates but nothing happens

  • Verify main.php exists in the addon's root directory
  • Check that hook names match exactly (case-sensitive)
  • Verify the hook point exists in the current theme's templates
  • Check PHP error logs for syntax errors in main.php
  • If hooking into showad_* hooks, make sure there are ads to view

Templates don't render

  • Verify the template directory matches the current theme name exactly
  • Check the template filename matches what you pass to addon_template() (without .php)
  • Verify the template file exists and is readable
  • Check $xtheme value matches your template directory name

CSS/JS not loading

  • Verify files exist in the assets/ subdirectory
  • Check the browser's Network tab for 404 errors
  • Ensure addon_enqueue_css()/addon_enqueue_js() is called at the top level of main.php, not inside a callback
  • Verify the addons/.htaccess allows the file extension

Admin page shows 404 or redirects to addon listing

  • Verify the slug in addon.json matches the ?page= parameter
  • Verify the file path in addon.json is correct and the file exists
  • Make sure the addon is active (admin pages only work for active addons)

Install script doesn't run

  • The install script only runs on first activation
  • If you need to re-run it: Uninstall the addon, then Activate again
  • Check that the on_install path in addon.json is correct
  • Check PHP error logs for errors in the install script

ZIP upload fails

  • Verify the file is a valid ZIP archive
  • Ensure addon.json is at the root level or one directory deep inside the ZIP
  • Check that the addon id uses only lowercase letters, numbers, and hyphens
  • Verify the PHP zip extension is enabled on the server
  • Check that the addons/ directory is writable by the web server
21

v6.5.4 Core Hooks — Filters & Actions in PHP

Core PHP hooks added in v6.5.4 — filters and actions embedded in core PHP files for deeper addon integration beyond UI injection. These enable addons to intercept routing, authorization, ad creation, and more.

Section 8 documents the template action hooks (do_action() calls in theme templates). This section documents the core PHP hooks — filters and actions added to core PHP files in v6.5.4 to support deeper addon integration beyond UI injection.

Filter: pre_view_route

File: index.php | Signature: apply_filters('pre_view_route', $page, $xview) Fires before the view routing switch statement. Return a full file path to override routing for custom views, or return $page unchanged to let the default switch handle it.

ParameterTypeDescription
$pagestringCurrent page file to include (default: "main.php")
$xviewstringThe ?view= GET parameter (e.g., 'post', 'showad', 'login')
add_filter('pre_view_route', 'myaddon_route_views', 10);

function myaddon_route_views($page, $xview)
{
    if ($xview === 'my-custom-page') {
        return __DIR__ . '/pages/my-custom-page.php';
    }
    return $page;
}

Warning

The default switch statement runs after this filter. For built-in views like post, edit, showad, the switch will overwrite $page regardless. To intercept these views, perform a redirect (header() + exit) inside the filter callback rather than returning a different page path.

Action: post_after_insert

File: post.php | Signature: do_action('post_after_insert', $adid, $adtype, $data) Fires immediately after a new ad or event is inserted into the database, before image uploads and verification email.

ParameterTypeDescription
$adidintThe newly created ad/event ID
$adtypestring"ad" for regular ads, "event" for events
$dataarrayThe submitted form data (title, description, email, etc.). Values are already escaped for SQL.
add_action('post_after_insert', 'myaddon_on_new_post', 10);

function myaddon_on_new_post($adid, $adtype, $data)
{
    $adid = intval($adid);
    $ip = mysql_real_escape_string($_SERVER['REMOTE_ADDR']);
    execute("INSERT INTO clf_myaddon_post_log (adid, adtype, ip, created)
             VALUES ($adid, '$adtype', '$ip', NOW())");
}

Filter: post_skip_verification

File: post.php | Signature: apply_filters('post_skip_verification', false, $adid, $adtype) Fires after ad insertion and image uploads, before the verification email. Return true to skip the verification email entirely.

ParameterTypeDescription
$skipboolDefault false. Whether to skip email verification.
$adidintThe newly created ad/event ID
$adtypestring"ad" or "event"

Warning

When returning true, you are responsible for also setting verified = '1' on the ad record (via the post_after_insert hook), otherwise the ad will remain in unverified state.

Filter: edit_auth_check

File: userauth.inc.php | Signature: apply_filters('edit_auth_check', $auth, $adid, $isevent) Fires during edit authorization check, after the default cookie-based auth. Return 1 to grant edit access, 0 to deny.

ParameterTypeDescription
$authint1 if authorized by cookie, 0 if not
$adidint|nullThe ad ID being edited
$iseventint|null1 if editing an event, 0 or null for regular ad
add_filter('edit_auth_check', 'myaddon_owner_can_edit', 10);

function myaddon_owner_can_edit($auth, $adid, $isevent)
{
    if ($auth) return $auth;  // Don't override if already authorized
    if (current_user_owns_ad($adid)) return 1;
    return $auth;
}

Filter: header_account_link

File: themes/backpage/templates/parts/header.php | Signature: apply_filters('header_account_link', $default_html) Fires when rendering the account button area in the Backpage theme header. Return HTML to render in the account button area.

Note

This is Backpage-specific. The snet-phoenix theme uses the header_nav_end action hook instead (see Section 8).

Quick Reference — All v6.5.4 Core Hooks

HookTypeFilePurpose
pre_view_routeFilterindex.phpRoute or intercept view requests before the default switch
header_account_linkFilterthemes/backpage/.../header.phpReplace the Backpage account button HTML
edit_auth_checkFilteruserauth.inc.phpGrant or deny edit access beyond cookie auth
post_after_insertActionpost.phpReact to a new ad/event being created
post_skip_verificationFilterpost.phpSkip the verification email for trusted posts