Skip to content
Open
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
2 changes: 1 addition & 1 deletion inc/default-repo/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function bootstrap() {
*/
function get_default_repo_domain() : string {
if ( defined( 'FAIR_DEFAULT_REPO_DOMAIN' ) ) {
return FAIR_DEFAULT_REPO_DOMAIN;
return (string) \FAIR_DEFAULT_REPO_DOMAIN;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the output type is always correct and use the constant value from the global namespace since that is what defined( 'FAIR_DEFAULT_REPO_DOMAIN' ) checks for.

}

return 'api.aspirecloud.net';
Expand Down
31 changes: 20 additions & 11 deletions inc/packages/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -687,21 +687,30 @@ function get_banners( $banners ) : array {
* @return string
*/
function get_hashed_filename( $metadata ) : string {
$filename = $metadata->filename;
$type = str_replace( 'wp-', '', $metadata->type );
$did_hash = '-' . get_did_hash( $metadata->id );

list( $slug, $file ) = explode( '/', $filename, 2 );
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When $metadata->filename isn't specified (such as https://github.com/fairpm/fair-plugin/releases/latest/download/fair-metadata.json), the explode() failed with:

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in /inc/packages/namespace.php on line 694

and the $file was undefined which caused:

Warning: Undefined array key 1 in /inc/packages/namespace.php on line 694

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the problem is that $metadata->filename is supposed to exist and that the fair-metadata.json was incorrectly created.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the general package spec doesn’t require it so I think it makes sense that we support that scenario as a fallback.

https://github.com/fairpm/fair-protocol/blob/83be5fa21fc1f72d5bcbf2bdd654f9e72274bc39/specification.md?plain=1#L197

Arguably, WP determines the bootstrap file dynamically based on header presence (and themes don’t even need it) so I think should probably refactor this eventually.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We did update the spec and it now requires it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@afragen I think the spec shouldn't require it. None of the WP packages have that concept (at least from the existing WP-org API perspective).

I created #500 to illustrate how WP core resolves the plugin_file dynamically during install and never gets it from the update API. Asking package publishers to include the filename is simply unnecessary.

In #500 I demonstrate how we can resolve all the necessary filename data from the canonical places in WP without having to assume or deal with mismatches on the filesystem (what active_plugins thinks) and what the metadata reports.

if ( 'plugin' === $type ) {
if ( ! str_contains( $slug, $did_hash ) ) {
$slug .= $did_hash;
}
$filename = $slug . '/' . $file;
} else {
$filename = $slug . $did_hash;
// Use the slug from the filename, if present.
list( $slug, $file ) = array_pad( explode( '/', $metadata->filename ?? '', 2 ), 2, '' );
if ( '' === $slug ) {
$slug = $metadata->slug ?? '';
}

// Default to slug matching the plugin filename, if not specified.
if ( '' === $file ) {
$file = $slug . '.php';
}

// Append DID hash to slug if not already present.
if ( ! str_ends_with( $slug, $did_hash ) ) {
$slug .= $did_hash;
}

// WP plugins must include the plugin filename.
if ( 'wp-plugin' === ( $metadata->type ?? '' ) ) {
return $slug . '/' . $file;
}

return $filename;
return $slug;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, not sure I understand why a function named get_hashed_filename() no longer returns $filename.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It returns the filename only for wp-plugin type and returns the slug in all other cases. That is what this updated approach makes clear with a conditional return for that one edge case for WP plugins.

Feel free to push an update or I can update it if you can advise on the updated logic here.

}

/**
Expand Down
178 changes: 178 additions & 0 deletions tests/phpunit/tests/Packages/GetHashedFilenameTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?php
/**
* Tests for FAIR\Packages\get_hashed_filename().
*
* @package FAIR
*/

use FAIR\Packages\MetadataDocument;
use function FAIR\Packages\get_did_hash;
use function FAIR\Packages\get_hashed_filename;

/**
* Tests for FAIR\Packages\get_hashed_filename().
*
* @covers FAIR\Packages\get_hashed_filename
*/
class GetHashedFilenameTest extends WP_UnitTestCase {

/**
* Test that plugin filenames append the DID hash to the directory name.
*/
public function test_should_hash_plugin_directory_name() {
$metadata = $this->create_metadata_document(
[
'filename' => 'example/example.php',
'slug' => 'example',
'type' => 'wp-plugin',
]
);

$this->assertSame( 'example-' . get_did_hash( $metadata->id ) . '/example.php', get_hashed_filename( $metadata ) );
}

/**
* Test that missing plugin filenames fall back to the slug.
*/
public function test_should_fall_back_to_slug_when_plugin_filename_missing() {
$metadata = $this->create_metadata_document(
[
'filename' => null,
'slug' => 'example',
'type' => 'wp-plugin',
]
);

$this->assertSame( 'example-' . get_did_hash( $metadata->id ) . '/example.php', get_hashed_filename( $metadata ) );
}

/**
* Test that malformed plugin filenames still produce a valid hashed path.
*/
public function test_should_recover_when_plugin_filename_has_no_main_file() {
$metadata = $this->create_metadata_document(
[
'filename' => 'example',
'slug' => 'example',
'type' => 'wp-plugin',
]
);

$this->assertSame( 'example-' . get_did_hash( $metadata->id ) . '/example.php', get_hashed_filename( $metadata ) );
}

/**
* Test that theme filenames append the DID hash to the slug.
*/
public function test_should_hash_theme_slug() {
$metadata = $this->create_metadata_document(
[
'filename' => 'example',
'slug' => 'example',
'type' => 'wp-theme',
]
);

$this->assertSame( 'example-' . get_did_hash( $metadata->id ), get_hashed_filename( $metadata ) );
}

/**
* Test that missing non-plugin filenames still fall back to the slug.
*/
public function test_should_fall_back_to_slug_for_non_plugin_when_filename_missing() {
$metadata = $this->create_metadata_document(
[
'filename' => null,
'slug' => 'example',
'type' => 'wp-theme',
]
);

$this->assertSame( 'example-' . get_did_hash( $metadata->id ), get_hashed_filename( $metadata ) );
}

/**
* Test that a pre-hashed plugin slug is not hashed twice.
*/
public function test_should_not_append_hash_twice_for_plugin_slug() {
$hash = get_did_hash( 'did:plc:example1234567890123456789' );
$metadata = $this->create_metadata_document(
[
'filename' => 'example-' . $hash . '/example.php',
'slug' => 'example-' . $hash,
'type' => 'wp-plugin',
]
);

$this->assertSame( 'example-' . $hash . '/example.php', get_hashed_filename( $metadata ) );
}

/**
* Test that a hash-like substring in the middle of the slug still gets the DID hash appended.
*/
public function test_should_append_hash_when_same_value_appears_in_middle_of_plugin_slug() {
$hash = get_did_hash( 'did:plc:example1234567890123456789' );
$slug = 'vendor-' . $hash . '-plugin';
$metadata = $this->create_metadata_document(
[
'filename' => $slug . '/' . $slug . '.php',
'slug' => $slug,
'type' => 'wp-plugin',
]
);

$this->assertSame( $slug . '-' . $hash . '/' . $slug . '.php', get_hashed_filename( $metadata ) );
}

/**
* Test that empty plugin filenames behave the same as missing filenames.
*/
public function test_should_fall_back_to_slug_when_plugin_filename_is_empty_string() {
$metadata = $this->create_metadata_document(
[
'filename' => '',
'slug' => 'example',
'type' => 'wp-plugin',
]
);

$this->assertSame( 'example-' . get_did_hash( $metadata->id ) . '/example.php', get_hashed_filename( $metadata ) );
}

/**
* Test that a missing type is treated as non-plugin metadata.
*
* This covers the operator precedence difference between
* `'wp-plugin' === ( $metadata->type ?? '' )` and
* `'wp-plugin' === $metadata->type ?? ''`.
*/
public function test_should_treat_missing_type_as_non_plugin_metadata() {
$metadata = (object) [
'id' => 'did:plc:example1234567890123456789',
'slug' => 'example',
'filename' => 'example/example.php',
];

$this->assertSame( 'example-' . get_did_hash( $metadata->id ), get_hashed_filename( $metadata ) );
}

/**
* Create a metadata document for testing.
*
* @param array $overrides Document overrides.
* @return MetadataDocument
*/
private function create_metadata_document( array $overrides ) : MetadataDocument {
$metadata = new MetadataDocument();
$metadata->id = 'did:plc:example1234567890123456789';
$metadata->type = 'wp-plugin';
$metadata->slug = 'example';
$metadata->filename = 'example/example.php';

foreach ( $overrides as $key => $value ) {
$metadata->{$key} = $value;
}

return $metadata;
}
}
Loading