Skip to main content

Directory Theme class reference

27 min read

Software Download Theme — Class reference

The 36 classes that make _so behave differently from the shared Directory core. Generated from the theme source.

← Back to Software Download Theme features

BuyBox BuyBox.php​

Buy / download box for the Software / Digital Download Theme (Ppt_so). One button on the single page that does the right thing via Ppt_so\Download's acquire step: · already own it → re-download (signed link); · free → download now; · paid, not owned → checkout. When the listing has an external/affiliate download URL set, the button links out to that instead (no hosting, no entitlement) — see EXT_URL_META. LOOK: the Stock Photo Theme's "License this asset" box (Ppt_ph\LicenseBox) — an eyebrow + licence-type pill, the headline price, the seller's pricing plans as licence tier rows (Regular / Extended badge, activations, price), the download button, a green terms note and the spec rows. Same .ppt-ph-lic-* classes, styled by the fork's templates/parts/single-sidebar-top.php, so the two pages are one design. Only the look is shared: pricing, cart, ownership, members-only, member prices and delivery are all this fork's own (Checkout / Cart / Purchases / Entitlements / Download).

API
externalUrl()render()sellerCard()
inc/_so/BuyBox.php

Cart Cart.php​

Download-store cart (Ppt_so) — several downloads in one order. The sales page's headline "built-in shopping cart". Until now a buyer paid for one download at a time (/checkout/?buy=``); this lets them collect several and pay once. Ported from the Shop's Ppt\_sp\Cart and trimmed to what a digital product needs: no quantity (you buy a download once — ownership is product-level, see Ppt_so\Purchases), no stock, no shipping, no variants beyond the buyer-selectable pricing plan (Ppt_so\Checkout::plans). Storage, identity and the coupon cookie are the same shape as the Shop's so the two stay recognisably one system: · a dedicated table rather than a PHP session (survives session expiry, plays well with page caching); rows carry only listing + plan + a price snapshot — the live price is re-read from the listing on every request; · a cart belongs to a cart_key: 'u' . user_id when signed in, else a v4 UUID in a long-lived httponly cookie, merged onto the user's cart at login; · a download already OWNED by the signed-in buyer drops out of the cart on read. Checkout is Ppt_so\Checkout (/checkout/?cart=1); one SOFT- order with a line per download, every line granted to the buyer's library on completion.

API
register()enabled()enqueueAssets()headerIcon()table()install()ensureCookie()cartKey()userKey()mergeOnLogin()adopt()items()count()subtotal()isEmpty()has()appliedCoupon()totals()clearCoupon()add()removeRow()clear()clearFor()handlePost()
inc/_so/Cart.php

Checkout Checkout.php​

Software / Digital Download Theme — single-item purchase checkout (Ppt_so). A buyer clicks "Buy / Download" on a priced product; that routes here as /checkout/?buy=

``. This class prices the product (− coupon + tax), shows an order summary + gateway choice, takes payment through the shared gateway machinery, and on success records ownership (Ppt_so\Purchases) so the buyer can download from their account. Also supports paying with the prepaid credit wallet (Ppt\Credits). A sibling to the other forks' checkouts (_ph/_sp/_ct) — same ppt_gateway_start / ppt_gateway_verify contracts, the shared ppt_orders CPT, the /checkout/ + /callback/ virtual pages, and receipt emails. Order refs are prefixed SOFT- so Pages::content() routes callbacks here. Digital goods: no shipping, no address, no stock — many buyers can own the same download. Guest checkout (Settings ▸ Checkout ▸ Guest checkout, ON by default): a signed-out buyer gives a name + email on this page and the account is created — and signed in — as the payment starts (guestSignUp(), the Shop's Ppt_sp\Checkout twin), so the download is theirs the moment payment completes and stays in their library. (Single-item first; a multi-item cart layers on later — the sales page's headline "shopping cart" — reusing this order/grant machinery.)

API
register()buyUrl()cartCheckoutUrl()callbackUrl()isSoftwareRef()basePrice()demoPrice()plans()licenceFor()plan()priceFor()canAccess()isBuyable()hasDeliverable()isDemoSite()coupon()totals()creditCost()canPayWithCredits()handlePay()countryFromRequest()countryFor()orderItems()markPaid()
inc/_so/Checkout.php

Software / Digital Download Theme — the consent boxes a buyer must tick before paying. A UK/EU seller of downloadable content needs two things at the point of payment: the buyer's agreement to the terms, and their EXPRESS request for immediate delivery plus an acknowledgement that, once the download starts, the 14-day right to cancel is lost (Consumer Contracts Regulations 2013, reg. 37). Neither can be a pre-ticked box, and the wording is the seller's own — with links to their terms and privacy pages. So the site owner writes the boxes in Settings ▸ Checkout ▸ Consent (as many as they like, each with its own wording, links allowed, each marked required or optional), and this class: · renders them, UNTICKED, above every "Pay" button on the download store's checkout (a single "Buy now", the cart, the test gateway and the pay-with-credits form each carry their own set, because each is its own form); · refuses to start an order when a required box was not ticked — the browser's required stops the honest case, the server check stops the rest; · records on the order what the buyer agreed to, the exact wording at the time and when — the evidence a dispute asks for — and shows it on the admin's order page. Software theme only: the setting and the render both gate on the profile, so the other products' checkouts are untouched. Wording is filterable (ppt_so_checkout_consent) for a site that wants to drive it from code.

API
register()enabled()rows()allowedTags()kses()suggested()render()verify()record()recorded()
inc/_so/Consent.php

DefaultFields DefaultFields.php​

The Software / Digital Download theme's factory custom fields — the commercial terms a buyer wants settled before they pay, seeded once on a fresh install. The _so fork shipped no default fields at all, so `

Fields::factorySeeds()was empty, the "Reset to defaults" control was hidden (nothing to restore), and a new store's Custom Fields screen read "No custom fields yet" — even though both the editor (Ppt\_so\ListingEditor::applicableFields) and the single page's "Details" accordion panel (inc/_so/templates/single-software.php) were already wired to render them. This class fills that gap; no template work was needed. DELIBERATELY DOES NOT OVERLAP Ppt\_so\SoftwareSpecs. That class owns the nine hard product facts (version, publisher, licence, platform, file size, language, last updated, requirements, website) and renders them in its own "Specifications" panel of the same accordion. Seeding those same facts here would print every one of them twice on the listing page and ask for them twice in the editor. So the split is: SPECIFICATIONS = what the product IS, DETAILS = what the BUYER GETS. DELIBERATELY NICHE-AGNOSTIC, the same constraint as Directory\DefaultFields. The Software theme is one product sold to plugin shops, font foundries, sample-pack labels, ebook sellers and game stores, so every field here has to read sensibly for a WordPress plugin AND a sample pack. That rules out System requirements detail, DAW compatibility or page count however well they suit one showroom — a niche pack can add those through the sameppt_default_fields` filter.

API
register()defaultFields()seeds()definitions()seedFields()
inc/_so/DefaultFields.php

DemoAccounts DemoAccounts.php​

Software Download Theme — what the DEMO member walks into (Account\DemoLogin's plain test member). a developer three apps lent to them (the count is raised here from none; a fresh site creates the rows from the landing design's own app cards — this product ships no curated sample pack — via DemoLogin::createRows()); a seller list prices on their apps (the design's cards mostly carry none, and a "Free" store cannot show a checkout); a customer two purchases granted on other showroom apps, one on a Personal plan and one on a Team plan, each with a licence key, so Wallet ▸ Your downloads and the invoices have rows; a shopper a SECOND developer ("Northwind Labs", a throwaway account) with four priced apps, built from the cards the member's three left on the landing design and then its sibling designs. The wallet's purchases are granted from the END of that pool, so its first two apps stay unowned: Add to cart, License & download, the cart and the checkout can all be walked through (the test gateway pays on a demo site). Reached from the hub's Favourites, which list them; a reviewer a 5-star review on one of their apps from a throwaway member (carrying DemoLogin::META_FLAG, purged with everything else), and three favourites. Undone at purge: the purchases live on the member (gone with them; the per-app sales counters are wound back here), the review is demo-marked and deleted, the reviewer is demo-flagged.

API
register()lendCount()furnish()unfurnish()
inc/_so/DemoAccounts.php

Download Download.php​

Acquire + protected delivery for the Software / Digital Download Theme (Ppt_so). Two steps, so a working file URL can never be shared: 1. ACQUIRE (admin-post ppt_so_get, the "Download" / "Get" button target): · already own the download → straight to delivery (re-download); · free (no price) → record a free purchase (signed-in) and go to delivery; · paid, not yet owned → the checkout (Ppt_so\Checkout) takes payment first. It never streams a file itself — it redirects to a freshly SIGNED delivery URL. 2. DELIVER (a signed ?ppt_so_dl=… link caught on template_redirect): a short-lived HMAC-signed URL bound to the buyer. Before streaming it re-checks the signature, the expiry, and — for a paid download — that the current user is the signed user AND still owns it (Ppt_so\Purchases). The raw attachment URL is never handed out; an expired/leaked link is inert. Anonymous links are minted only for genuinely free downloads. The deliverable is the dedicated "download file" (Ppt_so\ProtectedStore, out of the public tree) when set, else the featured image's original — so there's always something to deliver. See also the external-download-URL affiliate path on the single page, which bypasses this entirely.

API
register()actionUrl()signedUrl()files()fileInfo()count()dailyCounts()demoMessage()isDemoTarget()isSynthetic()isDemoDeadEnd()handle()maybeServe()
inc/_so/Download.php

Entitlements Entitlements.php​

Download-store entitlements — what a membership plan can switch on for the Software / Digital Download Theme (Ppt_so), and the gates that consult them. Core's {@see \Ppt\Account\Entitlements} ships only what core enforces; a fork adds its rows through ppt_entitlements. Every row here names a real chokepoint: so_downloads Download::handle() / serveSigned(), Checkout::canAccess(), Cart::add() / items(), BuyBox — a listing the seller marked "members only" (META_MEMBERS_ONLY) can only be bought / downloaded by a member whose plan carries this row. The page, gallery and description stay visible to everyone ("free tier previews only"). so_analytics ListingViews::ajaxData() + the hub's analytics button. so_listings ListingEditor::handleNew() — new listings a member may create a month (a quota; -1 = unlimited). MEMBER PRICING is a plan-level number rather than a row (a discount is not a bool or a quota): Admin\Memberships stores discount (percent) per plan, and a listing may override with an absolute member price (META_MEMBER_PRICE). memberPrice() resolves the two; Checkout::priceFor() applies it everywhere a price is read for a buyer. THE GATES FAIL OPEN, like the dating fork's: analytics and the listing quota only bite once some ENABLED plan actually says something about them (gated()). The members-only download gate is the exception — a seller who marked a file members-only meant it, so it stays locked even on a site that has not yet configured a plan to sell it (the editor warns in that case).

API
register()rows()gated()membersOnly()canDownloadMembersOnly()joinUrl()memberDiscount()memberPrice()bestDiscount()canUseAnalytics()listingQuotaBlock()consumeListing()membersOnlyListings()renderLibrary()
inc/_so/Entitlements.php

Expiry Expiry.php​

PPT — listing expiry / "Listing lifetime". The Settings ▸ Listings "Listing lifetime" (days) governs how long a listing stays live. 0 = never expires. A value > 0 stamps an expiry timestamp on each listing when it's published; a daily cron then runs the configured "On expiry" action (listings_expiry_action: nothing / draft / pending / trash) once the time is up. The expiry timestamp meta is SHARED with the pricing-plan expiry (PricingPlans::LISTING_EXPIRES_META). A listing that carries a pricing plan is governed by that plan's own duration (set in the editor / at checkout), so the global lifetime only applies to listings WITHOUT a plan. Either way, the cron here enforces whatever expiry timestamp a listing ends up with, and both editors show the time remaining.

API
register()metaKey()schedule()unschedule()onTransition()applyLifetime()runCheck()timestamp()remaining()dateLabel()
inc/_so/Expiry.php

Single-listing gallery styles. The admin picks one of the layouts on the Design ▸ Listings tab (stored in the ppt_design option via Branding), and the single-listing template renders the chosen layout for the listing's images. viewer — thumbnail rail on the left, big stage with arrows + zoom [default] standard — large hero photo + thumbnail strip (click to swap) grid — all photos in a tiled grid (first one featured) carousel — a swipeable/scroll-snap slider with prev/next tall — full-width photos stacked vertically (good for portraits)

API
styles()locked()current()isFullSpan()render()css()
inc/_so/Gallery.php

Geocoder Geocoder.php​

Bulk geocoder — fills in lat/lng for existing listings that have an address but no coordinates (e.g. listings created before the editor's map picker existed), so they get a precise map marker and the "Distance from me" feature. Uses the site's Maps provider (Settings ▸ API keys): Google / Mapbox geocoding APIs (their key), or OpenStreetMap Nominatim (keyless, rate-limited to ~1 req/sec with an identifying UA). A small box on the PPT Listings screen runs it in batches over AJAX. Listings that can't be geocoded are flagged (_ppt_geo_failed) so they aren't retried forever.

API
register()geocode()pendingCount()ajaxBatch()assets()box()
inc/_so/Geocoder.php

LicenseTypes LicenseTypes.php​

Canonical licence types for the Software / Digital Download Theme (Ppt_so). A CLOSED set, the software counterpart of Ppt_ph\LicenseTypes: every pricing plan carries one of these ids, whatever the seller captions the plan ("Pro", "Agency pack"…), so the terms, the badge and the admin Licensing screen all read the real licence model, never a free-text name. The taxonomy (the marketplace model buyers already know from CodeCanyon et al.): free — Free: no charge; a free plan makes the download free. regular — Regular: use in one end product whose end users are not charged. extended — Extended: use in one end product that end users pay for. A licence here is the TERMS the buyer is granted — no licence keys are issued. That is deliberate: the site owner decided against software activation keys, because there is no way for the site to validate them. Extend or relabel via the ppt_so_license_types filter.

API
all()normalise()has()get()label()badge()summary()isFree()choices()allowed()isAllowed()allowedChoices()defaultChoice()
inc/_so/LicenseTypes.php

ListingActions ListingActions.php​

Per-listing visitor actions shown on the single-listing section nav: - Add to favorites — toggles the listing in the member's saved list (user meta ppt_favorites); the account page shows them. Logged-in only; guests are routed to sign-in. - Report — flags the listing to the site admin. The reason is stored as a comment on the listing (type ppt_report, a custom approval status so it stays hidden from the front-end and the normal comment-moderation tabs) and also emailed to the admin. Reports are reviewed in the admin Comments screen's dedicated "Reports" tab. Open to guests and members.

API
register()favIds()favHas()favToggle()favListingIds()ajaxFav()ajaxReport()assets()favButton()reportButton()
inc/_so/ListingActions.php

ListingCard ListingCard.php​

The listing card for the Software / Digital Download Theme (Ppt_so). A download card, drawn after the Deckhaus "Trending downloads" block (user-approved 2026-09-11 as THE software card): preview image with a file-format badge and a save heart, a category eyebrow, the title, the author, star rating · download count, then a divided footer with the price (or "Free") and a Download / Add to cart button. Every _so surface (search results, single-page "related", and the live listing-grid blocks) renders through here, so a download looks the same everywhere. Its classes (.ppt-lc--so, .ppt-lc-so-*) live in assets/css/listing-card.css and are built on the design tokens, so the one card adopts each design's colours, fonts and radius. Normalized shape (adds author/format/downloads/grade to the base shape): name, link, img, cat, rating, desc, desc_long, price, city, dist, featured (bool), highlighted (bool), badge (string), id, author, author_avatar, format, downloads (int|string), grade badge is an optional ribbon label (e.g. "New"); when empty it falls back to "Featured" if featured is set, then to "Members only" for a gated download.

API
fromPost()fromSample()render()author()format()
inc/_so/ListingCard.php

ListingEditor ListingEditor.php​

Listing editor — the shared brain behind the two listing-edit screens: - Admin : PPT-styled screen at ?page=ppt_listings&edit=<id> (Chrome shell), rendered by Admin\Listings when the edit param is present. - Member : front-end screen at /account/listing/<id>/ inside the Member Hub, rendered by Account\MembersPage for a listing the member owns. Both POST to admin-post.php (action ppt_listing_save) and run through the one save() below, so the field set, sanitising and persistence live in a single place. The mode (admin|member) decides which extras are honoured: admins get status, author, slug, featured/verified and the "edit only" custom fields; members get a friendly subset scoped to their own listing. Fields = core (title, description, category, tags, featured image, gallery, FAQ) + every field defined in the Custom Fields admin (Admin\Fields / option ppt_fields), stored as post meta keyed by the field key — which is what the single-listing template already reads.

API
register()newUrl()handleNew()submissionsOpen()memberCanAdd()canEdit()adminEditUrl()memberEditUrl()applicableFields()locationKeys()coordKeys()mapPickerReady()mapConfig()locationFields()socialNetworks()socialValue()socialIcon()hoursDays()hoursValue()normTime()bookingValue()bookingCapacity()bookingBlackout()bookingTz()
inc/_so/ListingEditor.php

ListingFaq ListingFaq.php​

Single-listing FAQ. Reads per-listing FAQ items from the faq post meta (an array of ['q' => …, 'a' => …]). A live listing shows only its own saved FAQ — when it has none the section is omitted rather than filled with fabricated entries. Demo/preview mode supplies sample FAQ via DemoContent so a fresh design isn't empty. Populate a listing's own FAQ by saving faq meta, inject a site-wide default set via the ppt_listing_faq_default filter, or adjust the final list with the ppt_listing_faq filter.

API
items()
inc/_so/ListingFaq.php

ListingHours ListingHours.php​

Business hours display for the single-listing sidebar. Reads the business_hours meta written by the listing editor (ListingEditor::HOURS_META) and renders a Google-Business-style day list with an "Open now / Closed now" badge computed in the site's timezone. Times are formatted with the site's time format.

API
has()isOpenNow()renderSidebar()
inc/_so/ListingHours.php

ListingLocation ListingLocation.php​

Single-listing "Location" section — the richer location block: the full formatted address, an interactive map (ListingMap, precise when the listing has coordinates), a "Get directions" button, and a "Distance from you" control (browser geolocation → straight-line distance to the listing). Self-contained: it prints its own scoped CSS + JS once, so single.php just calls render().

API
address()coords()has()render()
inc/_so/ListingLocation.php

ListingMap ListingMap.php​

The single-listing "Location" map. Uses the site's Maps provider setting (Settings ▸ API keys — the same ppt_maps_provider filter the search map reads). When the listing has stored coordinates (lat/lng, set by the editor's map picker) the map centres on them precisely and drops a named marker; otherwise it falls back to geocoding the address string: google / mapbox(fallback) → a keyless Google Maps embed osm → a Leaflet map (marker from coords, else Nominatim) Map libraries load from their CDNs — the owner-approved front-end map exception.

API
render()
inc/_so/ListingMap.php

ListingPricing ListingPricing.php​

Single-listing pricing plans. A listing owner enters one or more plans / packages on the submission form (a name, a price, a short description, and the licence the plan sells); they're stored in the pricing_plans post meta as a list of ['name' => …, 'price' => float|null, 'desc' => …, 'type' => …]. type is a Ppt_so\LicenseTypes id (free / regular / extended) — the licence terms the plan grants (no licence keys are issued). A plan saved before licences existed reads as the fallback type. The LOWEST priced plan is mirrored into the searchable price meta (Ppt_so\SearchPage::PRICE_META) by the editor on save, so the price range filter, the price sort, and the result-card price all work — that's the link between "enter your pricing" and "find listings by price". Prices are stored in the site's base currency; render() converts + formats via Ppt\Content\Currencies for the viewer's chosen currency.

API
plans()demoPlans()has()lowestPrice()normalise()render()
inc/_so/ListingPricing.php

ListingSections ListingSections.php​

PPT — configurable single-listing sections. The admin (Design ▸ Listings ▸ "Listing page sections") gets a sortable list of on/off toggles for the optional feature blocks a listing page can carry — Booking, Business hours, Maps & Location, FAQ, Reviews. Turning one OFF removes it from BOTH the single listing page and the listing submission/edit form; the order of the list controls the order the (main-column) sections appear in. Config lives in the shared design option (Branding::OPTION = ppt_design) under the listing_sections key: { order:[keys…], enabled:{key:0|1} }. Anything not saved yet defaults to ON, in the catalog order — so existing sites are unchanged until the admin edits the list.

API
supported()catalog()config()order()enabled()orderedAmong()sanitize()
inc/_so/ListingSections.php

ListingTiles ListingTiles.php​

The tile row at the top of the Software listing form: Category. Category used to be a stacked chip-and-button field under the title — a row of chips, a "Choose category" button and a line of hint text, three stacked things for one decision. It now reads as the same square tile the Directory theme uses (Ppt\Directory\ListingTiles) and the Coupon theme before it: icon, label, and either the chips that are set or the word "Select". The tile opens the SHARED category picker modal (assets/js/category-picker.js) — searchable, with a Clear All / Continue footer — so there is still exactly one picker implementation across every fork. Software has no Business hours tile: the Directory row's second tile pulls that theme's hours card into a modal, and an app listing is not open on Tuesdays. The grid is auto-FILL, so a single tile stays tile-sized and left-aligned instead of stretching across the card.

API
categoryTile()assets()
inc/_so/ListingTiles.php

ListingViews ListingViews.php​

PPT listing analytics — records a "hit" every time a single listing page is viewed, and serves that data back to the listing owner as a graph in the member area (/account ▸ My listings ▸ the analytics icon). Each view stores the listing id, the timestamp (UTC), and — when the visitor is logged in — their user id + display name, so an owner can see who has been looking. Bots/crawlers are skipped so counts reflect real visitors. The owner (and admins) can read the analytics for a listing over the AJAX endpoint, which returns a daily series (for the chart), rolling-window totals (7/30/60/90 days) and a list of recent named viewers. Storage: a custom table {prefix}ppt_listing_views (dbDelta on init).

API
register()table()install()maybeRecord()total()countSince()series()recentViewers()ajaxData()
inc/_so/ListingViews.php

Media Media.php​

Listing images — the single, WordPress-native resolver for the PPT image system. A listing's photos are its WP featured image (primary) plus any image attachments parented to it (the gallery). No legacy ?imgid refs, no image-URL meta strings — everything flows through the media library.

API
primary()gallery()videos()media()counts()
inc/_so/Media.php

ProtectedStore ProtectedStore.php​

Protected storage for deliverable files (Ppt_so). Download-slot uploads are written into wp-content/uploads/ppt-protected/ instead of the normal year/month tree, and that folder carries an .htaccess that denies direct web access. The original can therefore only be read from disk by the signed delivery endpoint (Ppt_so\Download, via readfile() on the absolute path) — never fetched by guessing its URL. Apache honours the .htaccess; on nginx the equivalent must be a server location deny for /wp-content/uploads/ppt-protected/ (documented in STOCK-PHOTO-THEME-FEATURE-GAP.md). Files already stored publicly before this landed are not moved retroactively.

API
baseDir()ensure()uploadDirFilter()isProtected()randomFilename()rememberOriginal()displayName()displayNameForFile()
inc/_so/ProtectedStore.php

ProtectedStoreNotice ProtectedStoreNotice.php​

Is the protected-downloads folder REALLY closed to the web? (Ppt_so) Ppt_so\ProtectedStore drops an .htaccess into uploads/ppt-protected/, which only Apache honours. On nginx, LiteSpeed without .htaccess support, or Apache with AllowOverride None, every deliverable sits at a guessable public URL and the sales page's "direct file URL sharing blocked" promise is false — and until now nothing told the site owner. This service finds out the only reliable way: it fetches a harmless probe file from the folder over HTTP and checks whether the server let it through. Exposed → a persistent admin notice with the exact rule to add. Cached for twelve hours; "Re-check now" clears the cache.

API
register()maybeRecheck()status()notice()
inc/_so/ProtectedStoreNotice.php

Purchases Purchases.php​

Purchased downloads for the Software / Digital Download Theme (Ppt_so). When a buyer completes a purchase (Ppt_so\Checkout) — or grabs a free download — the product they now own is recorded here, a per-user ledger in user meta. It answers "does this buyer own this download?" (so the protected file unlocks and the buy button becomes a re-download) and "what has this buyer bought?" (the account library). Ownership is product-level; each row also records the licence type the chosen plan sold (Ppt_so\LicenseTypes), shown as a badge in the library. No licence keys are issued — the site owner's decision, since there's no way to validate them.

API
mine()has()grant()sales()revenue()isFreeRef()licenceOf()count()renderAccountPanel()
inc/_so/Purchases.php

RelatedListings RelatedListings.php​

"Related listings" (Software fork) — thin delegate to the shared Ppt\Directory\RelatedListings, which renders up to four FULL listing cards (the same canonical search-result card) and resolves the fork's own ListingCard (Ppt_so\ListingCard) via Theme::forkClass. Kept as a class so the _so templates' call sites keep working unchanged.

API
items()render()css()
inc/_so/RelatedListings.php

Reviews Reviews.php​

Listing reviews — built on native WordPress comments so they moderate and store like any comment, but stamped with the comment type "review" (the identifier that distinguishes them from ordinary blog comments in the admin) and a 1–5 star rating saved as comment meta. Reviews are always enabled on the listing CPT. Rendered in the single-listing "Reviews" (summary + breakdown + cards) and "Add Review" (star input + comment form) sections.

API
register()open()stampType()saveRating()onCommentChange()onStatusChange()syncPostRating()count()all()stats()stars()renderSummary()renderList()renderForm()
inc/_so/Reviews.php

SearchPage SearchPage.php​

Server-rendered search / archive for the listing post type — the clean PPT rebuild of DT10's PPT\Custom\Search\SearchPage. Runs the normal WordPress main query (WP_Query) via official hooks (pre_get_posts + template_include) so it is SEO-friendly and every core filter applies. Contexts it owns: - keyword search (/?s=…) - the listing post-type archive (/listing/) - listing category / tag archives Filters (GET, all optional, combinable): s keyword tax-listing_category category term id price1 / price2 min / max price (numeric, price meta) sort featured | newest | oldest | rating | price_low | price_high | title paged pagination

API
register()shapeQuery()nearCoords()radiusKm()distanceFrom()distanceLabel()radiusClausesMain()radiusClauses()featuredOrderClauses()isListingContext()postType()isMapTakeover()categoryTax()terms()taxQueryFromRequest()listingTaxonomies()hasActiveFilters()matchingIds()termCounts()priceMetaQuery()ratingMetaQuery()activeMetaQuery()sortOptions()template()
inc/_so/SearchPage.php

SingleListing SingleListing.php​

Single-listing page for the listing_type post type — the clean PPT rebuild of DT10's PPT\Custom\Single\SingleListing. Owns template_include for singular listings and renders its own token-styled template inside the normal main loop. No legacy $CORE / membership plumbing; standard WordPress access rules apply.

API
register()demoBookingProvider()template()related()
inc/_so/SingleListing.php

SoftwareCategories SoftwareCategories.php​

The Software theme's shipped CATEGORIES. Category is the site's main browse axis — the search filter, the /category/ archives, the category tiles on a live homepage, the chip on every card. A software store that installs with an empty category list has nothing to browse until someone types the tree in by hand, and one that installs with demo content used to inherit whatever the borrowed demo rows were filed under (Café, Plumber, Dentist…). So the theme ships its own marketplace tree and seeds it on first run: Scripts & Code → AngularJS, C & C++, C#, CSS, Django, Golang, Java, JavaScript, NodeJS, PHP Scripts, Python, ReactJS, Ruby, VB.NET App Templates · Themes · Plugins · Graphics This does NOT register a taxonomy: categories are the shared listing_category (Ppt\PostTypes\Listings::TAX_CATEGORY) every profile uses. Only the seed set is Software-specific, so it lives in the fork. Modelled on _rt\PropertyCategories. The seed is one-shot AND empty-only: a site that already has categories — one the owner curated, or one carried over from another profile — is left alone. Demo seeding is the one exception (see ensureShipped()).

API
register()defaults()seed()ensureShipped()demoSlugFor()leaves()
inc/_so/SoftwareCategories.php

SoftwareSpecs SoftwareSpecs.php​

Software spec fields for the Software / Digital Download Theme (Ppt_so). The download-specific detail buyers want before they buy — version, publisher / distributor, licence, platform, file size, language, last updated, official site. The analog of the Stock Photo Theme's Ppt_ph\StockMeta. Each field is stored as so_spec_`` post meta. This one class owns the whole feature: the editor card (contributor/admin fills the specs), the save (reads the submitted so_spec[...] array), and the buyer-facing "Details" table on the single page. Empty fields are simply omitted, so a listing only shows what's been filled in.

API
displayEnabled()fields()value()values()has()save()editorCard()display()displayValues()headline()hudChips()downloads()tagLinks()isDemoListing()render()rows()
inc/_so/SoftwareSpecs.php

SoftwareTaxonomies SoftwareTaxonomies.php​

The Software Theme's buyer-terms facets as real TAXONOMIES (user ask 2026-09-11). The eight factory custom fields of Ppt_so\DefaultFields — Delivery, What's included, Licence covers, Commercial use, Updates, Support, Try before you buy, Refund policy — used to be select / checkbox fields stored as plain post meta. A value in post meta can't be searched on, has no page of its own and can't be extended from one place. As taxonomies they get all three for free: - the search sidebar grows one facet per taxonomy, with live counts — Admin\Search::taxonomyFilters() picks up every show_ui listing taxonomy and they are ON by default (a key missing from the saved Search toggles defaults on); - archive URLs (/delivery-method/instant-download/ …); - the site owner edits the vocabulary on the ordinary Listings ▸ <taxonomy> screen. The Custom Fields entries STAY — as type "Taxonomy" pointing at these — so the listing editor, the single page's Details table and the Custom Fields admin all keep working unchanged. Seven of the eight are one-answer questions, so the editor offers them as radios (the ppt_taxonomy_single_pick filter answered here); "What's included" stays multi-pick. Three jobs: - register the taxonomies (init 11 — after Category, so its facet stays first); - seed each one ONCE with the factory option lines (init 12), standing down when the taxonomy already has terms — never fight a curated list; - migrate an existing install ONCE (admin_init): flip the stored field definitions to type "taxonomy" and copy every listing's stored answer into the matching term. The old meta is left in place (nothing reads it any more; kept so the change is reversible from a backup of the field list alone). Software only: registered from inc/_so/, which Theme::boot() only wires under the software profile.

API
register()map()taxFor()isSingle()singlePick()registerTaxonomies()seed()migrate()migrateValues()ensureShipped()fillDemoAnswers()flushOnce()
inc/_so/SoftwareTaxonomies.php

TermMeta TermMeta.php​

Per-term IMAGE + ICON for the listing taxonomies. Adds two custom fields to the native WordPress term add/edit screens (Categories, Tags and any custom listing taxonomy): a media-library image picker and a swatch picker over the theme icon library. The values are stored as term meta and consumed by the live-category blocks (via Blocks\Support\Categories) so an admin can give each category a branded photo and icon instead of the auto-derived listing photo / fallback glyph. Admin-only UI; the read helpers (image()/imageId()/icon()) are safe to call anywhere. Taxonomies are resolved live from the listing post type, so new custom taxonomies get the fields automatically.

API
register()hookTaxonomies()taxonomies()imageId()image()icon()assets()addFields()editFields()save()
inc/_so/TermMeta.php

Uploads Uploads.php​

Listing media uploads — the AJAX backend for the Uppy uploader on the listing editor (replacing the WordPress media frame). Uppy's XHRUpload posts one file per request to ppt_listing_upload; each becomes a normal WordPress attachment (so Media::gallery()/Media::videos() and the single-listing template keep working). Files are left unattached (post_parent = 0) until the listing form is saved, which parents the final set (ListingEditor::save()). A companion ppt_listing_media_remove deletes a freshly uploaded, not-yet- saved attachment when the user removes it in the editor, so abandoned uploads don't pile up in the media library.

API
register()downloadMaxBytes()ajaxChunk()imageMimes()videoMimes()downloadExtMap()ajaxUpload()ajaxRemove()ajaxPoster()ajaxPersist()
inc/_so/Uploads.php