Releases: ultrapackadm/deckwp-connect
Release list
v0.41.0
Two places where the connector knew more than it told the dashboard. In
both, the operator-facing result was wrong in a way that pointed at the
wrong culprit — a theme that "wasn't premium" and a permissions problem
that wasn't a permissions problem.
Fixed
-
A theme installed under a folder name that differs from its catalog
slug lost its premium identity. The theme result echoed back the
slug it was asked for and discarded the stylesheet directory it had
just resolved. Citadela ships a package that extracts to
citadela-themewhile the catalog calls itcitadela, so the
dashboard could not match its own install against the request: the
next heartbeat saw an unknown directory, created a fresh row for it
with the default wp.org source, and the item's premium classification
— along with its update channel — was gone. Every theme result path
now carriesinstalled_slug, and it is present even when the two
names agree, so its absence means one thing only: an older connector. -
A failed install or update was reported as a filesystem-permissions
problem regardless of what actually went wrong. When WordPress's
upgrader returns a non-truthy result without aWP_Error, the
connector used to state, flatly, that this is "usually a
filesystem-permissions issue." The most common real cause is the
opposite: a clean download that WordPress then refuses to unpack —
a corrupt or truncated archive, or a package whose structure the
upgrader rejects. Sending the operator tochmodfor that is a dead
end. The connector now asks the upgrader skin first — for a
WP_Errorit raised but did not return, then for the last upgrade
message (markup stripped, since this lands in an operator-facing
field) — and only falls back to a hint when the skin has nothing to
say. That hint now names both possibilities instead of asserting one.
Internal
- The repository gained a CI workflow that runs the test suite on PHP
7.4 (the compatibility floor) and 8.3 (production) on every push, so
a release can no longer be cut from a red tree. New coverage for the
active-plugin upgrade path, which reactivates a plugin WordPress
switched off mid-upgrade.
v0.40.0
An end-to-end pass — create an account, pair a site, update plugins and
themes, roll one back, scan, disconnect — turned up a set of problems
that shared one root: the connector kept telling the dashboard what an
operation was supposed to do instead of what WordPress actually did.
This release replaces that inference with read-back facts, and turns on
two schedulers that had been shipping switched off.
Fixed
-
The scheduled heartbeat and the scheduled scan never ran. On any
site. Both were gated behind constants (DECKWP_CONNECT_ENABLE_ HEARTBEAT,DECKWP_CONNECT_ENABLE_SCAN) that defaulted to OFF while
the dashboard's ingest endpoints were still being built. The
endpoints went live long ago; nothing ever defined the constants. The
practical effect was that a paired site reported its inventory once —
or never — and then went quiet:last_seen_atnever advanced, plugin
lists went stale the moment anything changed on the site, and the
only fresh data the dashboard ever saw came from an operator pressing
Refresh by hand. Both constants are now opt-out, honoured only
when defined and falsy:define( 'DECKWP_CONNECT_ENABLE_HEARTBEAT', false ); define( 'DECKWP_CONNECT_ENABLE_SCAN', false ); -
A freshly paired site showed up on the dashboard with zero plugins
and zero themes. The handshake ended at the credential write and
left everything else to a cron tick that might be five minutes out —
or, per the item above, never come. Pairing now finishes the job:
Pairing\Setupschedules both cron events and pushes the first
inventory before the pairing screen answers. If that first push
can't be delivered the pairing still succeeds, and the settings page
says why rather than leaving the operator staring at an empty site. -
A rollback left the dashboard showing the version it had just
rolled back from.restore-backupanswered a bare{"ok": true},
so the dashboard had nothing to go on but the backup row it had asked
us to restore. The response now carries the post-state read back off
the install —installed,version,active— after the folder
swap and after WordPress's plugin cache is invalidated, which is what
made the old read return pre-swap data. -
A plugin could come back from a restore or a failed update switched
off, while the panel went on calling it Active. Core's upgrader
deactivates a plugin before replacing its files and leaves
re-activation to an admin page load that never happens for us. The
connector now restores the activation state itself — on the success
path, on the error path, and again after a rollback — preserving
network-wide activation as network-wide. When re-activation genuinely
fails, the response says so inreactivation_errorinstead of
reporting a clean success over a plugin that is no longer running. -
The security scan reported three findings on a brand-new WordPress,
and all three were wrong. Each came from treating a textual match
as a behavioural one:- the two-line "Silence is golden"
index.phpstub — written by
WordPress core, WooCommerce, most hosts, and by DeckWP's own backup
directory — was reported as a critical webshell. A PHP file with no
executable statement in it is now recognised by parsing it, not by
keeping a list of filenames to forgive. - the scanner's own source file was flagged, because its
documentation spelled out one of the signatures it looks for. Code
inside a comment does not run, and no longer counts as evidence.
(Every security plugin that ships a signature list had the same
problem.) wp-config.phpwas reported world-writable at 0666 on Windows,
where every writable file reports 0666 because NTFS has no POSIX
mode bits. That check now runs only where the answer means
something.
None of this is fixed by exempting DeckWP's own files — the
connector's directory is still scanned like any other plugin's,
because "the attacker hid it in the security plugin's folder" is a
real thing that happens. - the two-line "Silence is golden"
-
"All up to date" was also what the dashboard said when it hadn't
been able to ask. When wordpress.org was unreachable, WP's update
transient came back with an empty response and the panel rendered it
as good news. Both the heartbeat and the inventory pull now carry an
update_checkblock — whether a poll went out, whether it answered,
the reason if it failed, and the transient's own timestamp — so an
unanswered question can be told apart from an answer of "nothing". -
Disconnecting from the WordPress side left the cron events
running, and disconnecting from the dashboard side didn't tell the
site at all: its settings page kept saying "Connected" until a
heartbeat came back 401. There is now onePairing\Teardownbehind
all four paths — the site's own Disconnect button, the dashboard's
newPOST /deckwp/v1/unpairpush, and the 401 fallbacks in both
schedulers — so every route out leaves the site in the same state,
cron included.
Added
-
wporg_slugon every plugin and theme row. WordPress identifies
a plugin by a path, which is not the same thing as its
wordpress.org slug:hello.phpishellolocally andhello-dolly
in the directory. Anything building a download URL from the local
slug 404s — and since Hello Dolly ships with every WordPress install,
every paired site carried at least one item whose scan could only
ever fail. The connector now reports the slug WordPress itself
matched the item to, harvested from both buckets of the update
transient (responseandno_update, so an up-to-date plugin is
covered too), and reportsnullwhen wordpress.org has no such item
— which is a real answer, not a missing one. -
POST /deckwp/v1/unpair— HMAC-signed, so the dashboard can
clear a site's credentials at the moment the operator clicks
Disconnect rather than waiting for the site to discover it. -
activeon everyinstall-batchresult row, read back from
WordPress after the operation rather than echoed from what the
operation intended.
Changed
Heartbeat\SchedulerandScan\Schedulertake an optional
Teardowncollaborator;Pairing\Handlertakes an optionalSetup.
All are defaulted, so existing construction sites keep working.Pairing\Handler::pair()gained afirst_heartbeatkey in its
result array, reporting whether the first inventory push landed.
v0.39.1
Fixed
-
PHP 8.4 deprecation notices on every plugin boot. 58 constructor
parameters across 24 classes were written asFoo $bar = null, which
PHP 8.4 deprecates ("Implicitly marking parameter as nullable is
deprecated") and PHP 9 will reject outright. On a host running 8.4
withWP_DEBUG_DISPLAYon, these notices printed on every request
that booted the connector — including REST responses, where leading
output corrupts the JSON body. All 58 are now explicitly?Foo $bar = null.No behaviour changes:
?Typehas been valid since PHP 7.1 and the
plugin's floor is 7.4, so this does not move the compatibility floor.
Verified against a live PHP 8.4.19 install — 21 notices before, zero
after — with the HMAC channel re-tested end to end afterwards.
v0.39.0
Added
- License protection. New
License\LicenseDetectorreports, per
installed plugin/theme, whether the site carries an ACTIVE official
license (license_state=licensed_active|unknown, plus a
license_providerlabel). Signals: an update in the WP transient
whose package is neither wp.org nor DeckWP (a third-party author
updater), a customUpdate URI:header off wp.org, and known
frameworks (EDD<slug>_license_status, Freemiusfs_accounts),
extensible via thedeckwp_detect_active_licensefilter. Fail-open:
only flags when a signal is confident. - The inventory / heartbeat payload now carries
license_state+
license_providerper plugin and theme (consumed by the dashboard to
skip auto-updates on licensed items and require an explicit override
on manual ones). - Install-time safeguard.
Installerrefuses to overwrite a plugin
or theme that carries an active official license with a catalog build
(download_urlpresent) unless the request includes
license_override: true. Uses framework signals only (the update
transient is circular at the point of application). Final line of
defense behind the dashboard gate.
Testing
- First unit-test harness for the connector:
tests/bootstrap.php
(minimal WP stubs) +phpunit.xml.dist+LicenseDetectorTest
(11 cases).
Compatibility
- Purely additive — extra payload fields (ignored by older dashboards)
and a new refusal path that only triggers for licensed items without
an override. No change to existing endpoints or the wire contract.
v0.38.0
Added
-
Async off-site upload endpoint. New
POST /deckwp/v1/backup-offsite-uploadships an already-written local
backup zip to a pre-signed PUT URL (Backblaze B2), given
{ local_path, url, headers?, key? }. It resolveslocal_path
relative to the uploads basedir and streams the zip via
BackupManager::uploadOffsite()(containment-checked to the managed
backups directory).This is the off-site path for pre-update backups: those are taken
insideinstall-batch(the critical upgrade path), so rather than
extend that request with a potentially-large B2 upload — and risk a
timeout mid-upgrade — the dashboard snapshots locally during the
upgrade as before, then calls this route from a queued job afterwards.
Egress latency is fully decoupled from the upgrade. Works for any
existing local backup, not just pre-update ones.
Compatibility
- Purely additive — a new route, no change to existing endpoints.
Requires cURL (already required).
v0.37.0
Added
-
Off-site restore fallback.
POST /deckwp/v1/restore-backupnow
accepts an optionaldownload_url— a short-lived pre-signed GET url
for the backup's off-site (Backblaze B2) copy. It's used only when
the local zip atlocal_pathis missing on this server (disaster
recovery: the site was rebuilt, the uploads dir was wiped, the server
was migrated). When the local zip is present, restore uses it exactly
as before — no egress, no behaviour change.When the fallback kicks in, the connector streams the off-site copy
into the managed backups directory (viaBackupManager::downloadOffsite())
and then validates + extracts it through the normal restore path,
including the SHA-256 checksum guard. The download streams to a temp
file and renames atomically into place, so an interrupted fetch never
leaves a half-written zip for restore to choke on. The target path is
containment-checked to the managed directory.
Compatibility
- Fully backwards-compatible. A dashboard that omits
download_url(or
any older dashboard) gets identical local-only restore behaviour.
Requires cURL (already required); pairs with the dashboard sending a
download_urlonly for backups that have an off-site copy.
v0.36.0
Added
-
Off-site backup upload (opt-in).
POST /deckwp/v1/backup-create
now accepts an optionaloffsitedescriptor — a short-lived
pre-signed PUT url, the signed headers to send with it, and the
target object key. When present, the connector streams the
just-written backup zip straight to remote object storage
(Backblaze B2) right after taking the local snapshot, and reports the
outcome back to the dashboard underbackup.offsite(okplus the
storedkey, orok:falsewith an error code).The local snapshot remains the source of truth and is unchanged: the
off-site copy is purely additive, enabled per-team on the dashboard,
and best-effort — a failed or rejected upload is reported but
never turns a locally-successful backup into an error.The upload streams from disk via cURL with an explicit content length
(never chunked, which S3/B2 pre-signed PUTs reject) so even a large
plugin/theme zip never has to sit in PHP memory. The file is
containment-checked to live inside the managed backups directory
before upload, so a malformed request can't exfiltrate an arbitrary
server file.
Compatibility
- Fully backwards-compatible. A dashboard that omits
offsite(or any
older dashboard) gets the exact same local-only behaviour as before —
nobackup.offsiteblock is returned. Requires PHP 7.4+ and cURL
(already required); when cURL is unavailable the upload is skipped
with anoffsite_no_curlcode and the local backup still succeeds.
v0.35.0
Added
-
Fallback transport for sites that block the REST API. Some hosts
and "harden WordPress" security plugins firewall/wp-jsonentirely,
which previously left a paired site unreachable from your DeckWP
dashboard even though the connector was installed and healthy.DeckWP Connect now accepts the dashboard's management commands over a
second, REST-independent path: an HMAC-signed request to a normal
front-end URL, caught early on theinithook and routed to the same
command handlers as the REST API. If/wp-jsonworks, nothing
changes; the fallback only matters when it's blocked.Security and privacy are unchanged from the REST surface: every
command is HMAC-SHA256 verified with the same 60-second anti-replay
window, the requested action is cryptographically bound to the
signature (it can't be tampered into a different command), and the
connector still does nothing on ordinary visitor page loads — it only
acts when your dashboard sends a signed request.
Compatibility
- No configuration needed. WordPress 5.2+ / PHP 7.4+ unchanged.
v0.33.0
Added
-
Theme post-update smoke check — pairs with the theme backup
pipeline shipped in v0.32.0 to enable auto-rollback when a theme
upgrade leaves the site in a broken state.The new
PostUpdateChecker::verifyTheme()mirrors the plugin
smoke shape with theme-specific verification points:- Theme folder exists after upgrade (themes are always
folder-based, no single-file equivalent to plugin layouts
like Hello Dolly). style.cssexists — WP refuses to load a theme without
its main metadata file.index.phpexists — bare-minimum template required by
WP for any active theme.functions.phpPHP token parse when present (optional
file, but a syntax error there breaks the whole site since
it loads on every page).- Active-state survived when the theme was active before
the upgrade — catches the rare case where the upgrade
payload internally renamed the slug or shipped a different
theme's content under the same folder name.
An optional home-page 5xx probe (
smoke_check_home: trueon
the install-batch payload) extends coverage to "the site as a
whole is up" the same way the plugin path does. - Theme folder exists after upgrade (themes are always
Changed
-
Installer::handleSmokeFailure()is now kind-aware
('plugin' | 'theme') and dispatches the rollback to the
matchingBackupManagermethod (restorevsrestoreTheme).
The error-envelope shape, snapshot-resolution logic, and
rolled_back response payload are identical between kinds. -
Installer::installOneTheme()captures the active-theme state
viaget_stylesheet()before runningTheme_Upgrader::upgrade()
so the smoke check has the$wasActivesignal it needs. Mirrors
the plugin path'sisPluginActive()capture.
v0.32.0
Added
-
Theme backup pipeline — pre-update theme backups now work the
same as plugins. The dashboard sendstype: themeon the wire and
the connector materialises a zip ofwp-content/themes/<slug>/
underwp-content/uploads/deckwp-backups/with the same retention
and checksum semantics as plugin backups.Three integration points now accept the discriminator:
-
BackupManager::snapshotTheme(string $slug)— mirror of
snapshot()but targets the themes tree. Themes are always
folder-based so we skip the single-file (Hello Dolly) branch.
Ceiling raised to 800 MB to accommodate Avada / Divi / Beaver
Builder all-in-ones; below the disk-fill safety floor. -
BackupManager::restoreTheme(...)— mirror ofrestore().
Same temp-extract / move-old-aside / move-new-into-place /
rollback choreography as plugin restore. -
POST /wp-json/deckwp/v1/backup-createaccepts
{ slug, type: 'plugin'|'theme' }— defaultpluginkeeps
backwards compatibility with v0.12.0+ dashboards. -
POST /wp-json/deckwp/v1/restore-backupaccepts the same
typediscriminator. -
Installer::installOneThemehonorsbackup_requiredon the
upgrade path — the dashboard's pre-update backup row now
actually materialises a zip on disk when the orchestrator
targets a theme installation. Previously themes silently
skipped the snapshot ("no backup/smoke check yet" — fixed
here for the backup leg; smoke check remains TODO).
-
Wire contract change
/backup-create + /restore-backup body field type is now
read. Dashboards on connector v0.31.0 or earlier ignore the field
on outbound (always sends nothing for type, default 'plugin'
applies). Backwards-compatible by design.