Build the download address from the plugin header, not from the forge
The v1.4.0 release went out and the feed came back readable, with this in it:
"browser_download_url":
"http://57.129.89.117:25001/kw/woocommerce-email-discount-manager/..."
That field is filled from the forge's own configured root URL, which is not
necessarily the address the outside world reaches it on. A shop handed that
would be downloading plugin code over plain HTTP from a raw IP on a
non-standard port, bypassing the Cloudflare tunnel in front of gitea.nolimit.dk
and breaking the day that address changes.
parse_release() now rebuilds both the package and the release-page address from
the repository address in the plugin header, which is the one place that
already had to be right for any of this to work. tests/smoke.php pins it with
the exact response the live feed returned.
The release check had the same bug from the other end, and a worse version of
it: it verified the address the API reported rather than the one the plugin
builds, so it was a green check on a URL no shop ever uses. It now downloads
the package the plugin would download and opens the zip to confirm it is the
plugin and not an error page with a 200 on it.
And it only runs on the forge named in "Update URI". The other one is a mirror;
requiring a mirror to be anonymously readable failed the GitHub half of the
v1.4.0 release for a repository that is private on purpose.
ci/update-smoke.sh is the check none of the above can stand in for: install the
previous version into a throwaway shop and let WordPress find, download and
install the new one by itself. Every piece can be individually correct and the
update still not arrive.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
daa72f8f32
commit
8d0fa8c9d2
+44
-24
@@ -176,49 +176,69 @@ class WC_EDM_Updater {
|
||||
return null;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
$release = $this->parse_release( json_decode( wp_remote_retrieve_body( $response ), true ) );
|
||||
|
||||
if ( ! is_array( $body ) || empty( $body['tag_name'] ) ) {
|
||||
if ( ! $release ) {
|
||||
set_transient( self::TRANSIENT, 'none', 15 * MINUTE_IN_SECONDS );
|
||||
return null;
|
||||
}
|
||||
|
||||
$package = '';
|
||||
$tested = '';
|
||||
set_transient( self::TRANSIENT, $release, self::TTL );
|
||||
|
||||
return $release;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a release API response into what WordPress needs.
|
||||
*
|
||||
* The download address is rebuilt from the repository address in the
|
||||
* plugin header rather than taken from the response's
|
||||
* browser_download_url. That field is built from the forge's own
|
||||
* configured root URL, which is not necessarily the address the outside
|
||||
* world reaches it on — gitea.nolimit.dk returns
|
||||
* "http://57.129.89.117:25001/..." there, a plain-HTTP raw IP that
|
||||
* bypasses the Cloudflare tunnel in front of it. A shop handed that would
|
||||
* be downloading plugin code over an unencrypted connection to an address
|
||||
* that is free to change. Measured on the v1.4.0 release.
|
||||
*
|
||||
* @param mixed $body Decoded API response.
|
||||
* @return array|null { version, package, url, tested }, or null.
|
||||
*/
|
||||
private function parse_release( $body ) {
|
||||
if ( ! is_array( $body ) || empty( $body['tag_name'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tag = $body['tag_name'];
|
||||
$base = rtrim( self::update_uri(), '/' );
|
||||
$zip = '';
|
||||
|
||||
foreach ( isset( $body['assets'] ) ? $body['assets'] : array() as $asset ) {
|
||||
if ( empty( $asset['name'] ) || empty( $asset['browser_download_url'] ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( substr( $asset['name'], -4 ) === '.zip' ) {
|
||||
$package = $asset['browser_download_url'];
|
||||
if ( ! empty( $asset['name'] ) && '.zip' === substr( $asset['name'], -4 ) ) {
|
||||
$zip = $asset['name'];
|
||||
}
|
||||
}
|
||||
|
||||
if ( '' === $zip ) {
|
||||
// A release with no zip attached is a half-finished release.
|
||||
// Offering it would hand WordPress a download it cannot install.
|
||||
return null;
|
||||
}
|
||||
|
||||
// The release notes carry the compatibility line the release workflow
|
||||
// writes, so wp-admin can show what this version was actually tested
|
||||
// against instead of what the previous one claimed.
|
||||
$tested = '';
|
||||
if ( ! empty( $body['body'] ) && preg_match( '/^Tested up to:\s*(\S+)/mi', $body['body'], $m ) ) {
|
||||
$tested = $m[1];
|
||||
}
|
||||
|
||||
if ( '' === $package ) {
|
||||
// A release with no zip attached is a half-finished release.
|
||||
// Offering it would hand WordPress a download it cannot install.
|
||||
set_transient( self::TRANSIENT, 'none', 15 * MINUTE_IN_SECONDS );
|
||||
return null;
|
||||
}
|
||||
|
||||
$release = array(
|
||||
'version' => ltrim( $body['tag_name'], 'vV' ),
|
||||
'package' => $package,
|
||||
'url' => ! empty( $body['html_url'] ) ? $body['html_url'] : $uri,
|
||||
return array(
|
||||
'version' => ltrim( $tag, 'vV' ),
|
||||
'package' => $base . '/releases/download/' . rawurlencode( $tag ) . '/' . rawurlencode( $zip ),
|
||||
'url' => $base . '/releases/tag/' . rawurlencode( $tag ),
|
||||
'tested' => $tested,
|
||||
);
|
||||
|
||||
set_transient( self::TRANSIENT, $release, self::TTL );
|
||||
|
||||
return $release;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user