Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions inc/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

const CACHE_BASE = 'fair-';
const CACHE_LIFETIME = 12 * HOUR_IN_SECONDS;
const CACHE_LIFETIME_FAILURE = HOUR_IN_SECONDS;
const NS_SEPARATOR = '\\';

/**
Expand Down
57 changes: 52 additions & 5 deletions inc/packages/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use const FAIR\CACHE_BASE;
use const FAIR\CACHE_LIFETIME;
use const FAIR\CACHE_LIFETIME_FAILURE;
use FAIR\Packages\DID\Document as DIDDocument;
use FAIR\Packages\DID\PLC;
use FAIR\Packages\DID\Web;
Expand All @@ -22,12 +23,33 @@
const CACHE_KEY = CACHE_BASE . 'packages-';
const CACHE_METADATA_DOCUMENTS = CACHE_BASE . 'metadata-documents-';
const CACHE_RELEASE_PACKAGES = CACHE_BASE . 'release-packages';
const CACHE_UPDATE_ERRORS = CACHE_BASE . 'update-errors-';
const CACHE_DID_FOR_INSTALL = 'fair-install-did';
const CONTENT_TYPE = 'application/json+fair';
const SERVICE_ID = 'FairPackageManagementRepo';

// phpcs:disable WordPress.NamingConventions.ValidVariableName

/**
* Cache an update error for a package.
*
* @param string $did DID of the package.
* @param WP_Error $error The error to cache.
*/
function cache_update_error( string $did, WP_Error $error ): void {
$error->add_data( [ 'timestamp' => time() ], $error->get_error_code() );
set_site_transient( CACHE_UPDATE_ERRORS . $did, $error, CACHE_LIFETIME_FAILURE );
}

/**
* Clear a cached update error for a package.
*
* @param string $did DID of the package.
*/
function clear_update_error( string $did ): void {
delete_site_transient( CACHE_UPDATE_ERRORS . $did );
}

/**
* Bootstrap.
*
Expand Down Expand Up @@ -93,6 +115,12 @@ function get_did_hash( string $id ) {
* @return DIDDocument|WP_Error
*/
function get_did_document( string $id ) {
// Check for cached error from previous failed request.
$cached_error = get_site_transient( CACHE_UPDATE_ERRORS . $id );
if ( is_wp_error( $cached_error ) ) {
return $cached_error;
}

$cached = get_site_transient( CACHE_METADATA_DOCUMENTS . $id );
if ( $cached ) {
return $cached;
Expand All @@ -101,13 +129,18 @@ function get_did_document( string $id ) {
// Parse the DID, then fetch the details.
$did = parse_did( $id );
if ( is_wp_error( $did ) ) {
cache_update_error( $id, $did );
return $did;
}

$document = $did->fetch_document();
if ( is_wp_error( $document ) ) {
cache_update_error( $id, $document );
return $document;
}

// Clear any previous error on success.
clear_update_error( $id );
set_site_transient( CACHE_METADATA_DOCUMENTS . $id, $document, CACHE_LIFETIME );

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

$metadata = fetch_metadata_doc( $repo_url );
$metadata = fetch_metadata_doc( $repo_url, $id );

if ( is_wp_error( $metadata ) ) {
return $metadata;
}

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

// Clear any previous error on success.
clear_update_error( $id );

return $metadata;
}

/**
* Fetch the metadata document for a package.
*
* @param string $url URL for the metadata document.
* @param string $did DID of the package.
* @return MetadataDocument|WP_Error
*/
function fetch_metadata_doc( string $url ) {
function fetch_metadata_doc( string $url, string $did ) {
$cache_key = CACHE_KEY . md5( $url );
$response = get_site_transient( $cache_key );
$response = fetch_metadata_from_local( $response, $url );
Expand All @@ -219,9 +260,15 @@ function fetch_metadata_doc( string $url ) {
$response = wp_remote_get( $url, $options );
$code = wp_remote_retrieve_response_code( $response );
if ( is_wp_error( $response ) ) {
cache_update_error( $did, $response );
return $response;
} elseif ( $code !== 200 ) {
return new WP_Error( 'fair.packages.metadata.failure', __( 'HTTP error code received', 'fair' ) );
$error = new WP_Error(
'fair.packages.metadata.http_error',
sprintf( __( 'HTTP %d error received', 'fair' ), $code )
);
cache_update_error( $did, $error );
return $error;
}

// Reorder sections before caching.
Expand Down
68 changes: 68 additions & 0 deletions inc/updater/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

namespace FAIR\Updater;

use const FAIR\CACHE_LIFETIME_FAILURE;
use const FAIR\Packages\CACHE_DID_FOR_INSTALL;
use const FAIR\Packages\CACHE_RELEASE_PACKAGES;
use const FAIR\Packages\CACHE_UPDATE_ERRORS;
use FAIR\Packages;
use function FAIR\is_wp_cli;
use Plugin_Upgrader;
Expand All @@ -22,6 +24,7 @@
*/
function bootstrap() {
add_action( 'init', __NAMESPACE__ . '\\run' );
add_action( 'admin_init', __NAMESPACE__ . '\\register_plugin_row_hooks' );
}

/**
Expand Down Expand Up @@ -79,6 +82,71 @@ function run() {
}
}

/**
* Register hooks to display update errors below plugin rows.
*/
function register_plugin_row_hooks(): void {
$packages = get_packages();
$plugins = $packages['plugins'] ?? [];

foreach ( $plugins as $did => $path ) {
$plugin_file = plugin_basename( $path );
add_action(
"after_plugin_row_{$plugin_file}",
function ( $file, $plugin_data, $status ) use ( $did ) {
display_plugin_update_error( $file, $plugin_data, $status, $did );
},
10,
3
);
}
}

/**
* Display a cached update error below the plugin row.
*
* @param string $plugin_file Path to the plugin file relative to the plugins directory.
* @param array $plugin_data An array of plugin data.
* @param string $status Status filter currently applied to the plugin list.
* @param string $did The DID of the plugin.
*/
function display_plugin_update_error( $plugin_file, $plugin_data, $status, $did ): void {
$error = get_site_transient( CACHE_UPDATE_ERRORS . $did );
if ( ! is_wp_error( $error ) ) {
return;
}

$wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
$colspan = $wp_list_table->get_column_count();

// Calculate time remaining until retry.
$error_data = $error->get_error_data();
$timestamp = $error_data['timestamp'] ?? 0;
$retry_time = $timestamp + CACHE_LIFETIME_FAILURE;
$time_remaining = human_time_diff( time(), $retry_time );

$message = sprintf(
/* translators: %1$s: Error message, %2$s: Time period */
__( 'Error: %1$s. Update checks paused for %2$s.', 'fair' ),
$error->get_error_message(),
$time_remaining,
);

$active_class = is_plugin_active( $plugin_file ) ? ' active' : '';

printf(
'<tr class="plugin-update-tr%1$s" id="fair-error-%2$s">
<td colspan="%3$d" class="plugin-update colspanchange">
<div class="update-message notice inline notice-error notice-alt"><p>%4$s</p></div>
</td>
</tr>',
esc_attr( $active_class ),
esc_attr( sanitize_title( $plugin_file ) ),
esc_attr( $colspan ),
esc_html( $message ),
);
}

/**
* Download a package with signature verification.
*
Expand Down