Support
Support
Shared across every PremiumPress product. 13 classes in inc/Support.
Ajax Ajax.php
One guard for the top of an admin-ajax handler: nonce, capability, rate limit. NEW AJAX HANDLERS SHOULD START WITH THIS instead of hand-rolling the same three checks (and their slightly different error shapes and status codes) again: public static function ajaxSave(): void { Ajax::guard(self::NONCE, 'edit_posts', 'nonce', array('bucket' => 'save', 'max' => 30, 'window' => 60)); // …only reached when every check passed… } On failure it ends the request with wp_send_json_error(): 403 {code: 'bad_nonce'} — missing/expired nonce (the page usually just needs a reload) 403 {code: 'forbidden'} — logged-out or lacking the capability 429 {code: 'rate_limited'} — the Throttle bucket is spent for this window Existing handlers have NOT been migrated (deliberately — too many, each with its own messages the front end matches on). Move one over only when you are already in it.
Asset Asset.php
Central resolver for theme asset URLs. PPT ships all assets LOCALLY (theme /assets). We never hard-code a CDN. Every asset URL is built from a single base that defaults to the local /assets folder but can be repointed to a CDN later via the ppt_asset_base filter — one change swaps every asset to the CDN without editing any call site. Versions always come from the LOCAL file's mtime (for cache-busting), even when the URL is served from a CDN.
AuthScreen AuthScreen.php
"Is this request one of the standalone AUTH screens?" The login, lost-password and sign-up (/join/) screens are their own documents: a form column beside an image panel, no site header, no site footer, one job. They still call wp_head()/wp_footer() — they need the theme's styles and scripts — so every floating widget in the theme happily paints itself over them. That is at best noise and at worst a bug. A visitor part-way through signing up has no notifications to read, no messages to answer and nobody to chat to, and the floating bell sits in the bottom-left corner directly over the terms line. It gets WORSE after the account is created mid-wizard: the member is signed in from the password step onwards, so every piece of member chrome switches on for the last few questions of their sign-up. Same shape as Ppt\Support\EditorCanvas, and used by the same floating widgets: one helper, so a new auth screen only has to call mark() and a new widget only has to ask isActive().
CacheGen CacheGen.php
"Has anything changed since?" stamps for caching query results — and a remember() helper built on them. WordPress keeps last_changed stamps for posts and terms (wp_cache_get_last_changed), but they live in the OBJECT cache. Without a persistent object cache that cache dies with the request, so the stamp is a fresh microtime on every page load and any transient keyed on it never hits. This class answers with WordPress's own stamp when a persistent object cache is present, and otherwise with a stamp kept in one small autoloaded option, bumped on the same events WordPress bumps its own: posts — clean_post_cache (every insert/update/trash/delete/status change), post meta writes, and term (re)assignments; terms — clean_term_cache, clean_object_term_cache, term meta writes, term assignments. The option is written at most ONCE per request (at shutdown), however many writes a request makes — a bulk import does not turn into thousands of option updates. Within the request that made a change, get() returns a request-unique stamp, so nothing cached before the change is served afterwards. Hooks are attached by boot(), called from Directory\ListingViews::shared(), which every product line's view recorder runs on every request.
Demo Demo.php
Ppt\Support\Demo — the one place that decides what counts as demo content. Seeded sample content has to be removable EXACTLY: a "go live" wipe that misses a row leaves invented data on a paying customer's site, and one that over-reaches deletes their real imported catalogue. Both used to be possible, because the theme grew several markers independently — ppt-demo (the PremiumPress convention, used by the sample-data tool and generated media), ppt_cp_demo (the coupon demo feed), plus ppt_demo / _ppt_demo recognised by the social-proof emitters. Nothing wrote all of them, so no single query found everything. This class owns {@see META} and marks POSTS AND TERMS alike. The historic keys stay readable through {@see metaKeys()} so an install seeded by an older build is still found; only META is ever written. Terms matter as much as posts. Demo coupons are imported through the ordinary feed pipeline, which creates store terms exactly as a real Awin import does — so a demo store is indistinguishable from a real advertiser unless something marks it at seed time. That is what {@see markTerm()} is for.
DisplayNames DisplayNames.php
Public display names that are never an email address. WordPress sets a new account's display_name to its user_login, and a site whose members sign up with their email as the username (the theme's own sign-up wizard, WooCommerce-style registration, most social-login bridges) ends up with "aptfx7@gmail.com" as the name printed on every listing card, contributor box, review, comment and image alt attribute the theme renders. That is a privacy leak as much as an eyesore: a member's private email becomes public the moment they publish anything. Seen live on a Stock Photo install where the owner's address was on all 12 home-page tiles and every search card. Three layers, so the fix holds wherever the name is read: 1. DATA — user_register rewrites an email-shaped display_name the moment an account is created, and Admin\Migrations::displayNamesNotEmails() does the same once for accounts that already exist. Every $user->display_name property read (270+ across the theme) is then clean without a change. 2. READ — the get_the_author_display_name / the_author filters clean the name on the way out for any account written by something that bypassed (1), e.g. a plugin that sets display_name to the email deliberately. 3. CALL — forUser() for the handful of renderers that want the guarantee inline (the fork ListingCards' author line). The replacement is the best real name on file: first + last name, else a nickname that isn't itself an email, else the part of the address before the @ with dots, dashes and underscores turned into spaces and each word capitalised ("mark.jones" → "Mark Jones", "aptfx7" → "Aptfx7"). Never the whole address.
EditorCanvas EditorCanvas.php
"Is this request a canvas rather than a live browsing session?" Two kinds of canvas, one answer: 1. A PAGE-BUILDER editor. Every builder renders the page it is editing on the FRONT END, inside the editor's preview iframe, so is_admin() is false there and anything hooked to wp_footer paints itself over the editing canvas. 2. An ADMIN-AREA page or an admin-launched PREVIEW. wp-admin itself is covered by is_admin(), but our own previews (Block Explorer / Site Generator block previews, the studio canvas, header/footer and search-layout previews) are front-end URLs shown in an admin iframe, so they need naming explicitly. In both cases floating chrome — the notification bell, message toasts — is not part of the design being edited, covers the thing being worked on, and does not belong. One helper, used by every floating widget, so a new builder or preview route only has to be added here.
Icons Icons.php
Ppt\Support\Icons — the theme's ONE line-icon library. These are the 24x24 stroke glyphs the admin chrome has always drawn (they lived inside Ppt\Admin\Chrome::icon(), which now delegates here). They were lifted out when the SEARCH FILTERS gained a per-filter icon an admin picks in PremiumPress > Search: that picker offers this set, and the front-end search page draws the chosen glyph inside the field — so the same map is now read from both wp-admin and the front end, and a second copy would drift. Everything here is a pure static over a literal array: no WordPress calls, no state, safe to call at any point in a request.
Log Log.php
A tiny debug logger for the places that used to swallow a failure silently. \Ppt\Support\Log::write('bookings.tz', $e, array('listing' => $id)); Writes to the PHP error log ONLY when WP_DEBUG is on, so a production site never grows a log from it and behaviour is otherwise unchanged. Context data is passed through a redactor first: any key that looks like a credential (key, token, secret, password, nonce, cookie, auth…) is replaced with "[redacted]" at any depth, so a caller can hand over a whole request array without leaking what is in it. There is deliberately no global ppt_log() function — the theme has no helpers include, and the class autoloads via inc/autoload.php (Ppt\Support\Log).
PhoneInput PhoneInput.php
International phone input assets — the vendored intl-tel-input widget (flag + dial-code dropdown) plus the theme's init glue. Bundled locally under assets/vendor/intl-tel-input (no CDN). Any screen that shows a phone / WhatsApp number field calls PhoneInput::enqueue() before wp_head; markup opts a field in with data-ppt-tel (or the JS inits it dynamically via window.pptTelInit).
Request Request.php
Request facts that security code depends on — chiefly "who is calling", which is not as obvious as it looks.
TermTree TermTree.php
Hierarchical term lists for the flat controls that offer them. The search page's taxonomy filter used to print every term in one A–Z run, so a category tree like "Scripts & Code → PHP Scripts, Python…" came out as "Plugins, Python, ReactJS, Ruby, Scripts & Code, Themes" — the parent buried among its own children and nothing saying which belonged to what. This puts each parent first with its children indented beneath it. Counts are rolled up the same way: the search query already matches a parent's children (tax_query include_children), so a parent's number has to be its whole branch or "Scripts & Code (0)" would sit above "PHP Scripts (40)". Listings are counted once per branch — one filed under a parent AND its child, or under two siblings, is one listing, not two (the same rule WP core's pad_counts uses). A flat taxonomy (no term with a parent in the list) is left exactly as it was: isTree() is false and callers keep their existing path.
Throttle Throttle.php
A small counting rate limiter, for the endpoints that cost real money or real reputation to run — outbound email, SMS, account creation.