-
Notifications
You must be signed in to change notification settings - Fork 85
Support installing plugins from single PHP file URLs #486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Co-authored-by: swissspidy <[email protected]>
Co-authored-by: swissspidy <[email protected]>
Co-authored-by: swissspidy <[email protected]>
…oad message timing Co-authored-by: swissspidy <[email protected]>
…plugin headers Co-authored-by: swissspidy <[email protected]>
…dation Co-authored-by: swissspidy <[email protected]>
|
@copilot Make sure |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
Co-authored-by: swissspidy <[email protected]>
This comment was marked as resolved.
This comment was marked as resolved.
There was a problem hiding this 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,
--forceflag 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.
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
|
/gemini review |
There was a problem hiding this 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
.phpfile 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.
| if ( preg_match( '#^https?://gist\.github\.com/[^/]+/([a-f0-9]+)/?$#i', $url, $matches ) ) { | ||
| return $matches[1]; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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];
}| if ( pathinfo( $dest_filename, PATHINFO_EXTENSION ) !== 'php' ) { | ||
| return new WP_Error( 'invalid_filename', 'The sanitized filename does not have a .php extension.' ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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' ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 ) ) ) {There was a problem hiding this 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 ] ] : []; |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
| $request_arguments = $token ? [ 'headers' => [ 'Authorization' => 'Bearer ' . $token ] ] : []; | |
| $request_arguments = $token ? [ 'headers' => 'Authorization: Bearer ' . $token ] : []; |
| if ( 403 === $response_code ) { | ||
| return new \WP_Error( | ||
| 'api_rate_limit', | ||
| $this->build_rate_limiting_error_message( $decoded_body ), | ||
| [ 'status' => 403 ] | ||
| ); |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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().
|
|
||
| // Ensure plugin directory exists. | ||
| if ( ! is_dir( WP_PLUGIN_DIR ) ) { | ||
| wp_mkdir_p( WP_PLUGIN_DIR ); |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
| 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.' ); | |
| } |
| if ( preg_match( '#^https?://gist\.github\.com/[^/]+/([a-f0-9]+)/?$#i', $url, $matches ) ) { | ||
| return $matches[1]; |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
| 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 ] | ||
| ); | ||
| } |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
wp plugin installnow 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/idImplementation
is_php_file_url()helper to identify PHP file URLs (plugins only)install_from_php_file()method handles the download and installation flowgist.githubusercontent.com) and page URLs (gist.github.com)..,/,\).phpextension after sanitization--forceflagFiles Changed
src/WP_CLI/CommandWithUpgrade.php: Core implementation including gist URL handlingsrc/Plugin_Command.php: Updated documentation and examplesfeatures/plugin-install.feature: Added test coverage for single PHP files,--forceflag, and gist page URLsOriginal prompt
plugin install#233💡 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.