Skip to content

Commit e9dd5d2

Browse files
johnbillionchuckadamsafragennorcrosscdils
authored
Rearchitect Updater into a registry (#428)
Signed-off-by: John Blackbourn <john@johnblackbourn.com> Signed-off-by: Andy Fragen <andy@thefragens.com> Signed-off-by: Carrie Dils <carriedils@gmail.com> Signed-off-by: Norcross <andrew.norcross@gmail.com> Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Signed-off-by: joedolson <joedolson@users.noreply.github.com> Signed-off-by: Joe Dolson <design@joedolson.com> Signed-off-by: Shadi Sharaf <shady@sharaf.me> Co-authored-by: Chuck Adams <chaz@chaz.works> Co-authored-by: Andy Fragen <andy@thefragens.com> Co-authored-by: Norcross <andrew.norcross@gmail.com> Co-authored-by: Carrie Dils <cdils@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: rmccue <21655+rmccue@users.noreply.github.com> Co-authored-by: cdils <3099408+cdils@users.noreply.github.com> Co-authored-by: joedolson <joedolson@users.noreply.github.com> Co-authored-by: Joe Dolson <design@joedolson.com> Co-authored-by: Shady Sharaf <shady@sharaf.me>
1 parent 8f3ab3c commit e9dd5d2

File tree

14 files changed

+1148
-165
lines changed

14 files changed

+1148
-165
lines changed

.github/workflows/releases.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ jobs:
3737
/tmp/${{ github.event.repository.name }}-${{ steps.tag.outputs.tag }}.zip
3838
/tmp/fair-dist/*
3939
40+
- name: Upload to Fastly Object Storage
41+
# note the plugin filename is always 'fair-connect', regardless of the repo name or slug
42+
run: |
43+
aws s3 cp /tmp/${{ github.event.repository.name }}-${{ steps.tag.outputs.tag }}.zip s3://download.fair.pm/release/fair-connect-${{ steps.tag.outputs.tag }}.zip
44+
aws s3 cp --recursive --exclude '*' --include '*.zip' /tmp/fair-dist s3://download.fair.pm/release/
45+
env:
46+
AWS_ACCESS_KEY_ID: ${{ secrets.DOWNLOAD_BUCKET_KEY_ID }}
47+
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOWNLOAD_BUCKET_SECRET_KEY }}
48+
AWS_DEFAULT_REGION: 'us-west'
49+
AWS_ENDPOINT_URL: 'https://us-west.object.fastlystorage.app'
50+
AWS_REQUEST_CHECKSUM_CALCULATION: 'WHEN_REQUIRED'
51+
4052
- name: Build provenance attestation
4153
uses: actions/attest-build-provenance@v2
4254
with:

bin/bundle.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ touch /tmp/fair-dist/SHA1SUMS
1919
touch /tmp/fair-dist/SHA256SUMS
2020
touch /tmp/fair-dist/SHA384SUMS
2121

22+
# Auto-detect and hash plugin zip.
23+
PLUGIN_VERSION=$(get_plugin_header "Version")
24+
REPO_NAME=$(basename "$GITHUB_REPOSITORY")
25+
PLUGIN_ZIP="/tmp/${REPO_NAME}-${PLUGIN_VERSION}.zip"
26+
27+
if [ -f "$PLUGIN_ZIP" ]; then
28+
echo "Hashing plugin zip: $PLUGIN_ZIP" >&2
29+
# Change to /tmp so hash files contain just filename, not full path
30+
cd /tmp
31+
md5sum -b "$(basename "$PLUGIN_ZIP")" >> /tmp/fair-dist/MD5SUMS
32+
sha1sum -b "$(basename "$PLUGIN_ZIP")" >> /tmp/fair-dist/SHA1SUMS
33+
sha256sum -b "$(basename "$PLUGIN_ZIP")" >> /tmp/fair-dist/SHA256SUMS
34+
sha384sum -b "$(basename "$PLUGIN_ZIP")" >> /tmp/fair-dist/SHA384SUMS
35+
cd - > /dev/null
36+
fi
37+
2238
# Bundle our plugin first.
2339
[ -d /tmp/fair-temp ] && rm -rf /tmp/fair-temp
2440
mkdir -p /tmp/fair-temp/wordpress/wp-content/plugins/fair-plugin
@@ -54,6 +70,12 @@ for VERSION in $AVAILABLE_VERSIONS; do
5470
curl -sSL "$WP_ZIP_URL" -o "$WP_ZIP_FILE"
5571
EXPECTED_HASH=$(curl -sSL "$WP_ZIP_URL.sha1")
5672

73+
# Skip if we can't get a valid hash.
74+
if [[ ! "$EXPECTED_HASH" =~ ^[a-z0-9]{40}$ ]]; then
75+
echo "Failed to fetch valid hash for $VERSION" >&2
76+
continue
77+
fi
78+
5779
# Verify the checksum.
5880
# (sha1 is suboptimal, but it's all we've got.)
5981
echo " Verifying checksum" >&2

inc/default-repo/namespace.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function replace_repo_api_urls( $status, $args, $url ) {
5353
if (
5454
! str_contains( $url, 'api.wordpress.org/plugins/' )
5555
&& ! str_contains( $url, 'api.wordpress.org/themes/' )
56+
&& ! str_contains( $url, 'api.wordpress.org/translations/' )
5657
&& ! str_contains( $url, 'api.wordpress.org/core/version-check/' )
5758
) {
5859
return $status;

inc/namespace.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
const CACHE_BASE = 'fair-';
1313
const CACHE_LIFETIME = 12 * HOUR_IN_SECONDS;
14+
const CACHE_LIFETIME_FAILURE = HOUR_IN_SECONDS;
1415
const NS_SEPARATOR = '\\';
1516

1617
/**

inc/packages/namespace.php

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use const FAIR\CACHE_BASE;
1111
use const FAIR\CACHE_LIFETIME;
12+
use const FAIR\CACHE_LIFETIME_FAILURE;
1213
use FAIR\Packages\DID\Document as DIDDocument;
1314
use FAIR\Packages\DID\PLC;
1415
use FAIR\Packages\DID\Web;
@@ -22,12 +23,33 @@
2223
const CACHE_KEY = CACHE_BASE . 'packages-';
2324
const CACHE_METADATA_DOCUMENTS = CACHE_BASE . 'metadata-documents-';
2425
const CACHE_RELEASE_PACKAGES = CACHE_BASE . 'release-packages';
26+
const CACHE_UPDATE_ERRORS = CACHE_BASE . 'update-errors-';
2527
const CACHE_DID_FOR_INSTALL = 'fair-install-did';
2628
const CONTENT_TYPE = 'application/json+fair';
2729
const SERVICE_ID = 'FairPackageManagementRepo';
2830

2931
// phpcs:disable WordPress.NamingConventions.ValidVariableName
3032

33+
/**
34+
* Cache an update error for a package.
35+
*
36+
* @param string $did DID of the package.
37+
* @param WP_Error $error The error to cache.
38+
*/
39+
function cache_update_error( string $did, WP_Error $error ): void {
40+
$error->add_data( [ 'timestamp' => time() ], $error->get_error_code() );
41+
set_site_transient( CACHE_UPDATE_ERRORS . $did, $error, CACHE_LIFETIME_FAILURE );
42+
}
43+
44+
/**
45+
* Clear a cached update error for a package.
46+
*
47+
* @param string $did DID of the package.
48+
*/
49+
function clear_update_error( string $did ): void {
50+
delete_site_transient( CACHE_UPDATE_ERRORS . $did );
51+
}
52+
3153
/**
3254
* Bootstrap.
3355
*
@@ -93,6 +115,12 @@ function get_did_hash( string $id ) {
93115
* @return DIDDocument|WP_Error
94116
*/
95117
function get_did_document( string $id ) {
118+
// Check for cached error from previous failed request.
119+
$cached_error = get_site_transient( CACHE_UPDATE_ERRORS . $id );
120+
if ( is_wp_error( $cached_error ) ) {
121+
return $cached_error;
122+
}
123+
96124
$cached = get_site_transient( CACHE_METADATA_DOCUMENTS . $id );
97125
if ( $cached ) {
98126
return $cached;
@@ -101,13 +129,18 @@ function get_did_document( string $id ) {
101129
// Parse the DID, then fetch the details.
102130
$did = parse_did( $id );
103131
if ( is_wp_error( $did ) ) {
132+
cache_update_error( $id, $did );
104133
return $did;
105134
}
106135

107136
$document = $did->fetch_document();
108137
if ( is_wp_error( $document ) ) {
138+
cache_update_error( $id, $document );
109139
return $document;
110140
}
141+
142+
// Clear any previous error on success.
143+
clear_update_error( $id );
111144
set_site_transient( CACHE_METADATA_DOCUMENTS . $id, $document, CACHE_LIFETIME );
112145

113146
return $document;
@@ -177,30 +210,38 @@ function fetch_package_metadata( string $id ) {
177210
// Fetch data from the repository.
178211
$service = $document->get_service( SERVICE_ID );
179212
if ( empty( $service ) ) {
180-
return new WP_Error( 'fair.packages.fetch_metadata.no_service', __( 'DID is not a valid package to fetch metadata for.', 'fair' ) );
213+
$error = new WP_Error( 'fair.packages.fetch_metadata.no_service', __( 'DID is not a valid package to fetch metadata for.', 'fair' ) );
214+
cache_update_error( $id, $error );
215+
return $error;
181216
}
182217
$repo_url = $service->serviceEndpoint;
183218

184-
$metadata = fetch_metadata_doc( $repo_url );
219+
$metadata = fetch_metadata_doc( $repo_url, $id );
185220

186221
if ( is_wp_error( $metadata ) ) {
187222
return $metadata;
188223
}
189224

190225
if ( $metadata->id !== $id ) {
191-
return new WP_Error( 'fair.packages.fetch_metadata.mismatch', __( 'Fetched metadata does not match the requested DID.', 'fair' ) );
226+
$error = new WP_Error( 'fair.packages.fetch_metadata.mismatch', __( 'Fetched metadata does not match the requested DID.', 'fair' ) );
227+
cache_update_error( $id, $error );
228+
return $error;
192229
}
193230

231+
// Clear any previous error on success.
232+
clear_update_error( $id );
233+
194234
return $metadata;
195235
}
196236

197237
/**
198238
* Fetch the metadata document for a package.
199239
*
200240
* @param string $url URL for the metadata document.
241+
* @param string $did DID of the package.
201242
* @return MetadataDocument|WP_Error
202243
*/
203-
function fetch_metadata_doc( string $url ) {
244+
function fetch_metadata_doc( string $url, string $did ) {
204245
$cache_key = CACHE_KEY . md5( $url );
205246
$response = get_site_transient( $cache_key );
206247
$response = fetch_metadata_from_local( $response, $url );
@@ -219,9 +260,15 @@ function fetch_metadata_doc( string $url ) {
219260
$response = wp_remote_get( $url, $options );
220261
$code = wp_remote_retrieve_response_code( $response );
221262
if ( is_wp_error( $response ) ) {
263+
cache_update_error( $did, $response );
222264
return $response;
223265
} elseif ( $code !== 200 ) {
224-
return new WP_Error( 'fair.packages.metadata.failure', __( 'HTTP error code received', 'fair' ) );
266+
$error = new WP_Error(
267+
'fair.packages.metadata.http_error',
268+
sprintf( __( 'HTTP %d error received', 'fair' ), $code )
269+
);
270+
cache_update_error( $did, $error );
271+
return $error;
225272
}
226273

227274
// Reorder sections before caching.
@@ -727,6 +774,9 @@ function cache_did_for_install( array $options ): array {
727774
$did = array_find_key(
728775
$releases,
729776
function ( $release ) use ( $options ) {
777+
if ( ! is_array( $release->artifacts->package ) ) {
778+
return false;
779+
}
730780
$artifact = pick_artifact_by_lang( $release->artifacts->package );
731781
return $artifact && $artifact->url === $options['package'];
732782
}
@@ -754,7 +804,7 @@ function delete_cached_did_for_install(): void {
754804
*
755805
* This is commonly required for packages from Git hosts.
756806
*
757-
* @param string $source Path of $source.
807+
* @param string|WP_Error $source Path of $source, or a WP_Error object.
758808
* @param string $remote_source Path of $remote_source.
759809
* @param WP_Upgrader $upgrader An Upgrader object.
760810
* @param array $hook_extra Array of hook data.

inc/updater/class-lite.php

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ public function run() {
125125
);
126126
$response = get_site_transient( "git-updater-lite_{$this->file}" );
127127
if ( ! $response ) {
128+
/* Apply filter to API URL.
129+
* Add `channel=development` query arg to URL to get pre-release versions.
130+
*
131+
* @param string $url The API URL.
132+
* @param string $slug The plugin/theme slug
133+
*/
134+
$url = apply_filters( 'git_updater_lite_api_url', $url, $this->slug );
128135
$response = wp_remote_get( $url );
129136
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) === 404 ) {
130137
return $response;
@@ -136,11 +143,9 @@ public function run() {
136143
}
137144
$this->api_data->file = $this->file;
138145

139-
/*
140-
* Set transient for 5 minutes as AWS sets 5 minute timeout
141-
* for release asset redirect.
142-
*/
143-
set_site_transient( "git-updater-lite_{$this->file}", $this->api_data, 5 * \MINUTE_IN_SECONDS );
146+
// Set timeout for transient via filter.
147+
$timeout = apply_filters( 'git_updater_lite_transient_timeout', 6 * HOUR_IN_SECONDS, $this->file );
148+
set_site_transient( "git-updater-lite_{$this->file}", $this->api_data, $timeout );
144149
} else {
145150
if ( property_exists( $response, 'error' ) ) {
146151
return new WP_Error( 'repo-no-exist', 'Specified repo does not exist' );
@@ -178,18 +183,23 @@ function () {
178183
/**
179184
* Correctly rename dependency for activation.
180185
*
181-
* @param string $source Path of $source.
182-
* @param string $remote_source Path of $remote_source.
183-
* @param WP_Upgrader $upgrader An Upgrader object.
184-
* @param array $hook_extra Array of hook data.
186+
* @param string|WP_Error $source Path of $source, or a WP_Error object.
187+
* @param string $remote_source Path of $remote_source.
188+
* @param WP_Upgrader $upgrader An Upgrader object.
189+
* @param array $hook_extra Array of hook data.
185190
*
186191
* @throws TypeError If the type of $upgrader is not correct.
187192
*
188193
* @return string|WP_Error
189194
*/
190-
public function upgrader_source_selection( string $source, string $remote_source, WP_Upgrader $upgrader, $hook_extra = null ) {
195+
public function upgrader_source_selection( $source, string $remote_source, WP_Upgrader $upgrader, $hook_extra = null ) {
191196
global $wp_filesystem;
192197

198+
// Exit early for errors.
199+
if ( is_wp_error( $source ) ) {
200+
return $source;
201+
}
202+
193203
$new_source = $source;
194204

195205
// Exit if installing.

inc/updater/class-package.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
/**
3+
* Package data container.
4+
*
5+
* @package FAIR
6+
*/
7+
8+
namespace FAIR\Updater;
9+
10+
use FAIR\Packages;
11+
12+
/**
13+
* Represents a registered FAIR package (plugin or theme).
14+
*/
15+
abstract class Package {
16+
17+
/**
18+
* The DID of the package.
19+
*
20+
* @var string
21+
*/
22+
public string $did;
23+
24+
/**
25+
* Absolute path to the main file.
26+
*
27+
* @var string
28+
*/
29+
public string $filepath;
30+
31+
/**
32+
* Current installed version.
33+
*
34+
* @var string|null
35+
*/
36+
public ?string $local_version;
37+
38+
/**
39+
* Cached metadata document.
40+
*
41+
* @var \FAIR\Packages\MetadataDocument|null
42+
*/
43+
private $metadata = null;
44+
45+
/**
46+
* Cached release document.
47+
*
48+
* @var \FAIR\Packages\ReleaseDocument|null
49+
*/
50+
private $release = null;
51+
52+
/**
53+
* Constructor.
54+
*
55+
* @param string $did The DID of the package.
56+
* @param string $filepath Absolute path to the main file.
57+
*/
58+
public function __construct( string $did, string $filepath ) {
59+
$this->did = $did;
60+
$this->filepath = $filepath;
61+
$this->local_version = $filepath ? get_file_data( $filepath, [ 'Version' => 'Version' ] )['Version'] : null;
62+
}
63+
64+
/**
65+
* Get the package slug.
66+
*
67+
* @return string The slug (directory name for plugins, stylesheet for themes).
68+
*/
69+
abstract public function get_slug(): string;
70+
71+
/**
72+
* Get the relative path used in update transients.
73+
*
74+
* @return string The relative path.
75+
*/
76+
abstract public function get_relative_path(): string;
77+
78+
/**
79+
* Get the metadata document, fetching and caching if needed.
80+
*
81+
* @return \FAIR\Packages\MetadataDocument|\WP_Error|null
82+
*/
83+
final public function get_metadata() {
84+
if ( $this->metadata === null ) {
85+
$metadata = Packages\fetch_package_metadata( $this->did );
86+
if ( ! is_wp_error( $metadata ) ) {
87+
$this->metadata = $metadata;
88+
}
89+
return $metadata;
90+
}
91+
return $this->metadata;
92+
}
93+
94+
/**
95+
* Get the release document, fetching and caching if needed.
96+
*
97+
* @return \FAIR\Packages\ReleaseDocument|\WP_Error|null
98+
*/
99+
final public function get_release() {
100+
if ( $this->release === null ) {
101+
$release = Packages\get_latest_release_from_did( $this->did );
102+
if ( ! is_wp_error( $release ) ) {
103+
$this->release = $release;
104+
}
105+
return $release;
106+
}
107+
return $this->release;
108+
}
109+
}

0 commit comments

Comments
 (0)