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
4 changes: 2 additions & 2 deletions resources/js/components/molecules/VersionDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ export function VersionDisplay( {
installableBusy = false,
onUpdate,
}: VersionDisplayProps ) {
if ( feature.has_update ) {
if ( feature.update_version ) {
return (
<div className="flex items-center gap-1.5">
<span className="text-xs font-mono text-muted-foreground line-through">
v{ feature.installed_version }
</span>
<span className="text-muted-foreground text-xs">→</span>
<span className="text-xs font-mono font-bold">
v{ feature.version }
v{ feature.update_version }
</span>
{ ( upgradeLabel || onUpdate ) && (
<UpdateButton
Expand Down
4 changes: 2 additions & 2 deletions resources/js/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ interface BaseFeature {
*/
installed_version?: string;
/**
* Whether a newer version is available and the feature is currently installed.
* Version available in the WordPress update transient, or null when no update is pending.
* Only present for installable features (plugin/theme).
*/
has_update?: boolean;
update_version?: string | null;
}

/**
Expand Down
17 changes: 9 additions & 8 deletions src/Harbor/API/REST/V1/Feature_Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace LiquidWeb\Harbor\API\REST\V1;

use LiquidWeb\Harbor\Features\Error_Code;
use LiquidWeb\Harbor\Features\Feature_Resource;
use LiquidWeb\Harbor\Features\Manager;
use LiquidWeb\Harbor\Features\Types\Feature;
use LiquidWeb\Harbor\Utils\Cast;
Expand Down Expand Up @@ -191,7 +192,7 @@ public function get_items( $request ) {
$data = [];

foreach ( $features as $feature ) {
$data[] = $feature->to_array();
$data[] = Feature_Resource::from_feature( $feature )->to_array();
}

return new WP_REST_Response( $data );
Expand Down Expand Up @@ -224,7 +225,7 @@ public function get_item( $request ) {
);
}

return new WP_REST_Response( $feature->to_array() );
return new WP_REST_Response( Feature_Resource::from_feature( $feature )->to_array() );
}

/**
Expand All @@ -246,7 +247,7 @@ public function enable( WP_REST_Request $request ) {
}

return new WP_REST_Response(
$feature->to_array()
Feature_Resource::from_feature( $feature )->to_array()
);
}

Expand All @@ -268,7 +269,7 @@ public function disable( WP_REST_Request $request ) {
}

return new WP_REST_Response(
$feature->to_array()
Feature_Resource::from_feature( $feature )->to_array()
);
}

Expand All @@ -290,7 +291,7 @@ public function update_item( $request ) {
}

return new WP_REST_Response(
$feature->to_array()
Feature_Resource::from_feature( $feature )->to_array()
);
}

Expand Down Expand Up @@ -411,9 +412,9 @@ public function get_item_schema(): array {
'readonly' => true,
'context' => [ 'view' ],
],
'has_update' => [
'description' => __( 'Whether a newer version is available and the feature is currently installed.', '%TEXTDOMAIN%' ),
'type' => 'boolean',
'update_version' => [
'description' => __( 'Version available in the WordPress update transient, or null when no update is pending.', '%TEXTDOMAIN%' ),
'type' => [ 'string', 'null' ],
'readonly' => true,
'context' => [ 'view' ],
],
Expand Down
12 changes: 0 additions & 12 deletions src/Harbor/Features/Contracts/Installable.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,6 @@ public function is_installed(): bool;
*/
public function get_installed_version(): ?string;

/**
* Whether a newer version is available and the feature is currently installed.
*
* Returns true only when the feature is installed on disk and the catalog version
* is strictly greater than the installed version.
*
* @since 1.0.0
*
* @return bool
*/
public function has_update(): bool;

/**
* Builds the complete update data array for this feature type.
*
Expand Down
182 changes: 182 additions & 0 deletions src/Harbor/Features/Feature_Resource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<?php declare( strict_types=1 );

namespace LiquidWeb\Harbor\Features;

use LiquidWeb\Harbor\Features\Types\Feature;
use LiquidWeb\Harbor\Features\Types\Plugin;
use LiquidWeb\Harbor\Features\Types\Theme;

/**
* Decorates a resolved Feature with transient-sourced update information.
*
* After feature resolution is complete (Feature_Repository cache is warm),
* reading the update transient via get_site_transient() is safe: the transient
* filter chain hits the cached repository result, so there is no circular
* dependency.
*
* update_version is the version string from the WordPress update transient
* response object, or null when the feature has no pending update registered
* by the handler (e.g. dot-org plugins, unlicensed, or not installed).
*
* @since 1.0.0
*/
final class Feature_Resource {

/**
* The resolved Feature.
*
* @since 1.0.0
*
* @var Feature
*/
private Feature $feature;

/**
* The update version sourced from the WordPress update transient, or null.
*
* @since 1.0.0
*
* @var string|null
*/
private $update_version;

/**
* @since 1.0.0
*
* @param Feature $feature The resolved feature.
* @param string|null $update_version Version from the update transient, or null.
*/
public function __construct( Feature $feature, $update_version ) {
$this->feature = $feature;
$this->update_version = $update_version;
}

/**
* Constructs a Feature_Resource from a resolved Feature by reading the
* appropriate WordPress update transient.
*
* Should only be called after Feature_Repository has already cached its
* results so transient access does not trigger re-resolution.
*
* @since 1.0.0
*
* @param Feature $feature The resolved feature.
*
* @return self
*/
public static function from_feature( Feature $feature ) {
$update_version = null;

if ( $feature instanceof Plugin ) {
$update_version = self::get_plugin_update_version( $feature );
} elseif ( $feature instanceof Theme ) {
$update_version = self::get_theme_update_version( $feature );
}

return new self( $feature, $update_version );
}

/**
* Returns the data array for use in REST API responses.
*
* Merges all feature attributes with the transient-sourced update_version.
*
* @since 1.0.0
*
* @return array<string, mixed>
*/
public function to_array(): array {
return array_merge(
$this->feature->to_array(),
[ 'update_version' => $this->update_version ]
);
}

/**
* Returns the update version, or null if no update is pending.
*
* @since 1.0.0
*
* @return string|null
*/
public function get_update_version() {
return $this->update_version;
}

/**
* Returns the decorated Feature.
*
* @since 1.0.0
*
* @return Feature
*/
public function get_feature(): Feature {
return $this->feature;
}

/**
* Reads update_version from the plugin update transient for the given plugin feature.
*
* @since 1.0.0
*
* @param Plugin $feature The plugin feature.
*
* @return string|null
*/
private static function get_plugin_update_version( Plugin $feature ) {
$transient = get_site_transient( 'update_plugins' );

if ( ! is_object( $transient ) || ! isset( $transient->response ) || ! is_array( $transient->response ) ) {
return null;
}

$plugin_file = $feature->get_plugin_file();

if ( ! isset( $transient->response[ $plugin_file ] ) ) {
return null;
}

$response = $transient->response[ $plugin_file ];

$version = is_object( $response ) ? ( $response->new_version ?? null ) : null;

if ( ! is_string( $version ) || $version === '' ) {
return null;
}

return $version;
}

/**
* Reads update_version from the theme update transient for the given theme feature.
*
* @since 1.0.0
*
* @param Theme $feature The theme feature.
*
* @return string|null
*/
private static function get_theme_update_version( Theme $feature ) {
$transient = get_site_transient( 'update_themes' );

if ( ! is_object( $transient ) || ! isset( $transient->response ) || ! is_array( $transient->response ) ) {
return null;
}

$slug = $feature->get_slug();

if ( ! isset( $transient->response[ $slug ] ) ) {
return null;
}

$response = $transient->response[ $slug ];

$version = is_array( $response ) ? ( $response['new_version'] ?? null ) : null;

if ( ! is_string( $version ) || $version === '' ) {
return null;
}

return $version;
}
}
35 changes: 6 additions & 29 deletions src/Harbor/Features/Types/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ public function __construct( array $attributes ) {
);

parent::__construct( $attributes );

// has_update() reads $this->attributes, so it must be set after parent::__construct().
$this->attributes['has_update'] = $this->has_update();
}

/**
Expand Down Expand Up @@ -100,29 +97,6 @@ public function is_dot_org(): bool {
return Cast::to_bool( $this->attributes['is_dot_org'] ?? false );
}

/**
* Whether a newer version is available and this plugin is currently installed.
*
* @since 1.0.0
*
* @return bool
*/
public function has_update(): bool {
$installed_version = $this->get_installed_version();

if ( $installed_version === null ) {
return false;
}

$catalog_version = Cast::to_string( $this->attributes['version'] ?? '' );

if ( $catalog_version === '' ) {
return false;
}

return version_compare( $catalog_version, $installed_version, '>' );
}

/**
* Builds the complete update data array for this Plugin feature.
*
Expand All @@ -133,19 +107,22 @@ public function has_update(): bool {
* @return array<string, mixed>
*/
public function get_update_data( Catalog_Feature $catalog_feature ): array {
$installed_version = $this->get_installed_version() ?? '';
$catalog_version = $catalog_feature->get_version() ?? '';

return [
'name' => $this->get_name(),
'slug' => $this->get_slug(),
'version' => $catalog_feature->get_version() ?? '',
'version' => $catalog_version,
'package' => $catalog_feature->get_download_url() ?? '',
'url' => $this->get_documentation_url(),
'author' => implode( ', ', $this->get_authors() ),
'sections' => [
'description' => $this->get_description(),
],
'plugin_file' => $this->get_plugin_file(),
'installed_version' => $this->get_installed_version() ?? '',
'has_update' => $this->has_update(),
'installed_version' => $installed_version,
'has_update' => $installed_version !== '' && $catalog_version !== '' && version_compare( $catalog_version, $installed_version, '>' ),
];
}

Expand Down
Loading
Loading