Skip to content
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

Only use version from plugin header. #93

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
}
],
"require": {
"php": ">=7.1",
"php": ">=7.4",
"wp-cli/wp-cli": "^2",
"inmarelibero/gitignore-checker": "^1.0.2"
},
Expand Down
38 changes: 38 additions & 0 deletions features/dist-archive.feature
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,41 @@ Feature: Generate a distribution archive of a project
"""
And STDERR should be empty
And the foo.zip file should exist

Scenario: Uses version from plugin header when multiple versions are present
Given an empty directory
And a lifterlms/.distignore file:
"""
"""
And a lifterlms/lifterlms.php file:
"""
<?php
/**
* Main LifterLMS plugin file
*
* @version 5.3.0
*
* Plugin Name: LifterLMS
* Version: 7.6.0
*/
"""
And a lifterlms/class-lifterlms.php file:
"""
<?php
/**
* Main LifterLMS class
*
* @package LifterLMS/Main
*
* @since 1.0.0
* @version 7.2.0
*/
"""

When I run `wp dist-archive lifterlms`
Then STDOUT should be:
"""
Success: Created lifterlms.7.6.0.zip
"""
And STDERR should be empty
And the lifterlms.7.6.0.zip file should exist
2 changes: 1 addition & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

<!-- For help understanding the `testVersion` configuration setting:
https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions -->
<config name="testVersion" value="5.6-"/>
<config name="testVersion" value="7.4-"/>

<!-- Verify that everything in the global namespace is either namespaced or prefixed.
See: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#naming-conventions-prefix-everything-in-the-global-namespace -->
Expand Down
124 changes: 5 additions & 119 deletions src/Dist_Archive_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class Dist_Archive_Command {
*/
private $checker;

private Version_Tool $version_tool;

/**
* Create a distribution archive based on a project's .distignore file.
*
Expand Down Expand Up @@ -73,6 +75,8 @@ public function __invoke( $args, $assoc_args ) {

$this->checker = new GitIgnoreChecker( $path, '.distignore' );

$this->version_tool = new Version_Tool();

if ( isset( $args[1] ) ) {
// If the end of the string is a filename (file.ext), use it for the output archive filename.
if ( 1 === preg_match( '/^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?\.[a-zA-Z0-9_-]+$/', basename( $args[1] ) ) ) {
Expand Down Expand Up @@ -114,49 +118,7 @@ public function __invoke( $args, $assoc_args ) {
$source_base = basename( $path );
$archive_base = isset( $assoc_args['plugin-dirname'] ) ? rtrim( $assoc_args['plugin-dirname'], '/' ) : $source_base;

$version = '';

/**
* If the path is a theme (meaning it contains a style.css file)
* parse the theme's version from the headers using a regex pattern.
* The pattern used is extracted from the get_file_data() function in core.
*
* @link https://developer.wordpress.org/reference/functions/get_file_data/
*/
if ( file_exists( $path . '/style.css' ) ) {
$contents = file_get_contents( $path . '/style.css', false, null, 0, 5000 );
$contents = str_replace( "\r", "\n", $contents );
$pattern = '/^' . preg_quote( 'Version', ',' ) . ':(.*)$/mi';
if ( preg_match( $pattern, $contents, $match ) && $match[1] ) {
$version = trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $match[1] ) );
}
}

if ( empty( $version ) ) {
foreach ( glob( $path . '/*.php' ) as $php_file ) {
$contents = file_get_contents( $php_file, false, null, 0, 5000 );
$version = $this->get_version_in_code( $contents );
if ( ! empty( $version ) ) {
$version = trim( $version );
break;
}
}
}

if ( empty( $version ) && file_exists( $path . '/composer.json' ) ) {
$composer_obj = json_decode( file_get_contents( $path . '/composer.json' ) );
if ( ! empty( $composer_obj->version ) ) {
$version = trim( $composer_obj->version );
}
}

if ( ! empty( $version ) && false !== stripos( $version, '-alpha' ) && is_dir( $path . '/.git' ) ) {
$response = WP_CLI::launch( "cd {$path}; git log --pretty=format:'%h' -n 1", false, true );
$maybe_hash = trim( $response->stdout );
if ( $maybe_hash && 7 === strlen( $maybe_hash ) ) {
$version .= '-' . $maybe_hash;
}
}
$version = $this->version_tool->get_version( $path );

if ( $archive_base !== $source_base || $this->is_path_contains_symlink( $path ) ) {
$tmp_dir = sys_get_temp_dir() . '/' . uniqid( "{$archive_base}.{$version}" );
Expand Down Expand Up @@ -279,82 +241,6 @@ private function maybe_create_directory( $archive_file ) {
}
}

/**
* Gets the content of a version tag in any doc block in the given source code string.
*
* The version tag might be specified as "@version x.y.z" or "Version: x.y.z" and it can
* be preceded by an asterisk (*).
*
* @param string $code_str The source code string to look into.
* @return null|string The detected version string.
*/
private function get_version_in_code( $code_str ) {
$tokens = array_values(
array_filter(
token_get_all( $code_str ),
function ( $token ) {
return ! is_array( $token ) || T_WHITESPACE !== $token[0];
}
)
);
foreach ( $tokens as $token ) {
if ( T_DOC_COMMENT === $token[0] ) {
$version = $this->get_version_in_docblock( $token[1] );
if ( null !== $version ) {
return $version;
}
}
}
return null;
}

/**
* Gets the content of a version tag in a docblock.
*
* @param string $docblock Docblock to parse.
* @return null|string The content of the version tag.
*/
private function get_version_in_docblock( $docblock ) {
$docblocktags = $this->parse_doc_block( $docblock );
if ( isset( $docblocktags['version'] ) ) {
return $docblocktags['version'];
}
return null;
}

/**
* Parses a docblock and gets an array of tags with their values.
*
* The tags might be specified as "@version x.y.z" or "Version: x.y.z" and they can
* be preceded by an asterisk (*).
*
* This code is based on the 'phpactor' package.
* @see https://github.com/phpactor/docblock/blob/master/lib/Parser.php
*
* @param string $docblock Docblock to parse.
* @return array Associative array of parsed data.
*/
private function parse_doc_block( $docblock ) {
$tag_documentor = '{@([a-zA-Z0-9-_\\\]+)\s*?(.*)?}';
$tag_property = '{\s*\*?\s*(.*?):(.*)}';
$lines = explode( PHP_EOL, $docblock );
$tags = [];

foreach ( $lines as $line ) {
if ( 0 === preg_match( $tag_documentor, $line, $matches ) ) {
if ( 0 === preg_match( $tag_property, $line, $matches ) ) {
continue;
}
}

$tag_name = strtolower( $matches[1] );
$metadata = trim( isset( $matches[2] ) ? $matches[2] : '' );

$tags[ $tag_name ] = $metadata;
}
return $tags;
}

/**
* Run PHP's escapeshellcmd() then undo escaping known intentional characters.
*
Expand Down
119 changes: 119 additions & 0 deletions src/Version_Tool.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

class Version_Tool {

public function get_version( string $path ): ?string {

$version = '';

/**
* If the path is a theme (meaning it contains a style.css file)
* parse the theme's version from the headers using a regex pattern.
* The pattern used is extracted from the get_file_data() function in core.
*
* @link https://developer.wordpress.org/reference/functions/get_file_data/
*/
if ( file_exists( $path . '/style.css' ) ) {
$contents = file_get_contents( $path . '/style.css', false, null, 0, 5000 );
$contents = str_replace( "\r", "\n", $contents );
$pattern = '/^' . preg_quote( 'Version', ',' ) . ':(.*)$/mi';
if ( preg_match( $pattern, $contents, $match ) && $match[1] ) {
$version = trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $match[1] ) );
}
}

if ( empty( $version ) ) {
foreach ( glob( $path . '/*.php' ) as $php_file ) {
$headers = $this->get_file_data(
$php_file,
array(
'name' => 'Plugin Name',
'version' => 'Version',
)
);
if ( empty( $headers['name'] ) ) {
continue;
}
if ( ! empty( $headers['version'] ) ) {
$version = $headers['version'];
break;
}
}
}

if ( empty( $version ) && file_exists( $path . '/composer.json' ) ) {
$composer_obj = json_decode( file_get_contents( $path . '/composer.json' ) );
if ( ! empty( $composer_obj->version ) ) {
$version = trim( $composer_obj->version );
}
}

if ( ! empty( $version ) && false !== stripos( $version, '-alpha' ) && is_dir( $path . '/.git' ) ) {
$response = WP_CLI::launch( "cd {$path}; git log --pretty=format:'%h' -n 1", false, true );
$maybe_hash = trim( $response->stdout );
if ( $maybe_hash && 7 === strlen( $maybe_hash ) ) {
$version .= '-' . $maybe_hash;
}
}

return $version;
}

/**
* Retrieves metadata from a file.
*
* Modified slightly from WordPress 6.5.2 wp-includes/functions.php:6830
* @see get_file_data()
* @see https://github.com/WordPress/WordPress/blob/ddc3f387b5df4687f5b829119d0c0f797be674bf/wp-includes/functions.php#L6830-L6888
*
* Searches for metadata in the first 8 KB of a file, such as a plugin or theme.
* Each piece of metadata must be on its own line. Fields can not span multiple
* lines, the value will get cut at the end of the first line.
*
* @link https://codex.wordpress.org/File_Header
*
* @param string $file Absolute path to the file.
* @param array $all_headers List of headers, in the format `array( 'HeaderKey' => 'Header Name' )`.
* @return string[] Array of file header values keyed by header name.
*/
private function get_file_data( string $file, array $all_headers ): array {

/**
* @see wp_initial_constants()
* `define( 'KB_IN_BYTES', 1024 );`
*/
$kb_in_bytes = 1024;

// Pull only the first 8 KB of the file in.
$file_data = file_get_contents( $file, false, null, 0, 8 * $kb_in_bytes );

if ( false === $file_data ) {
$file_data = '';
}

// Make sure we catch CR-only line endings.
$file_data = str_replace( "\r", "\n", $file_data );

/**
* Strips close comment and close php tags from file headers used by WP.
*
* functions.php:6763
*
* @param string $str Header comment to clean up.
* @return string
*/
$_cleanup_header_comment = function ( $str ) {
return trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $str ) );
};

foreach ( $all_headers as $field => $regex ) {
if ( preg_match( '/^(?:[ \t]*<\?php)?[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) {
$all_headers[ $field ] = $_cleanup_header_comment( $match[1] );
} else {
$all_headers[ $field ] = '';
}
}

return $all_headers;
}
}