@prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/portfolio/lh-block-and-shortcode-auditor/>
  a sioc:Post ;
  dc:title "Architecture" ;
  dcterms:identifier 147631 ;
  dc:modified "2026-10-16T09:22:46Z"^^xsd:dateTime ;
  dc:created "2026-09-16T22:49:26Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-block-and-shortcode-auditor/architecture/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded """<ul class="lh_portfolio-meta"><li><strong>Part of:</strong> <a href="https://lhero.org/portfolio/lh-block-and-shortcode-auditor/">LH Block and Shortcode Auditor</a></li></ul>
<p class="wp-block-paragraph">LH Block and Shortcode Auditor is network-activated and multisite-only. A site&#8217;s block and shortcode registries only exist inside that site&#8217;s own request, and <code>switch_to_blog()</code> does not load another site&#8217;s plugins or theme, so every site captures and scans itself and writes into two central tables on the base prefix. Network and MCP reporting read those tables.</p>



<h2 class="wp-block-heading">Components</h2>



<ul class="wp-block-list"><li><strong>Storage</strong> (<code>class-lh-block-and-shortcode-auditor-storage.php</code>): table install via <code>dbDelta</code> gated on a network schema version option, and all reads and writes.</li><li><strong>Registry capture</strong> (<code>…-registry.php</code>): at <code>shutdown</code> on a front-end request, when the site&#8217;s registry signature (active plugins, network plugins, theme, plugin version) has changed or a capture was requested, records registered blocks and shortcodes with a provider guess and rebuilds the site&#8217;s summary. <code>refresh_counts()</code> rebuilds the summary from the last captured registry plus current usage, so it is safe from admin and cron.</li><li><strong>Scanner</strong> (<code>…-scanner.php</code>): content sweeps in batches of 100 posts, single-post rescans on save and delete, and widget scans. A forced sweep runs whenever the scanner version changes (currently 5). Each sweep carries a random token passed to every batch job; a batch whose token is no longer current exits, so a forced re-sweep never races an older chain.</li><li><strong>Queue</strong> (<code>…-queue.php</code>): every background job goes through this class, which uses Action Scheduler when it is loaded and falls back to WP-Cron. No hard dependency.</li><li><strong>Admin screens</strong> (<code>…-site-admin.php</code>, <code>…-network-admin.php</code> and three list tables).</li><li><strong>MCP abilities</strong> (<code>…-abilities.php</code> registrar plus one class per ability). The registrar&#8217;s <code>describe_object()</code> builds titles and edit and view links for both the abilities and the network screen.</li></ul>



<h2 class="wp-block-heading">Detection</h2>



<ul class="wp-block-list"><li><strong>Blocks:</strong> recursive <code>parse_blocks()</code> walk, counting nested blocks.</li><li><strong>Shortcodes:</strong> core&#8217;s shortcode regex with a generic lowercase tag pattern, after stripping HTML comments, <code>pre</code> and <code>code</code>. Escaped <code>[[tags]]</code>, single-character tags, markdown <code>[text](url)</code> links and print_r style <code>[key] =&gt;</code> output (plain or entity-escaped) are ignored. A bare single word counts only if it is registered, has <code>key=value</code> attributes, or is self-closing or enclosing; tags containing <code>_</code> or <code>-</code> always count.</li><li><strong>Providers:</strong> block asset URLs first, then the render or shortcode callback&#8217;s file via reflection, then the block namespace. Values are <code>plugin:slug</code>, <code>mu-plugin:slug</code>, <code>theme:slug</code>, <code>core</code>, <code>namespace:name</code> or empty.</li></ul>



<h2 class="wp-block-heading">Scope</h2>



<ul class="wp-block-list"><li>All post types except core system types and machine-written types whose content is not editable in its own right: <code>lh_wpn_message</code> push notification logs, and ActivityPub <code>ap_outbox</code> items (stored copies of outgoing activities, which would count the original post&#8217;s tags twice). ActivityPub profile extra fields (<code>ap_extrafield</code>, <code>ap_extrafield_blog</code>) are real user content and are scanned. Templates, template parts and patterns are scanned only where stored in the database.</li><li>All statuses except <code>auto-draft</code>, <code>inherit</code> and <code>trash</code>, read late so custom statuses are included.</li><li>Block, text and custom HTML widgets.</li><li>Archived, spam and deleted sites are not scanned. Theme template files, BuddyPress and bbPress content, comments, postmeta and JavaScript-only blocks are out of scope.</li></ul>



<h2 class="wp-block-heading">Concurrency and gotchas</h2>



<ul class="wp-block-list"><li>Summary rewrites run under a MySQL <code>GET_LOCK</code> per site. <code>wp_cache_add</code> was not atomic on this cache backend and caused duplicate-key errors when a version bump made every site capture at once.</li><li>A signature-triggered capture re-reads the stored signature under the lock and skips if another request has just captured.</li><li>The batch chain is queued as non-unique async jobs: an Action Scheduler unique job would block its own continuation.</li><li>A batch can die without finishing (the host killing the PHP process leaves the job in progress with no error), and since each batch queues the next only when it ends, the sweep would never complete. Before 0.1.17 this abandoned about one Princes Park Touch daily sweep in three. The watchdog job resumes a sweep from its recorded progress when no batch has finished for 15 minutes. Batches also suspend object cache additions while scanning, since Action Scheduler does not flush the in-memory cache between jobs when a persistent object cache is in use.</li><li>The sweep token, sweep start and finish times and progress are read with the option cache cleared, since an Action Scheduler runner processes many jobs in one request and would otherwise keep a stale copy. The token is checked before a batch scans and again before it finishes or queues the next batch.</li><li>From network admin, rescans are requested rather than run: the site&#8217;s scanner version option is cleared and a cache-busted loopback makes the site sweep itself in its own context.</li><li>When the auditor reports a tag on an item that cannot be edited, check the post type first: it is usually a machine-written copy that should be added to the excluded post types, as happened with <code>ap_outbox</code> in 0.1.14.</li></ul>
"""^^rdf:XMLLiteral ;
  sioc:content """Part of: LH Block and Shortcode Auditor
LH Block and Shortcode Auditor is network-activated and multisite-only. A site&#8217;s block and shortcode registries only exist inside that site&#8217;s own request, and switch_to_blog() does not load another site&#8217;s plugins or theme, so every site captures and scans itself and writes into two central tables on the base prefix. Network and MCP reporting read those tables.



Components



Storage (class-lh-block-and-shortcode-auditor-storage.php): table install via dbDelta gated on a network schema version option, and all reads and writes.Registry capture (…-registry.php): at shutdown on a front-end request, when the site&#8217;s registry signature (active plugins, network plugins, theme, plugin version) has changed or a capture was requested, records registered blocks and shortcodes with a provider guess and rebuilds the site&#8217;s summary. refresh_counts() rebuilds the summary from the last captured registry plus current usage, so it is safe from admin and cron.Scanner (…-scanner.php): content sweeps in batches of 100 posts, single-post rescans on save and delete, and widget scans. A forced sweep runs whenever the scanner version changes (currently 5). Each sweep carries a random token passed to every batch job; a batch whose token is no longer current exits, so a forced re-sweep never races an older chain.Queue (…-queue.php): every background job goes through this class, which uses Action Scheduler when it is loaded and falls back to WP-Cron. No hard dependency.Admin screens (…-site-admin.php, …-network-admin.php and three list tables).MCP abilities (…-abilities.php registrar plus one class per ability). The registrar&#8217;s describe_object() builds titles and edit and view links for both the abilities and the network screen.



Detection



Blocks: recursive parse_blocks() walk, counting nested blocks.Shortcodes: core&#8217;s shortcode regex with a generic lowercase tag pattern, after stripping HTML comments, pre and code. Escaped [[tags]], single-character tags, markdown [text](url) links and print_r style [key] =&gt; output (plain or entity-escaped) are ignored. A bare single word counts only if it is registered, has key=value attributes, or is self-closing or enclosing; tags containing _ or - always count.Providers: block asset URLs first, then the render or shortcode callback&#8217;s file via reflection, then the block namespace. Values are plugin:slug, mu-plugin:slug, theme:slug, core, namespace:name or empty.



Scope



All post types except core system types and machine-written types whose content is not editable in its own right: lh_wpn_message push notification logs, and ActivityPub ap_outbox items (stored copies of outgoing activities, which would count the original post&#8217;s tags twice). ActivityPub profile extra fields (ap_extrafield, ap_extrafield_blog) are real user content and are scanned. Templates, template parts and patterns are scanned only where stored in the database.All statuses except auto-draft, inherit and trash, read late so custom statuses are included.Block, text and custom HTML widgets.Archived, spam and deleted sites are not scanned. Theme template files, BuddyPress and bbPress content, comments, postmeta and JavaScript-only blocks are out of scope.



Concurrency and gotchas



Summary rewrites run under a MySQL GET_LOCK per site. wp_cache_add was not atomic on this cache backend and caused duplicate-key errors when a version bump made every site capture at once.A signature-triggered capture re-reads the stored signature under the lock and skips if another request has just captured.The batch chain is queued as non-unique async jobs: an Action Scheduler unique job would block its own continuation.A batch can die without finishing (the host killing the PHP process leaves the job in progress with no error), and since each batch queues the next only when it ends, the sweep would never complete. Before 0.1.17 this abandoned about one Princes Park Touch daily sweep in three. The watchdog job resumes a sweep from its recorded progress when no batch has finished for 15 minutes. Batches also suspend object cache additions while scanning, since Action Scheduler does not flush the in-memory cache between jobs when a persistent object cache is in use.The sweep token, sweep start and finish times and progress are read with the option cache cleared, since an Action Scheduler runner processes many jobs in one request and would otherwise keep a stale copy. The token is checked before a batch scans and again before it finishes or queues the next batch.From network admin, rescans are requested rather than run: the site&#8217;s scanner version option is cleared and a cache-busted loopback makes the site sweep itself in its own context.When the auditor reports a tag on an item that cannot be edited, check the post type first: it is usually a machine-written copy that should be added to the excluded post types, as happened with ap_outbox in 0.1.14.
""" ;
  sioc:topic <https://lhero.org/?taxonomy=author&term=cap-1> .

<https://lhero.org/author/1/#account> rdfs:seeAlso <https://lhero.org/author/1/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
