oficinasuport-wp-theme/vendor/wptrt/wpthemereview/WPThemeReview/Sniffs/Plugins/CorrectTGMPAVersionSniff.php

547 lines
19 KiB
PHP

<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\Plugins;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* WordPress_Sniffs_Theme_CorrectTGMPAVersionSniff.
*
* Verifies that if the TGM Plugin Activation library is included, the correct version is used.
* - Check whether the version included is up to date.
* - Check whether the version included is downloaded via the Custom Generator with
* the correct settings.
* - Check against a persistent manual search & replace error made by theme authors.
*
* @link https://make.wordpress.org/themes/handbook/review/...... @todo
*
* @since 0.1.0
*
* {@internal This sniff currently has not (yet) been detached from the WordPressCS\WordPress\Sniff class
* as the `detect_manual_editing()` method uses the WPCS `get_function_call_parameter()` and
* `strip_quotes()` methods.
* If/when WPCS re-organizes the generic methods into traits as is being discussed in
* {@link https://github.com/WordPress/WordPress-Coding-Standards/issues/1465},
* this sniff could be decoupled and use the trait(s) instead.}}
*/
class CorrectTGMPAVersionSniff extends Sniff {
const GITHUB_TGMPA_API_URL = 'https://api.github.com/repos/TGMPA/TGM-Plugin-Activation/releases/latest';
const GITHUB_API_OAUTH_QUERY = '?access_token=%s';
/**
* TGMPA flavour required.
*
* By default, any flavour is ok. If this property is set in a custom ruleset an additional
* check will be run to verify if the correct flavour of TGMPA is used.
*
* Note: The Custom Generator only has flavours for themes, so this property is only
* relevant when checking a theme.
*
* Valid values:
* - 'wporg'
* - 'themeforest'
*
* @var string
*/
public $required_flavour = '';
/**
* GitHub oAuth token.
*
* Intended to be set in the ruleset.
*
* This is to prevent issues with rate limiting if a lot of requests are made from the same server.
*
* "Normal" users generaly won't need to set this, but if the sniffs are run for all themes
* uploaded to wordpress.org, that IP address might run into the rate limit of 60 calls per hour.
* Setting a oauth token in the custom ruleset used will prevent this.
*
* Alternatively, the token can also be set via an environment key called `GITHUB_OAUTH_TOKEN`.
*
* @var string
*/
public $github_oauth_token = '';
/**
* Fall-back for the latest version number for if the API call fails.
*
* @var string
*/
private $current_version = '2.6.1';
/**
* Whether or not a call has been made to the GitHub API to retrieve the current TGMPA version number.
*
* Note: an API call will only be made once per PHPCS run and only when the TGMPA library
* has been positively identified.
* The API call will be skipped when running the unit tests.
*
* @var bool
*/
private $gh_call_made = false;
/**
* Classes and functions declared by TGMPA.
*
* @var array
*/
private $tgmpa_classes_functions = [
// Classes.
'TGM_Plugin_Activation' => true,
'TGMPA_List_Table' => true,
'TGM_Bulk_Installer' => true,
'TGM_Bulk_Installer_Skin' => true,
'TGMPA_Bulk_Installer' => true,
'TGMPA_Bulk_Installer_Skin' => true,
'TGMPA_Utils' => true,
// Functions.
'load_tgm_plugin_activation' => true,
'tgmpa_initialize' => true, // New in v 2.6.2.
'tgmpa' => true,
'tgmpa_load_bulk_installer' => true,
];
/**
* List of available TGMPA flavours - other than the default.
*
* Used to validate `$required_flavour` property.
*
* Keys are the allowed values for the flavour property.
* Value is how this is expanded in the @version tag used by TGMPA.
*
* @var array
*/
private $valid_flavours = [
'wporg' => 'WordPress.org',
'themeforest' => 'ThemeForest',
];
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return [
T_OPEN_TAG,
];
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
$has_class_function = $this->phpcsFile->findNext( [ T_CLASS, T_FUNCTION ], ( $stackPtr + 1 ) );
if ( false === $has_class_function ) {
// No class, function or constant declaration found, definitely not TGMPA file.
// Skip this file from further checks.
return ( $this->phpcsFile->numTokens + 1 );
}
$is_tgmpa = false;
// First check based on filename.
$file_name = basename( $this->phpcsFile->getFileName() );
if ( false !== $file_name ) {
$file_name = strtolower( $file_name );
if ( strpos( $file_name, 'class-tgm-plugin-activation.php' ) !== false ) {
$is_tgmpa = true;
} elseif ( defined( 'PHP_CODESNIFFER_IN_TESTS' ) && preg_match( '`class-tgm-plugin-activation[^.]+\.inc`', $file_name, $discard ) > 0 ) {
$is_tgmpa = true;
}
}
/*
* Otherwise, check whether any of the TGMPA classes or function names are encountered.
*
* Will detect TGMPA, even when:
* - the class and function prefix has been changed
* - the file has been split up into several files
* - the file is combined with other code
*/
if ( false === $is_tgmpa ) {
while ( false !== $has_class_function ) {
$name = $this->phpcsFile->getDeclarationName( $has_class_function );
if ( ! empty( $name ) ) {
if ( isset( $this->tgmpa_classes_functions[ $name ] ) ) {
$is_tgmpa = true;
break;
} elseif ( strpos( $name, '_Plugin_Activation' ) !== false ) {
// This may be TGMPA with renamed prefixes, so look for typical class comment.
$prev = $this->phpcsFile->findPrevious( T_WHITESPACE, ( $has_class_function - 1 ), null, true );
while ( false !== $prev && isset( Tokens::$commentTokens[ $this->tokens[ $prev ]['code'] ] ) ) {
if ( ( T_COMMENT === $this->tokens[ $prev ]['code']
|| T_DOC_COMMENT_STRING === $this->tokens[ $prev ]['code'] )
&& ( strpos( $this->tokens[ $prev ]['content'], 'Automatic plugin installation and activation library' ) !== false
|| strpos( $this->tokens[ $prev ]['content'], 'Automatic plugin installation and activation class' ) !== false )
) {
$is_tgmpa = true;
break;
}
$prev = $this->phpcsFile->findPrevious( T_WHITESPACE, ( $prev - 1 ), null, true );
}
}
}
$start = ( $has_class_function + 1 );
if ( isset( $this->tokens[ $has_class_function ]['scope_condition'], $this->tokens[ $stackPtr ]['scope_closer'] )
&& $this->tokens[ $has_class_function ]['scope_condition'] === $has_class_function
) {
// Skip past anything within the class or function.
$start = $this->tokens[ $stackPtr ]['scope_closer'];
}
$has_class_function = $this->phpcsFile->findNext( [ T_CLASS, T_FUNCTION ], $start );
}
}
// If we're still not 100% sure this is TGMPA, exclude the file from further checks.
if ( false === $is_tgmpa ) {
return ( $this->phpcsFile->numTokens + 1 );
}
// Check whether the correct version of TGMPA is being used.
$version = $this->uses_latest_version();
// Check for typical manual search & replace error.
$this->detect_manual_editing( $version );
// No need to check the same file again.
return ( $this->phpcsFile->numTokens + 1 );
}
/**
* Check whether the latest version of TGMPA is being used.
*/
protected function uses_latest_version() {
if ( false === $this->gh_call_made ) {
// Get the current version number for TGMPA from GitHub.
$this->update_current_version();
$this->gh_call_made = true;
}
/*
* Walk the doc block comments to find if this is the correct version of TGMPA.
* Normally this will be in the first doc block encountered, so this is not as 'heavy' as it looks.
*/
$next_doc_block = 0;
$version = false;
$pcre_esc_flavours = array_map(
'preg_quote',
$this->valid_flavours,
array_fill( 0, count( $this->valid_flavours ), '`' )
);
$pcre_flavours = implode( '|', $pcre_esc_flavours );
do {
$next_doc_block = $this->phpcsFile->findNext( T_DOC_COMMENT_OPEN_TAG, ( $next_doc_block + 1 ) );
if ( false === $next_doc_block ) {
break;
}
$tags = $this->get_docblock_tags( $next_doc_block );
if ( empty( $tags ) ) {
continue;
}
if ( ! isset( $tags['package'], $tags['version'] ) || 'TGM-Plugin-Activation' !== $tags['package'] ) {
continue;
}
if ( preg_match( '`^([0-9\.]+(?:-(?:alpha|beta|RC)(?:[0-9])?)?)`', $tags['version'], $matches ) > 0 ) {
$version = $matches[1];
$this->phpcsFile->recordMetric( 0, 'Version', $version );
if ( true === version_compare( $this->current_version, $version, '>' ) ) {
$error = 'Upgrade of the included TGM plugin activation library required. Current version: %s. Found version: %s';
$data = [
$this->current_version,
$version,
];
$this->phpcsFile->addError( $error, 0, 'upgradeRequired', $data );
} elseif ( true === version_compare( $this->current_version, $version, '<' ) ) {
$error = 'Non-stable version of the TGM plugin activation library found. The current version is %s. Found version: %s';
$data = [
$this->current_version,
$version,
];
$this->phpcsFile->addError( $error, 0, 'unstableVersion', $data );
}
unset( $matches, $error, $data );
if ( strpos( $tags['version'], 'for parent theme' ) !== false ) {
$this->phpcsFile->recordMetric( 0, 'Used in', 'parent theme' );
} elseif ( strpos( $tags['version'], 'for child theme' ) !== false ) {
$this->phpcsFile->recordMetric( 0, 'Used in', 'child theme' );
} else {
$this->phpcsFile->recordMetric( 0, 'Used in', 'unknown' );
}
if ( preg_match( '`for publication on (' . $pcre_flavours . ')`i', $tags['version'], $flavour_match ) > 0 ) {
$this->phpcsFile->recordMetric( 0, 'Publication Channel', $flavour_match[1] );
} else {
$this->phpcsFile->recordMetric( 0, 'Publication Channel', 'n/a' );
}
if ( ! empty( $this->required_flavour ) && isset( $this->valid_flavours[ $this->required_flavour ] ) ) {
$flavour_phrase = sprintf( 'for publication on %s', $this->valid_flavours[ $this->required_flavour ] );
if ( strpos( $tags['version'], $flavour_phrase ) === false ) {
$warning = 'You are required to use a version of the TGM Plugin Activation library downloaded through the Custom TGMPA Generator. Download a fresh copy and make sure you select "%s" as your publication channel to get the correct version. http://tgmpluginactivation.com/download/';
$this->phpcsFile->addWarning(
$warning,
0,
'wrongVersion',
[ $this->valid_flavours[ $this->required_flavour ] ]
);
}
}
}
break;
} while ( false !== $next_doc_block );
// The file was recognized as TGMPA, but no valid file doc block for TGMPA was found.
if ( false === $version ) {
$this->phpcsFile->recordMetric( 0, 'Version', 'unknown' );
$error = 'The TGM Plugin Activation library was detected, but the version could not be determined. Ensure you use the latest stable release of the TGM Plugin Activation library (%s). Download a fresh copy now using the Custom TGMPA Generator at http://tgmpluginactivation.com/download/';
$data = [ $this->current_version ];
$this->phpcsFile->addError( $error, 0, 'versionUndetermined', $data );
$has_error = true;
}
return $version;
}
/**
* Check against a typical manual search & replace error often encountered.
*
* In that case the `tgmpa` in `if ( ! function_exists( 'tgmpa' ) )` has been replaced
* with the theme slug causing fatal errors when an end-user also uses a plugin using TGMPA.
*
* @param string $version The version of TGMPA found.
*/
protected function detect_manual_editing( $version ) {
// Skip this check for TGMPA versions which didn't have the `tgmpa()` function or
// didn't have the `function_exists()` wrapper.
if ( false === $version || true === version_compare( $version, '2.2.0', '<' ) ) {
return;
}
$checkTokens = [
// This is what we're looking for.
T_FUNCTION => true,
// These are just here to be able to skip as much as we can.
T_CLASS => true,
T_ARRAY => true,
T_OPEN_SHORT_ARRAY => true,
];
for ( $ptr = 0; $ptr < $this->phpcsFile->numTokens; $ptr++ ) {
if ( ! isset( $checkTokens[ $this->tokens[ $ptr ]['code'] ] ) ) {
continue;
}
// Skip as much as we can.
if ( T_CLASS === $this->tokens[ $ptr ]['code'] && isset( $this->tokens[ $ptr ]['scope_closer'] ) ) {
$ptr = $this->tokens[ $ptr ]['scope_closer'];
continue;
} elseif ( T_OPEN_SHORT_ARRAY === $this->tokens[ $ptr ]['code'] && isset( $this->tokens[ $ptr ]['bracket_closer'] ) ) {
$ptr = $this->tokens[ $ptr ]['bracket_closer'];
continue;
} elseif ( T_ARRAY === $this->tokens[ $ptr ]['code'] && isset( $this->tokens[ $ptr ]['parenthesis_closer'] ) ) {
$ptr = $this->tokens[ $ptr ]['parenthesis_closer'];
continue;
}
// Detect whether this is the `tgmpa()` function declaration.
if ( T_FUNCTION === $this->tokens[ $ptr ]['code'] ) {
$function_name = $this->phpcsFile->getDeclarationName( $ptr );
if ( 'tgmpa' !== $function_name ) {
if ( isset( $this->tokens[ $ptr ]['scope_closer'] ) ) {
// Skip the rest of the function.
$ptr = $this->tokens[ $ptr ]['scope_closer'];
}
continue;
}
// Ok, found the tgmpa function declaration. Now let's check for the typical
// manual text-domain replacement error.
$function_exists = $this->phpcsFile->findPrevious( T_STRING, ( $ptr - 1 ), null, false, 'function_exists' );
$param = false;
if ( false !== $function_exists ) {
$param = $this->get_function_call_parameter( $function_exists, 1 );
$param = $this->strip_quotes( $param['raw'] );
}
if ( false === $function_exists || 'tgmpa' !== $param ) {
$this->phpcsFile->recordMetric( 0, 'Manual editing detected', 'yes' );
$this->phpcsFile->addError(
'Manual editing of the TGM Plugin Activation file detected. Your edit will cause fatal errors for end-users. Download an official copy using the Custom TGMPA Generator. http://tgmpluginactivation.com/download/',
0,
'ManualEditDetected',
[],
9
);
} else {
$this->phpcsFile->recordMetric( 0, 'Manual editing detected', 'no' );
}
break;
}
}
}
/**
* Retrieve an array with the doc block tags from a T_DOC_COMMENT_OPEN_TAG.
*
* @param int $comment_opener The position of the comment opener.
*
* @return array
*/
protected function get_docblock_tags( $comment_opener ) {
$tags = [];
$opener = $this->tokens[ $comment_opener ];
if ( ! isset( $opener['comment_tags'] ) ) {
return $tags;
}
$closer = null;
if ( isset( $opener['comment_closer'] ) ) {
$closer = $opener['comment_closer'];
}
$tag_count = count( $opener['comment_tags'] );
for ( $i = 0; $i < $tag_count; $i++ ) {
$tag_token = $opener['comment_tags'][ $i ];
$tag = trim( $this->tokens[ $tag_token ]['content'], '@' );
$search_end = $closer;
if ( ( $i + 1 ) < $tag_count ) {
$search_end = $opener['comment_tags'][ ( $i + 1 ) ];
}
$value_token = $this->phpcsFile->findNext( T_DOC_COMMENT_STRING, ( $tag_token + 1 ), $search_end );
$tags[ $tag ] = trim( $this->tokens[ $value_token ]['content'] );
unset( $tag_token, $tag, $search_end, $value );
}
return $tags;
}
/**
* Get the version number (tag_name) of the latest TGMPA release from the GitHub API.
*/
protected function update_current_version() {
if ( defined( 'PHP_CODESNIFFER_IN_TESTS' ) || true === $this->gh_call_made ) {
return;
}
$api_url = self::GITHUB_TGMPA_API_URL;
$oauth_token = false;
if ( '' !== $this->github_oauth_token && is_string( $this->github_oauth_token ) ) {
$oauth_token = $this->github_oauth_token;
} elseif ( false !== getenv( 'GITHUB_OAUTH_TOKEN' ) ) {
$oauth_token = getenv( 'GITHUB_OAUTH_TOKEN' );
}
if ( false !== $oauth_token ) {
$api_url .= sprintf( self::GITHUB_API_OAUTH_QUERY, $oauth_token );
}
$stream_options = [
'http' => [
'method' => 'GET',
'user_agent' => 'WordPress-Coding-Standards/Theme-Review-Sniffs',
'protocol_version' => 1.1,
],
];
$stream_context = stream_context_create( $stream_options );
$response = file_get_contents( $api_url, false, $stream_context );
$headers = $this->parse_response_headers( $http_response_header );
// Check for invalid oAuth token response.
if ( 401 === $headers['response_code'] && false !== $oauth_token ) {
$this->phpcsFile->addWarning(
'The GITHUB_OAUTH_TOKEN you provided is invalid. Please update the token in your custom ruleset or environment properties.',
0,
'githubOauthTokenInvalid'
);
$this->oauth_error = false;
return;
}
// Check for rate limit error response.
if ( 403 === $headers['response_code'] && '0' === $headers['X-RateLimit-Remaining'] ) {
// @todo Add link to GH wiki page documenting the properties.
$this->phpcsFile->addWarning(
'You are running PHPCS more than 60 times per hour. You may want to consider setting the `github_oauth_token` property in your custom ruleset for Theme Review. For more information see: ... (GH wiki page).',
0,
'githubRateLimitReached'
);
$this->rate_limit_error = false;
return;
}
if ( 200 !== $headers['response_code'] ) {
// Something unexpected going on, just ignore it.
return;
}
// Ok, we have received a valid response.
$response = json_decode( $response );
if ( ! empty( $response->tag_name ) && ( ! isset( $response->prerelease ) || false === $response->prerelease ) ) {
// Should there be a check for `v` at the start of a version number ?
$this->current_version = $response->tag_name;
}
}
/**
* Parse HTTP response headers array to a more usable format.
*
* Based on http://php.net/manual/en/reserved.variables.httpresponseheader.php#117203
*
* @param array $headers HTTP response headers array.
*
* @return array
*/
private function parse_response_headers( $headers ) {
$head = [];
foreach ( $headers as $key => $value ) {
$tag = explode( ':', $value, 2 );
if ( isset( $tag[1] ) ) {
$head[ trim( $tag[0] ) ] = trim( $tag[1] );
} else {
$head[] = $value;
if ( preg_match( '`HTTP/[0-9\.]+\s+([0-9]+)`', $value, $out ) ) {
$head['response_code'] = intval( $out[1] );
}
}
}
return $head;
}
}