Skip to content

Releases: hozio/hozio-dynamic-tags

v4.21.1: HTML sitemap leaves out redirected categories and tags; Hub upgrade releases its own hold

Choose a tag to compare

@mtallo22 mtallo22 released this 25 Sep 03:06

Pre-release for sandbox testing. The fleet updater reads /releases/latest, which skips pre-releases, so no client site installs this until it is promoted.

Fixes

  • HTML sitemap: categories and tags whose URL the Redirection plugin sends elsewhere are no longer listed. A link is left out when an enabled Redirection rule, exact or regex, redirects every visitor from it (or answers it with a 404 or 410). Rules that apply only to some visitors, and "pass-through" or "do nothing" rules, leave it listed. Category and tag links are now escaped.
    • Sites without Redirection run no extra query. With it, the sitemap makes one table check, one query for regex rules and one indexed lookup for its own links, however many rules exist.
    • A regex rule that does not compile is skipped without a warning, and regex testing stops after a fixed budget, leaving any untested links listed.
    • Regex rules are tested with and without the leading slash, so a rule written as ^old-service-(.*)/ also hides the link it aims at, even though Redirection itself never fires it.
  • Hub rollback of Hozio Pro: after a Hub downgrade placed its 30-day hold on Hozio Pro, a later Hub install of the same or a newer version left the hold in place. That install now releases it (hold_released in the result), and a newer release also releases it on its first run. Only a hold the Hub placed is released this way.

See CHANGELOG.txt for details.

v4.21.0: Update holds, a site-wide update freeze, and wp hozio for managing tools

Choose a tag to compare

@mtallo22 mtallo22 released this 25 Sep 01:35

Pre-release for sandbox testing. The fleet updater reads /releases/latest, which skips pre-releases, so no client site installs this until it is promoted.

Fixes

  • Update holds. A plugin that had been rolled back could be auto-updated straight back to the version that broke the site. A hold now stops every automatic path from updating that plugin: the scheduled run, "Run Plugin Updates Now", the Hub's run_plugin_updates and update_plugin (even with force), WordPress's per-plugin setting, and Hozio Pro's own self-update. Holds last 30 days by default and never more than 180. A "skip" hold blocks only the versions it names.
  • Hub rollback of Hozio Pro: a downgrade now places a 30-day hold instead of switching auto-updates back on.
  • Crash guard: after a run it switches back on only the plugins that run deactivated, and never a held plugin or one that WordPress recovery mode paused. It deletes the maintenance file only if its own run wrote it, and no run starts while maintenance mode is already on.
  • Hub temporary login: it now uses its own account (hozio-hub-support) and never touches any other.
  • Hub update_option: names must be lower case (letters, digits, _ and -), which closes a case and trailing-space bypass of the protected-option list.
  • Settings: the Version Control card no longer reads a value before it is set.

Added

  • Site-wide update freeze (at most 7 days). While it lasts, no automatic plugin, theme, core or Hozio Pro update runs.
  • WP-CLI: wp hozio info, wp hozio log, and wp hozio updates hold|release|holds|freeze|unfreeze|history. Each prints one line of JSON and never prints secrets. Email and IP addresses are redacted from log lines and from the reason, source and reference of holds and the freeze (the heartbeat too). Contract version 1.
  • Hub commands: hold_plugin_updates, release_plugin_updates, freeze_updates and unfreeze_updates. The heartbeat now reports holds and the freeze.
  • Plugins screen: a "managed by Hozio Pro" or "held until" note, a Release hold link, and a warning on the update row of a held plugin. Updating a plugin by hand is never blocked.
  • Settings: an "Update holds and freeze" panel under Feature Toggles.

See CHANGELOG.txt for details.

v4.20.9: Audit and debug logs move out of wp-content into the database

Choose a tag to compare

@mtallo22 mtallo22 released this 25 Sep 00:00

Pre-release for sandbox testing. The fleet updater reads /releases/latest, which skips pre-releases, so no client site installs this until it is promoted.

Security

  • The audit log could be downloaded by anyone. hozio_audit_log() wrote wp-content/hozio-audit.log, and on hosts that serve wp-content files directly anyone who knew the URL could fetch it. It held admin usernames and IDs, the plugin list with versions, failed updates and Hub activity. hozio-debug.log was exposed the same way while debug mode was on.
  • Both logs now live in database tables: {prefix}hozio_audit_log and {prefix}hozio_debug_log (id, created_at in UTC, context, message). Each keeps its newest 5,000 entries, with messages capped at 4,000 bytes. If a table is missing or a write fails, the entry is dropped. It never falls back to a file.
  • On the first request after updating, the newest entries of both old files are imported and the files are deleted. This check runs on every request, so a site that is rolled back and then updated again gets cleaned again.
  • The Hub update_option command refuses hozio_log_* options.

Changed

  • Settings > Debug & Logging: View Log now opens a plain-text viewer for administrators only, checked with a nonce. It no longer opens the public file URL. A View Audit Log button is added.

See CHANGELOG.txt for details.

v4.20.7: Updates install unattended, dev-URL banner can be dismissed

Choose a tag to compare

@mtallo22 mtallo22 released this 22 Sep 19:25

Two fixes, both about the "2f" domains that kept coming back.

Sites were not installing updates on their own. The updater hooked itself only on wp-admin screens, but WordPress installs updates unattended from WP-Cron, where that test is false. During the one run that can actually install something, neither the update check nor the approval filter existed, so the plugin declined its own update - while wp-admin, where the check did run, kept reporting an update was available. Sites left behind on 4.20.5 still carry the old hostname bug, which is why "2f" domains kept being seen after 4.20.6 shipped. The updater now also registers during cron, WP-CLI and REST. An ordinary page view still registers nothing, and the GitHub release is cached for 12 hours.

The red dev-URL banner can now be dismissed for the rest of the browser session, instead of appearing on every page load. It returns if a later scan finds a different number of rows, and the toolbar item always stays. Dismissal is entirely client-side in a session cookie; the server never varies its output on it, so a cached page can never be served to someone else already dismissed.

Verified: 416 checks pass, including the dismiss script executed against a DOM and the updater's hook registration in each context.

v4.20.6: Dev URL fix no longer disconnects Elementor ACF fields

Choose a tag to compare

@mtallo22 mtallo22 released this 14 Sep 20:47

Dev URL Guard: "Fix now" no longer disconnects ACF fields on Elementor sites.

  • Elementor stores dynamic tag settings URL-encoded; the scan read the "2F" of %2F as part of the hostname ("2fsite.mystagingwebsite.com") and Fix removed it, leaving settings Elementor could not decode. Hostnames can no longer start inside an escape sequence, and encoded links keep every escape byte for byte.
  • Rows damaged by an earlier Fix are detected (shown as "Broken by an earlier fix") and repaired when you press Fix now. Nothing is written automatically.
  • Legacy "2f..." hostnames in reports and undo records are cleaned so Undo cannot write them back.
  • Elementor CSS/element cache is cleared after a fix or undo that changed rows.

Verified: read-only dry run on millermarineservices.com (26 rows, 0 dev hosts left, 0 broken tags), 222 new tests plus all existing suites passing.

v4.20.5: Stray-bracket filter no longer corrupts inline JavaScript

Choose a tag to compare

@mtallo22 mtallo22 released this 10 Sep 15:31

The stray-angle-bracket content filter was corrupting inline JavaScript on every page that had any.

What was wrong

hozio_escape_stray_angle_brackets() ran over the whole rendered post with no exemption for <script>, <style> or HTML comments. Anything inside a script matching its > rule was encoded:

In your script What rendered
item => item.id item =&gt; item.id
a->b a-&gt;b
return (x)>y return (x)&gt;y

One escaped character is a SyntaxError, and the whole script dies without a word in the page.

On angeloakspetcrematory.com that killed Gravity Forms' AJAX bootstrap and its iframe response handler: the submission lock never released, so every Continue and Save button showed an endless spinner.

The filter runs at priority 20 — after do_shortcode, and after Elementor injects its builder output at priority 9 — so it saw every script the page produced. That is why the reach was so wide.

What changed

<script>, <style> and HTML comments are exempt and come back byte for byte. Content holding an unclosed <script> or <style> is left completely untouched, because a partial block cannot be located reliably.

The hook, its priority, and the function name are unchanged.

Prose behaviour is verified unchanged, not assumed

The two rules are byte-identical to the previous ones. A differential test runs the shipped function against the original across 40,050 inputs — real markup, prose, entities, shortcodes, multi-byte text and randomised fuzzing — and asserts byte-identical output on every one. 18,052 of those are inputs where the rules actually change something.

Cost on a 290KB Elementor page: +0.97ms. Plain prose is unchanged.

One deliberate divergence from the brief

The exemption matches the blocks as an alternative inside the same pattern, rather than splitting the content first.

Both rules use lookbehind and lookahead. Splitting evaluates them against segment edges instead of the real neighbouring characters, which silently changes prose output next to a block. )><script> is the proof:

  • Old code: leaves the > alone, because a < follows it.
  • A preg_split-based fix: escapes it to )&gt;.
  • This fix: leaves it alone, matching the old code.

That case is now a test.

Also

Guards the original lacked: a non-string (or null) is returned untouched, and a regex-engine failure returns the content rather than the null preg_replace hands back on failure — which the old code passed straight through as the post content, blanking it.

New hozio_angle_bracket_fix_enabled filter turns the whole thing off for a site that needs it off.

Testing

156 assertions across five suites plus 19 environment scenarios, all passing; 35 files lint clean on PHP 8.2.

v4.20.4: Toolbar drop-downs no longer open behind the banner

Choose a tag to compare

@mtallo22 mtallo22 released this 09 Sep 21:49

WordPress toolbar drop-down menus opened behind the warning banner.

What was wrong

The banner sat at the very top of the stacking order — z-index: 2147483640, the maximum a browser accepts. WordPress puts #wpadminbar at 99999, and the toolbar's drop-down menus are children of the toolbar, so they share that stacking context.

The banner therefore outranked every toolbar menu. The bar itself was clear of the toolbar after 4.20.2, but the moment a menu dropped past it, that menu was painted underneath the banner and could not be clicked.

What changed

All three banners now sit just below the toolbar:

Banner z-index
Staging index 99990
Dev URLs 99989
robots.txt 99988

High enough to clear ordinary page content, low enough that the toolbar and everything it owns always wins.

The same fault would have covered wp-admin's own modal windows, which sit at 160000 — the media library picker among them. Fixed by the same change.

Testing

The render test now asserts the stacking order directly: the banner has a z-index, it is below 99999 so toolbar menus stay on top, and it is above ordinary page content. 128 assertions across four suites plus 19 environment scenarios, all passing; 35 files lint clean.

v4.20.3: Single-pass scan, and filenames are not hostnames

Choose a tag to compare

@mtallo22 mtallo22 released this 09 Sep 19:04

Two faults, found together on a live site whose only matches were four uploaded screenshots.

A filename is not a hostname

screencapture-2-hoziodev-2024-11-13.jpg reads as a hostname — labels of letters, digits and hyphens, ending in .jpg. The repair treated it as a link and would have replaced the filename with the live domain, orphaning the image.

A build pattern must now be a whole label of the name, which is what a real build host always is (18.hoziodev.com, client.mystagingwebsite.com). A pattern buried inside a single label, as it is in a filename, is no longer a hostname.

This was caught by the regression test written for the report, not by inspection.

The count said work was needed when none was

Every scan made three passes over each table — a COUNT, a 200-row sample for hostnames, then a full evaluation walk. On a large site that overran the time budget, and an unfinished pass fell back to assuming every row it had found was fixable.

The visible result was a panel that contradicted itself:

4 database rows still point at a dev domain
For information only — 0 rows mention a dev domain but are not links…
(followed by four listed rows)

…with a Fix button that ran for a long time and changed nothing, because none of the four could be rewritten.

The scan is now a single pass. It counts each row, decides whether the repair could rewrite it, and collects hostnames — all in one walk. Roughly a third of the database work, and the counts are exact.

Related

  • An unfinished pass no longer invents work. It reports only what it examined, says plainly on the settings screen that it stopped early, and leaves the banner keyed to what is known rather than assumed.
  • The time budget adapts to the site's own PHP execution limit instead of a fixed 20 seconds, with headroom so a long pass never dies mid-request.
  • A row left alone because the dev name sits in an uploaded file's name now says exactly that, instead of the generic "not a web address".

Testing

125 assertions across four suites plus 19 environment scenarios, all passing; 35 files lint clean. The new fixture reproduces the reported site exactly — four wp_postmeta rows whose values are upload paths containing a dev name — and asserts none is counted as fixable, all four are listed with a filename-specific reason, no hostname is claimed from them, and the banner stays off.

v4.20.2: Banners no longer cover the WordPress toolbar

Choose a tag to compare

@mtallo22 mtallo22 released this 09 Sep 14:43

The red warning banner covered the WordPress toolbar on the front end.

What was wrong

All three banners — staging index, dev URLs and robots.txt — sat on top of the admin bar for every logged-in administrator, instead of below it.

The offset was written as a stylesheet rule:

.admin-bar #hozio-sig-bar { top: 32px }

…while the element itself carried top:0 in its own inline style attribute. An inline declaration beats any stylesheet rule that is not marked !important, so the offset could never apply and the bar was pinned to the top of the window. Present since the first banner shipped in 4.17.0.

What changed

Placement now lives entirely in the stylesheet — the inline style sets position:fixed and nothing else.

The offset is worked out in PHP from is_admin_bar_showing() rather than from the admin-bar body class, which a theme that forgets body_class() never adds. So:

  • Toolbar showing → the bar starts below it (32px, or 46px below 782px wide, matching WordPress' own bump).
  • An administrator who has switched the toolbar off in their profile → no offset at all, and no pointless media query.

Testing

A render test runs the real banner and inspects the output: the inline style sets no top, left or right; the stylesheet carries the correct offset in both toolbar states; and nothing anywhere in the output pins the bar to the top of the window while the toolbar is present. 13 assertions across the two states, plus the existing 105 assertions and 19 environment scenarios — all passing, 35 files lint clean.

v4.20.1: A live site is no longer detected as staging after a transfer

Choose a tag to compare

@mtallo22 mtallo22 released this 04 Sep 13:29

Fixes a live site being detected as staging after a staging-to-live transfer.

What was wrong

WP_ENVIRONMENT_TYPE — which Pressable sets per site — travels with a staging-to-live transfer, in both the files and the database. A genuinely live site therefore kept claiming to be staging, and the guard believed it over the site's own address.

That is the dangerous direction. A site treated as staging is offered a Disallow: / robots.txt and a "discourage search engines" switch — the two things a live site must never have. Seen on two live client sites, both of which were presented with a "Fix both now" button that would have deindexed them.

No damage occurred on either: both still serve a correct robots.txt and send index, follow.

What changed

The site address decides. Every build environment in this fleet is identifiable from its address, and an address cannot lie about itself. A live domain is production even when WP_ENVIRONMENT_TYPE disagrees.

The manual override still wins over everything, which is how a dev site on a custom domain is handled.

The settings screen now shows why. A new "Detected because" line names the address that matched, or the override in force — so a surprising verdict is never unexplained again. Where WP_ENVIRONMENT_TYPE contradicts a live address, the panel says so and points at wp-config.php, since the stale value is worth clearing at the source.

Also

Spacing under the Save Settings button, which sat too close to the Live robots.txt section below it.

Testing

Two new regression scenarios: a live domain whose WP_ENVIRONMENT_TYPE still says staging must be production and must flag the contradiction; a real staging address must stay staging regardless. 105 assertions across four suites plus 15 environment scenarios, all passing; 35 files lint clean.