Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 19, 2026

wp plugin install now accepts URLs to single PHP files (e.g., GitHub Gists), not just ZIP archives.

Example

wp plugin install --activate https://gist.githubusercontent.com/user/id/raw/hash/plugin.php
# Also supports shorter gist page URLs:
wp plugin install --activate https://gist.github.com/user/id

Implementation

  • Detection: Added is_php_file_url() helper to identify PHP file URLs (plugins only)
  • Installation: New install_from_php_file() method handles the download and installation flow
  • GitHub Gist Support:
    • Detects both raw URLs (gist.githubusercontent.com) and page URLs (gist.github.com)
    • For page URLs, fetches gist metadata from GitHub API to find PHP files
    • Automatically converts gist page URLs to raw file URLs
    • Supports first PHP file when gist contains multiple files
  • Security:
    • Validates filenames for path traversal attempts (no .., /, \)
    • Requires valid WordPress plugin headers (minimum: Plugin Name)
    • Verifies .php extension after sanitization
    • Checks file existence before downloading (avoids unnecessary requests)
  • Integration: Returns filename for proper activation handling, supports --force flag

Files Changed

  • src/WP_CLI/CommandWithUpgrade.php: Core implementation including gist URL handling
  • src/Plugin_Command.php: Updated documentation and examples
  • features/plugin-install.feature: Added test coverage for single PHP files, --force flag, and gist page URLs
Original prompt

This section details on the original issue you should resolve

<issue_title>Links to single plugin PHP file should work with plugin install</issue_title>
<issue_description>When using wp plugin install <URL>, WP-CLI currently assumes that the URL is pointing to a ZIP file of a plugin.

This command should also work with a URL to a single PHP plugin file, so that it can for example directly install plugins from GitHub Gists:

Example of such a command that should just work:

wp plugin install --activate https://gist.githubusercontent.com/westonruter/dec7d190060732e29a09751ab99cc549/raw/d55866c2fc82ab16f8909ce73fc89986ab28d727/pwa-manifest-short-name.php

/cc @westonruter (whose numerous Gist plugins make me want to implement the above)</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Add support for installing plugins from PHP file URLs Support installing plugins from single PHP file URLs Jan 19, 2026
Copilot AI requested a review from swissspidy January 19, 2026 10:20
@github-actions github-actions bot added command:plugin-install Related to 'plugin install' command scope:testing Related to testing labels Jan 19, 2026
@swissspidy
Copy link
Member

@copilot Make sure wp plugin install --activate https://gist.github.com/westonruter/dec7d190060732e29a09751ab99cc549 works too and add a test for that

@codecov
Copy link

codecov bot commented Jan 19, 2026

Codecov Report

❌ Patch coverage is 74.74747% with 25 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/WP_CLI/CommandWithUpgrade.php 74.74% 25 Missing ⚠️

📢 Thoughts on this report? Let us know!

@swissspidy

This comment was marked as resolved.

@swissspidy

This comment was marked as resolved.

This comment was marked as resolved.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enables wp plugin install to accept URLs pointing to single PHP files in addition to ZIP archives, with special support for GitHub Gist URLs.

Changes:

  • Added detection and installation logic for single PHP file URLs
  • Implemented GitHub Gist page URL resolution to raw file URLs via the GitHub API
  • Added comprehensive test coverage for PHP file installations, --force flag behavior, and Gist URL handling

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
src/WP_CLI/CommandWithUpgrade.php Core implementation with Gist URL detection, raw URL conversion via GitHub API, and PHP file installation with security validations
src/Plugin_Command.php Updated documentation to reflect support for PHP file URLs
features/plugin-install.feature Added test scenarios for PHP file installations, --force flag, and Gist page URLs

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@swissspidy swissspidy requested a review from Copilot January 19, 2026 17:05
@swissspidy
Copy link
Member

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a great new feature to install plugins from single PHP file URLs, including from GitHub Gists. The implementation is well-structured and includes good security practices like filename validation and path traversal checks. The feature to resolve Gist page URLs to raw file URLs is a nice touch for user experience.

I've found a couple of areas for improvement:

  • The Gist URL detection doesn't support anonymous gists.
  • The checks for .php file extensions are case-sensitive, which could lead to valid files being missed.

I've provided specific suggestions to address these points. Overall, this is a solid contribution.

Comment on lines +1166 to +1168
if ( preg_match( '#^https?://gist\.github\.com/[^/]+/([a-f0-9]+)/?$#i', $url, $matches ) ) {
return $matches[1];
}

Choose a reason for hiding this comment

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

high

The regular expression for detecting a Gist URL doesn't account for anonymous gists, which don't have a username in the URL path (e.g., https://gist.github.com/1234567890abcdef). The current regex requires a username segment, causing it to fail for anonymous gists. To support both user-owned and anonymous gists, you should make the username part of the URL path optional.

if ( preg_match( '#^https?://gist\.github\.com/(?:[^/]+/)?([a-f0-9]+)/?$#i', $url, $matches ) ) {
	return $matches[1];
}

Comment on lines +377 to +379
if ( pathinfo( $dest_filename, PATHINFO_EXTENSION ) !== 'php' ) {
return new WP_Error( 'invalid_filename', 'The sanitized filename does not have a .php extension.' );
}

Choose a reason for hiding this comment

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

medium

The check for the .php extension on the sanitized filename is case-sensitive because it uses pathinfo(). This means a file with an extension like .PHP would be incorrectly rejected. To make this check more robust, you should convert the extension to lowercase before comparison.

if ( strtolower( pathinfo( $dest_filename, PATHINFO_EXTENSION ) ) !== 'php' ) {
	return new WP_Error( 'invalid_filename', 'The sanitized filename does not have a .php extension.' );
}

}

$url_path = Utils\parse_url( $slug, PHP_URL_PATH );
return is_string( $url_path ) && pathinfo( $url_path, PATHINFO_EXTENSION ) === 'php';

Choose a reason for hiding this comment

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

medium

The check for the .php extension in the URL is case-sensitive. This could cause URLs with an uppercase .PHP extension to be missed. To ensure all valid PHP file URLs are detected, you should perform a case-insensitive comparison.

return is_string( $url_path ) && 'php' === strtolower( pathinfo( $url_path, PATHINFO_EXTENSION ) );

$php_files = [];
$files = (array) $decoded_body->files;
foreach ( $files as $filename => $file_data ) {
if ( is_object( $file_data ) && isset( $file_data->raw_url ) && pathinfo( $filename, PATHINFO_EXTENSION ) === 'php' ) {

Choose a reason for hiding this comment

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

medium

When searching for PHP files within a Gist, the check for the .php extension is case-sensitive. This means files with extensions like .PHP would be ignored. To ensure all PHP files in a Gist are found, you should convert the file extension to lowercase before comparing it.

if ( is_object( $file_data ) && isset( $file_data->raw_url ) && 'php' === strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ) ) {

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

$api_url = 'https://api.github.com/gists/' . $gist_id;
$token = getenv( 'GITHUB_TOKEN' );

$request_arguments = $token ? [ 'headers' => [ 'Authorization' => 'Bearer ' . $token ] ] : [];
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The authorization header format is inconsistent with the existing code. In get_the_latest_github_version() at line 1070, headers are passed as a string value, but here they are passed as an array. This inconsistency should be fixed to match the existing pattern used elsewhere in the codebase.

Suggested change
$request_arguments = $token ? [ 'headers' => [ 'Authorization' => 'Bearer ' . $token ] ] : [];
$request_arguments = $token ? [ 'headers' => 'Authorization: Bearer ' . $token ] : [];

Copilot uses AI. Check for mistakes.
Comment on lines +1194 to +1199
if ( 403 === $response_code ) {
return new \WP_Error(
'api_rate_limit',
$this->build_rate_limiting_error_message( $decoded_body ),
[ 'status' => 403 ]
);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

There's a potential null pointer issue when handling rate limiting errors. The build_rate_limiting_error_message() method expects an object with message and documentation_url properties, but if the decoded_body is null or malformed, this could cause a fatal error. This check should occur before calling build_rate_limiting_error_message().

Copilot uses AI. Check for mistakes.

// Ensure plugin directory exists.
if ( ! is_dir( WP_PLUGIN_DIR ) ) {
wp_mkdir_p( WP_PLUGIN_DIR );
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

After calling wp_mkdir_p() to ensure the plugin directory exists, there's no verification that the directory was successfully created. If wp_mkdir_p() fails, the subsequent realpath() checks could return false unexpectedly. Consider checking the result of is_dir(WP_PLUGIN_DIR) after the wp_mkdir_p() call to ensure the directory was created successfully.

Suggested change
wp_mkdir_p( WP_PLUGIN_DIR );
wp_mkdir_p( WP_PLUGIN_DIR );
// Verify that the plugin directory was successfully created.
if ( ! is_dir( WP_PLUGIN_DIR ) ) {
return new WP_Error( 'invalid_path', 'Unable to create plugin directory.' );
}

Copilot uses AI. Check for mistakes.
Comment on lines +1166 to +1167
if ( preg_match( '#^https?://gist\.github\.com/[^/]+/([a-f0-9]+)/?$#i', $url, $matches ) ) {
return $matches[1];
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The regex pattern uses a case-insensitive flag (i) but the character class [a-f0-9]+ may not match uppercase hexadecimal digits (A-F) in gist IDs. While the 'i' flag makes literal characters case-insensitive, character classes typically need explicit uppercase ranges. If GitHub gist IDs can contain uppercase letters, this pattern should be [a-fA-F0-9]+ or [0-9a-f]+ with case normalization of the gist ID before matching.

Copilot uses AI. Check for mistakes.
Comment on lines +1210 to +1216
if ( null === $decoded_body || ! is_object( $decoded_body ) || ! isset( $decoded_body->files ) ) {
return new \WP_Error(
'invalid_gist_api_response',
'Invalid response from GitHub Gist API.',
[ 'status' => 500 ]
);
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The error handling for HTTP response codes only explicitly handles 403 (rate limit) and 404 (not found), but other error codes like 401 (unauthorized), 500 (server error), or 503 (service unavailable) fall through to the generic "Invalid response from GitHub Gist API" error message. Consider adding more specific error handling for common HTTP error codes to provide clearer feedback to users.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

command:plugin-install Related to 'plugin install' command scope:testing Related to testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Links to single plugin PHP file should work with plugin install

2 participants