基础代码

This commit is contained in:
2021-02-26 22:23:13 +08:00
parent 7884df52f0
commit a719feebba
2585 changed files with 328263 additions and 0 deletions
@@ -0,0 +1,316 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Discourages the use of various functions and suggests (WordPress) alternatives.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.11.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 - Takes the minimum supported WP version into account.
* - Takes exceptions based on passed parameters into account.
*
* @uses \WordPressCS\WordPress\Sniff::$minimum_supported_version
*/
class AlternativeFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Local input streams which should not be flagged for the file system function checks.
*
* @link http://php.net/manual/en/wrappers.php.php
*
* @var array
*/
protected $allowed_local_streams = array(
'php://input' => true,
'php://output' => true,
'php://stdin' => true,
'php://stdout' => true,
'php://stderr' => true,
);
/**
* Local input streams which should not be flagged for the file system function checks if
* the $filename starts with them.
*
* @link http://php.net/manual/en/wrappers.php.php
*
* @var array
*/
protected $allowed_local_stream_partials = array(
'php://temp/',
'php://fd/',
);
/**
* Local input stream constants which should not be flagged for the file system function checks.
*
* @link http://php.net/manual/en/wrappers.php.php
*
* @var array
*/
protected $allowed_local_stream_constants = array(
'STDIN' => true,
'STDOUT' => true,
'STDERR' => true,
);
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'since' => '4.9.0', //=> the WP version in which the alternative became available.
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'curl' => array(
'type' => 'warning',
'message' => 'Using cURL functions is highly discouraged. Use wp_remote_get() instead.',
'since' => '2.7.0',
'functions' => array(
'curl_*',
),
'whitelist' => array(
'curl_version' => true,
),
),
'parse_url' => array(
'type' => 'warning',
'message' => '%s() is discouraged because of inconsistency in the output across PHP versions; use wp_parse_url() instead.',
'since' => '4.4.0',
'functions' => array(
'parse_url',
),
),
'json_encode' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use wp_json_encode() instead.',
'since' => '4.1.0',
'functions' => array(
'json_encode',
),
),
'file_get_contents' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use wp_remote_get() for remote URLs instead.',
'since' => '2.7.0',
'functions' => array(
'file_get_contents',
),
),
'file_system_read' => array(
'type' => 'warning',
'message' => 'File operations should use WP_Filesystem methods instead of direct PHP filesystem calls. Found: %s()',
'since' => '2.5.0',
'functions' => array(
'readfile',
'fclose',
'fopen',
'fread',
'fwrite',
'file_put_contents',
'fsockopen',
'pfsockopen',
),
),
'strip_tags' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use the more comprehensive wp_strip_all_tags() instead.',
'since' => '2.9.0',
'functions' => array(
'strip_tags',
),
),
'rand_seeding' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Rand seeding is not necessary when using the wp_rand() function (as you should).',
'since' => '2.6.2',
'functions' => array(
'srand',
'mt_srand',
),
),
'rand' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use the far less predictable wp_rand() instead.',
'since' => '2.6.2',
'functions' => array(
'rand',
'mt_rand',
),
),
);
}
/**
* Process a matched token.
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_matched_token( $stackPtr, $group_name, $matched_content ) {
$this->get_wp_version_from_cl();
/*
* Deal with exceptions.
*/
switch ( $matched_content ) {
case 'strip_tags':
/*
* The function `wp_strip_all_tags()` is only a valid alternative when
* only the first parameter is passed to `strip_tags()`.
*/
if ( $this->get_function_call_parameter_count( $stackPtr ) !== 1 ) {
return;
}
break;
case 'wp_parse_url':
/*
* Before WP 4.7.0, the function `wp_parse_url()` was only a valid alternative
* if no second param was passed to `parse_url()`.
*
* @see https://developer.wordpress.org/reference/functions/wp_parse_url/#changelog
*/
if ( $this->get_function_call_parameter_count( $stackPtr ) !== 1
&& version_compare( $this->minimum_supported_version, '4.7.0', '<' )
) {
return;
}
break;
case 'file_get_contents':
/*
* Using `wp_remote_get()` will only work for remote URLs.
* See if we can determine is this function call is for a local file and if so, bow out.
*/
$params = $this->get_function_call_parameters( $stackPtr );
if ( isset( $params[2] ) && 'true' === $params[2]['raw'] ) {
// Setting `$use_include_path` to `true` is only relevant for local files.
return;
}
if ( isset( $params[1] ) === false ) {
// If the file to get is not set, this is a non-issue anyway.
return;
}
if ( strpos( $params[1]['raw'], 'http:' ) !== false
|| strpos( $params[1]['raw'], 'https:' ) !== false
) {
// Definitely a URL, throw notice.
break;
}
if ( preg_match( '`\b(?:ABSPATH|WP_(?:CONTENT|PLUGIN)_DIR|WPMU_PLUGIN_DIR|TEMPLATEPATH|STYLESHEETPATH|(?:MU)?PLUGINDIR)\b`', $params[1]['raw'] ) === 1 ) {
// Using any of the constants matched in this regex is an indicator of a local file.
return;
}
if ( preg_match( '`(?:get_home_path|plugin_dir_path|get_(?:stylesheet|template)_directory|wp_upload_dir)\s*\(`i', $params[1]['raw'] ) === 1 ) {
// Using any of the functions matched in the regex is an indicator of a local file.
return;
}
if ( $this->is_local_data_stream( $params[1]['raw'] ) === true ) {
// Local data stream.
return;
}
unset( $params );
break;
case 'readfile':
case 'fopen':
case 'file_put_contents':
/*
* Allow for handling raw data streams from the request body.
*/
$first_param = $this->get_function_call_parameter( $stackPtr, 1 );
if ( false === $first_param ) {
// If the file to work with is not set, local data streams don't come into play.
break;
}
if ( $this->is_local_data_stream( $first_param['raw'] ) === true ) {
// Local data stream.
return;
}
unset( $first_param );
break;
}
if ( ! isset( $this->groups[ $group_name ]['since'] ) ) {
return parent::process_matched_token( $stackPtr, $group_name, $matched_content );
}
// Verify if the alternative is available in the minimum supported WP version.
if ( version_compare( $this->groups[ $group_name ]['since'], $this->minimum_supported_version, '<=' ) ) {
return parent::process_matched_token( $stackPtr, $group_name, $matched_content );
}
}
/**
* Determine based on the "raw" parameter value, whether a file parameter points to
* a local data stream.
*
* @param string $raw_param_value Raw parameter value.
*
* @return bool True if this is a local data stream. False otherwise.
*/
protected function is_local_data_stream( $raw_param_value ) {
$raw_stripped = $this->strip_quotes( $raw_param_value );
if ( isset( $this->allowed_local_streams[ $raw_stripped ] )
|| isset( $this->allowed_local_stream_constants[ $raw_param_value ] )
) {
return true;
}
foreach ( $this->allowed_local_stream_partials as $partial ) {
if ( strpos( $raw_stripped, $partial ) === 0 ) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,292 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Capital P Dangit!
*
* Verify the correct spelling of `WordPress` in text strings, comments and class names.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.12.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class CapitalPDangitSniff extends Sniff {
/**
* Regex to match a large number or spelling variations of WordPress in text strings.
*
* Prevents matches on:
* - URLs for wordpress.org/com/net/test/tv.
* - `@...` usernames starting with `wordpress`
* - email addresses with a domain starting with `wordpress`
* - email addresses with a user name ending with `wordpress`
* - (most) variable names.
* - directory paths containing a folder starting or ending with `wordpress`.
* - file names containing `wordpress` for a limited set of extensions.
* - `wordpress` prefixed or suffixed with dashes as those are indicators that the
* term is probably used as part of a CSS class, such as `fa-wordpress`
* or filename/path like `class-wordpress-importer.php`.
* - back-tick quoted `wordpress`.
*
* @var string
*/
const WP_REGEX = '#(?<![\\\\/\$@`-])\b(Word[ _-]*Pres+)\b(?![@/`-]|\.(?:org|com|net|test|tv)|[^\s<>\'"()]*?\.(?:php|js|css|png|j[e]?pg|gif|pot))#i';
/**
* Regex to match a large number or spelling variations of WordPress in class names.
*
* @var string
*/
const WP_CLASSNAME_REGEX = '`(?:^|_)(Word[_]*Pres+)(?:_|$)`i';
/**
* String tokens we want to listen for.
*
* @var array
*/
private $text_string_tokens = array(
\T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING,
\T_DOUBLE_QUOTED_STRING => \T_DOUBLE_QUOTED_STRING,
\T_HEREDOC => \T_HEREDOC,
\T_NOWDOC => \T_NOWDOC,
\T_INLINE_HTML => \T_INLINE_HTML,
);
/**
* Comment tokens we want to listen for as they contain text strings.
*
* @var array
*/
private $comment_text_tokens = array(
\T_DOC_COMMENT => \T_DOC_COMMENT,
\T_DOC_COMMENT_STRING => \T_DOC_COMMENT_STRING,
\T_COMMENT => \T_COMMENT,
);
/**
* Combined text string and comment tokens array.
*
* This property is set in the register() method and used for lookups.
*
* @var array
*/
private $text_and_comment_tokens = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.12.0
*
* @return array
*/
public function register() {
// Union the arrays - keeps the array keys.
$this->text_and_comment_tokens = ( $this->text_string_tokens + $this->comment_text_tokens );
$targets = ( $this->text_and_comment_tokens + Tokens::$ooScopeTokens );
// Also sniff for array tokens to make skipping anything within those more efficient.
$targets[ \T_ARRAY ] = \T_ARRAY;
$targets[ \T_OPEN_SHORT_ARRAY ] = \T_OPEN_SHORT_ARRAY;
return $targets;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.12.0
*
* @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 ) {
if ( $this->has_whitelist_comment( 'spelling', $stackPtr ) ) {
return;
}
/*
* Ignore tokens within an array definition as this is a false positive in 80% of all cases.
*
* The return values skip to the end of the array.
* This prevents the sniff "hanging" on very long configuration arrays.
*/
if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code'] && isset( $this->tokens[ $stackPtr ]['bracket_closer'] ) ) {
return $this->tokens[ $stackPtr ]['bracket_closer'];
} elseif ( \T_ARRAY === $this->tokens[ $stackPtr ]['code'] && isset( $this->tokens[ $stackPtr ]['parenthesis_closer'] ) ) {
return $this->tokens[ $stackPtr ]['parenthesis_closer'];
}
/*
* Deal with misspellings in class/interface/trait names.
* These are not auto-fixable, but need the attention of a developer.
*/
if ( isset( Tokens::$ooScopeTokens[ $this->tokens[ $stackPtr ]['code'] ] ) ) {
$classname = $this->phpcsFile->getDeclarationName( $stackPtr );
if ( empty( $classname ) ) {
return;
}
if ( preg_match_all( self::WP_CLASSNAME_REGEX, $classname, $matches, \PREG_PATTERN_ORDER ) > 0 ) {
$mispelled = $this->retrieve_misspellings( $matches[1] );
if ( ! empty( $mispelled ) ) {
$this->phpcsFile->addWarning(
'Please spell "WordPress" correctly. Found: "%s" as part of the class/interface/trait name.',
$stackPtr,
'MisspelledClassName',
array( implode( ', ', $mispelled ) )
);
}
}
return;
}
/*
* Deal with misspellings in text strings and documentation.
*/
// Ignore content of docblock @link tags.
if ( \T_DOC_COMMENT_STRING === $this->tokens[ $stackPtr ]['code']
|| \T_DOC_COMMENT === $this->tokens[ $stackPtr ]['code']
) {
$comment_start = $this->phpcsFile->findPrevious( \T_DOC_COMMENT_OPEN_TAG, ( $stackPtr - 1 ) );
if ( false !== $comment_start ) {
$comment_tag = $this->phpcsFile->findPrevious( \T_DOC_COMMENT_TAG, ( $stackPtr - 1 ), $comment_start );
if ( false !== $comment_tag && '@link' === $this->tokens[ $comment_tag ]['content'] ) {
// @link tag, so ignore.
return;
}
}
}
// Ignore any text strings which are array keys `$var['key']` as this is a false positive in 80% of all cases.
if ( \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $stackPtr ]['code'] ) {
$prevToken = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true, null, true );
if ( false !== $prevToken && \T_OPEN_SQUARE_BRACKET === $this->tokens[ $prevToken ]['code'] ) {
$nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true );
if ( false !== $nextToken && \T_CLOSE_SQUARE_BRACKET === $this->tokens[ $nextToken ]['code'] ) {
return;
}
}
}
// Ignore constant declarations via define().
if ( $this->is_in_function_call( $stackPtr, array( 'define' => true ), true, true ) ) {
return;
}
// Ignore constant declarations using the const keyword.
$stop_points = array(
\T_CONST,
\T_SEMICOLON,
\T_OPEN_TAG,
\T_CLOSE_TAG,
\T_OPEN_CURLY_BRACKET,
);
$maybe_const = $this->phpcsFile->findPrevious( $stop_points, ( $stackPtr - 1 ) );
if ( false !== $maybe_const && \T_CONST === $this->tokens[ $maybe_const ]['code'] ) {
return;
}
$content = $this->tokens[ $stackPtr ]['content'];
if ( preg_match_all( self::WP_REGEX, $content, $matches, ( \PREG_PATTERN_ORDER | \PREG_OFFSET_CAPTURE ) ) > 0 ) {
/*
* Prevent some typical false positives.
*/
if ( isset( $this->text_and_comment_tokens[ $this->tokens[ $stackPtr ]['code'] ] ) ) {
$offset = 0;
foreach ( $matches[1] as $key => $match_data ) {
$next_offset = ( $match_data[1] + \strlen( $match_data[0] ) );
// Prevent matches on part of a URL.
if ( preg_match( '`http[s]?://[^\s<>\'"()]*' . preg_quote( $match_data[0], '`' ) . '`', $content, $discard, 0, $offset ) === 1 ) {
unset( $matches[1][ $key ] );
} elseif ( preg_match( '`[a-z]+=(["\'])' . preg_quote( $match_data[0], '`' ) . '\1`', $content, $discard, 0, $offset ) === 1 ) {
// Prevent matches on html attributes like: `value="wordpress"`.
unset( $matches[1][ $key ] );
} elseif ( preg_match( '`\\\\\'' . preg_quote( $match_data[0], '`' ) . '\\\\\'`', $content, $discard, 0, $offset ) === 1 ) {
// Prevent matches on xpath queries and such: `\'wordpress\'`.
unset( $matches[1][ $key ] );
} elseif ( preg_match( '`(?:\?|&amp;|&)[a-z0-9_]+=' . preg_quote( $match_data[0], '`' ) . '(?:&|$)`', $content, $discard, 0, $offset ) === 1 ) {
// Prevent matches on url query strings: `?something=wordpress`.
unset( $matches[1][ $key ] );
}
$offset = $next_offset;
}
if ( empty( $matches[1] ) ) {
return;
}
}
$mispelled = $this->retrieve_misspellings( $matches[1] );
if ( empty( $mispelled ) ) {
return;
}
$fix = $this->phpcsFile->addFixableWarning(
'Please spell "WordPress" correctly. Found %s misspelling(s): %s',
$stackPtr,
'Misspelled',
array(
\count( $mispelled ),
implode( ', ', $mispelled ),
)
);
if ( true === $fix ) {
// Apply fixes based on offset to ensure we don't replace false positives.
$replacement = $content;
foreach ( $matches[1] as $match ) {
$replacement = substr_replace( $replacement, 'WordPress', $match[1], \strlen( $match[0] ) );
}
$this->phpcsFile->fixer->replaceToken( $stackPtr, $replacement );
}
}
}
/**
* Retrieve a list of misspellings based on an array of matched variations on the target word.
*
* @param array $match_stack Array of matched variations of the target word.
* @return array Array containing only the misspelled variants.
*/
protected function retrieve_misspellings( $match_stack ) {
$mispelled = array();
foreach ( $match_stack as $match ) {
// Deal with multi-dimensional arrays when capturing offset.
if ( \is_array( $match ) ) {
$match = $match[0];
}
if ( 'WordPress' !== $match ) {
$mispelled[] = $match;
}
}
return $mispelled;
}
}
@@ -0,0 +1,235 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Flag cron schedules less than 15 minutes.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#cron-schedules-less-than-15-minutes-or-expensive-events
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.11.0 - Extends the WordPressCS native `Sniff` class.
* - Now deals correctly with WP time constants.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 0.14.0 The minimum cron interval tested against is now configurable.
* @since 1.0.0 This sniff has been moved from the `VIP` category to the `WP` category.
*/
class CronIntervalSniff extends Sniff {
/**
* Minimum allowed cron interval in seconds.
*
* Defaults to 900 (= 15 minutes), which is the requirement for the VIP platform.
*
* @since 0.14.0
*
* @var int
*/
public $min_interval = 900;
/**
* Known WP Time constant names and their value.
*
* @since 0.11.0
*
* @var array
*/
protected $wp_time_constants = array(
'MINUTE_IN_SECONDS' => 60,
'HOUR_IN_SECONDS' => 3600,
'DAY_IN_SECONDS' => 86400,
'WEEK_IN_SECONDS' => 604800,
'MONTH_IN_SECONDS' => 2592000,
'YEAR_IN_SECONDS' => 31536000,
);
/**
* Function within which the hook should be found.
*
* @var array
*/
protected $valid_functions = array(
'add_filter' => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_CONSTANT_ENCAPSED_STRING,
\T_DOUBLE_QUOTED_STRING,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
$token = $this->tokens[ $stackPtr ];
if ( 'cron_schedules' !== $this->strip_quotes( $token['content'] ) ) {
return;
}
// If within add_filter.
$functionPtr = $this->is_in_function_call( $stackPtr, $this->valid_functions );
if ( false === $functionPtr ) {
return;
}
$callback = $this->get_function_call_parameter( $functionPtr, 2 );
if ( false === $callback ) {
return;
}
if ( $stackPtr >= $callback['start'] ) {
// "cron_schedules" found in the second parameter, not the first.
return;
}
// Detect callback function name.
$callbackArrayPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $callback['start'], ( $callback['end'] + 1 ), true );
// If callback is array, get second element.
if ( false !== $callbackArrayPtr
&& ( \T_ARRAY === $this->tokens[ $callbackArrayPtr ]['code']
|| \T_OPEN_SHORT_ARRAY === $this->tokens[ $callbackArrayPtr ]['code'] )
) {
$callback = $this->get_function_call_parameter( $callbackArrayPtr, 2 );
if ( false === $callback ) {
$this->confused( $stackPtr );
return;
}
}
unset( $functionPtr );
// Search for the function in tokens.
$callbackFunctionPtr = $this->phpcsFile->findNext( array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING, \T_CLOSURE ), $callback['start'], ( $callback['end'] + 1 ) );
if ( false === $callbackFunctionPtr ) {
$this->confused( $stackPtr );
return;
}
if ( \T_CLOSURE === $this->tokens[ $callbackFunctionPtr ]['code'] ) {
$functionPtr = $callbackFunctionPtr;
} else {
$functionName = $this->strip_quotes( $this->tokens[ $callbackFunctionPtr ]['content'] );
for ( $ptr = 0; $ptr < $this->phpcsFile->numTokens; $ptr++ ) {
if ( \T_FUNCTION === $this->tokens[ $ptr ]['code'] ) {
$foundName = $this->phpcsFile->getDeclarationName( $ptr );
if ( $foundName === $functionName ) {
$functionPtr = $ptr;
break;
} elseif ( isset( $this->tokens[ $ptr ]['scope_closer'] ) ) {
// Skip to the end of the function definition.
$ptr = $this->tokens[ $ptr ]['scope_closer'];
}
}
}
}
if ( ! isset( $functionPtr ) ) {
$this->confused( $stackPtr );
return;
}
if ( ! isset( $this->tokens[ $functionPtr ]['scope_opener'], $this->tokens[ $functionPtr ]['scope_closer'] ) ) {
return;
}
$opening = $this->tokens[ $functionPtr ]['scope_opener'];
$closing = $this->tokens[ $functionPtr ]['scope_closer'];
for ( $i = $opening; $i <= $closing; $i++ ) {
if ( \in_array( $this->tokens[ $i ]['code'], array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING ), true ) ) {
if ( 'interval' === $this->strip_quotes( $this->tokens[ $i ]['content'] ) ) {
$operator = $this->phpcsFile->findNext( \T_DOUBLE_ARROW, $i, null, false, null, true );
if ( false === $operator ) {
$this->confused( $stackPtr );
return;
}
$valueStart = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $operator + 1 ), null, true, null, true );
$valueEnd = $this->phpcsFile->findNext( array( \T_COMMA, \T_CLOSE_PARENTHESIS ), ( $valueStart + 1 ) );
$value = '';
for ( $j = $valueStart; $j < $valueEnd; $j++ ) {
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $j ]['code'] ] ) ) {
continue;
}
$value .= $this->tokens[ $j ]['content'];
}
if ( is_numeric( $value ) ) {
$interval = $value;
break;
}
// Deal correctly with WP time constants.
$value = str_replace( array_keys( $this->wp_time_constants ), array_values( $this->wp_time_constants ), $value );
// If all digits and operators, eval!
if ( preg_match( '#^[\s\d+*/-]+$#', $value ) > 0 ) {
$interval = eval( "return ( $value );" ); // phpcs:ignore Squiz.PHP.Eval -- No harm here.
break;
}
$this->confused( $stackPtr );
return;
}
}
}
$this->min_interval = (int) $this->min_interval;
if ( isset( $interval ) && $interval < $this->min_interval ) {
$minutes = round( ( $this->min_interval / 60 ), 1 );
$this->phpcsFile->addWarning(
'Scheduling crons at %s sec ( less than %s minutes ) is discouraged.',
$stackPtr,
'CronSchedulesInterval',
array(
$interval,
$minutes,
)
);
return;
}
}
/**
* Add warning about unclear cron schedule change.
*
* @param int $stackPtr The position of the current token in the stack.
*/
public function confused( $stackPtr ) {
$this->phpcsFile->addWarning(
'Detected changing of cron_schedules, but could not detect the interval value.',
$stackPtr,
'ChangeDetected'
);
}
}
@@ -0,0 +1,120 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractClassRestrictionsSniff;
/**
* Restricts the use of deprecated WordPress classes and suggests alternatives.
*
* This sniff will throw an error when usage of a deprecated class is detected
* if the class was deprecated before the minimum supported WP version;
* a warning otherwise.
* By default, it is set to presume that a project will support the current
* WP version and up to three releases before.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.12.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 0.14.0 Now has the ability to handle minimum supported WP version
* being provided via the command-line or as as <config> value
* in a custom ruleset.
*
* @uses \WordPressCS\WordPress\Sniff::$minimum_supported_version
*/
class DeprecatedClassesSniff extends AbstractClassRestrictionsSniff {
/**
* List of deprecated classes with alternative when available.
*
* To be updated after every major release.
*
* Version numbers should be fully qualified.
*
* @var array
*/
private $deprecated_classes = array(
// WP 3.1.0.
'WP_User_Search' => array(
'alt' => 'WP_User_Query',
'version' => '3.1.0',
),
// WP 4.9.0.
'Customize_New_Menu_Section' => array(
'version' => '4.9.0',
),
'WP_Customize_New_Menu_Control' => array(
'version' => '4.9.0',
),
// WP 5.3.0.
'Services_JSON' => array(
'alt' => 'The PHP native JSON extension',
'version' => '5.3.0',
),
);
/**
* Groups of classes to restrict.
*
* @return array
*/
public function getGroups() {
// Make sure all array keys are lowercase.
$this->deprecated_classes = array_change_key_case( $this->deprecated_classes, CASE_LOWER );
return array(
'deprecated_classes' => array(
'classes' => array_keys( $this->deprecated_classes ),
),
);
}
/**
* Process a matched token.
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched. Will
* always be 'deprecated_classes'.
* @param string $matched_content The token content (class name) which was matched.
*
* @return void
*/
public function process_matched_token( $stackPtr, $group_name, $matched_content ) {
$this->get_wp_version_from_cl();
$class_name = ltrim( strtolower( $matched_content ), '\\' );
$message = 'The %s class has been deprecated since WordPress version %s.';
$data = array(
ltrim( $matched_content, '\\' ),
$this->deprecated_classes[ $class_name ]['version'],
);
if ( ! empty( $this->deprecated_classes[ $class_name ]['alt'] ) ) {
$message .= ' Use %s instead.';
$data[] = $this->deprecated_classes[ $class_name ]['alt'];
}
$this->addMessage(
$message,
$stackPtr,
( version_compare( $this->deprecated_classes[ $class_name ]['version'], $this->minimum_supported_version, '<' ) ),
$this->string_to_errorcode( $class_name . 'Found' ),
$data
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,216 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Check for usage of deprecated parameter values in WP functions and provide alternative based on the parameter passed.
*
* @package WPCS\WordPressCodingStandards
*
* @since 1.0.0
*
* @uses \WordPressCS\WordPress\Sniff::$minimum_supported_version
*/
class DeprecatedParameterValuesSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 1.0.0
*
* @var string
*/
protected $group_name = 'wp_deprecated_parameter_values';
/**
* Array of function, argument, and replacement function for deprecated argument.
*
* The list of deprecated parameter values can be found by
* looking for `_deprecated_argument()`.
* The list is sorted alphabetically by function name.
* Last updated for WordPress 4.9.6.
*
* @since 1.0.0
*
* @var array Multidimensional array with parameter details.
* $target_functions = array(
* (string) Function name. => array(
* (int) Target parameter position, 1-based. => array(
* (string) Parameter value. => array(
* 'alt' => (string) Suggested alternative.
* 'version' => (int) The WordPress version when deprecated.
* )
* )
* )
* );
*/
protected $target_functions = array(
'add_settings_field' => array(
4 => array(
'misc' => array(
'alt' => 'another settings group',
'version' => '3.0.0',
),
'privacy' => array(
'alt' => 'another settings group',
'version' => '3.5.0',
),
),
),
'add_settings_section' => array(
4 => array(
'misc' => array(
'alt' => 'another settings group',
'version' => '3.0.0',
),
'privacy' => array(
'alt' => 'another settings group',
'version' => '3.5.0',
),
),
),
'bloginfo' => array(
1 => array(
'home' => array(
'alt' => 'the "url" argument',
'version' => '2.2.0',
),
'siteurl' => array(
'alt' => 'the "url" argument',
'version' => '2.2.0',
),
'text_direction' => array(
'alt' => 'is_rtl()',
'version' => '2.2.0',
),
),
),
'get_bloginfo' => array(
1 => array(
'home' => array(
'alt' => 'the "url" argument',
'version' => '2.2.0',
),
'siteurl' => array(
'alt' => 'the "url" argument',
'version' => '2.2.0',
),
'text_direction' => array(
'alt' => 'is_rtl()',
'version' => '2.2.0',
),
),
),
'register_setting' => array(
1 => array(
'misc' => array(
'alt' => 'another settings group',
'version' => '3.0.0',
),
'privacy' => array(
'alt' => 'another settings group',
'version' => '3.5.0',
),
),
),
'unregister_setting' => array(
1 => array(
'misc' => array(
'alt' => 'another settings group',
'version' => '3.0.0',
),
'privacy' => array(
'alt' => 'another settings group',
'version' => '3.5.0',
),
),
),
);
/**
* Process the parameters of a matched function.
*
* @since 1.0.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return void
*/
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
$this->get_wp_version_from_cl();
$param_count = \count( $parameters );
foreach ( $this->target_functions[ $matched_content ] as $position => $parameter_args ) {
// Stop if the position is higher then the total number of parameters.
if ( $position > $param_count ) {
break;
}
$this->process_parameter( $matched_content, $parameters[ $position ], $parameter_args );
}
}
/**
* Process the parameter of a matched function.
*
* @since 1.0.0
*
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameter Array with start and end token positon of the parameter.
* @param array $parameter_args Array with alternative and WordPress deprecation version of the parameter.
*
* @return void
*/
protected function process_parameter( $matched_content, $parameter, $parameter_args ) {
$parameter_position = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$parameter['start'],
$parameter['end'] + 1,
true
);
if ( false === $parameter_position ) {
return;
}
$matched_parameter = $this->strip_quotes( $this->tokens[ $parameter_position ]['content'] );
if ( ! isset( $parameter_args[ $matched_parameter ] ) ) {
return;
}
$message = 'The parameter value "%s" has been deprecated since WordPress version %s.';
$data = array(
$matched_parameter,
$parameter_args[ $matched_parameter ]['version'],
);
if ( ! empty( $parameter_args[ $matched_parameter ]['alt'] ) ) {
$message .= ' Use %s instead.';
$data[] = $parameter_args[ $matched_parameter ]['alt'];
}
$is_error = version_compare( $parameter_args[ $matched_parameter ]['version'], $this->minimum_supported_version, '<' );
$this->addMessage(
$message,
$parameter_position,
$is_error,
$this->string_to_errorcode( 'Found' ),
$data
);
}
}
@@ -0,0 +1,338 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
/**
* Check for usage of deprecated parameters in WP functions and suggest alternative based on the parameter passed.
*
* This sniff will throw an error when usage of deprecated parameters is
* detected if the parameter was deprecated before the minimum supported
* WP version; a warning otherwise.
* By default, it is set to presume that a project will support the current
* WP version and up to three releases before.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.12.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 0.14.0 Now has the ability to handle minimum supported WP version
* being provided via the command-line or as as <config> value
* in a custom ruleset.
*
* @uses \WordPressCS\WordPress\Sniff::$minimum_supported_version
*/
class DeprecatedParametersSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 0.12.0
*
* @var string
*/
protected $group_name = 'wp_deprecated_parameters';
/**
* Array of function, argument, and default value for deprecated argument.
*
* The functions are ordered alphabetically.
* Last updated for WordPress 4.8.0.
*
* @since 0.12.0
*
* @var array Multidimensional array with parameter details.
* $target_functions = array(
* (string) Function name. => array(
* (int) Target parameter position, 1-based. => array(
* 'value' => (mixed) Expected default value for the
* deprecated parameter. Currently the default
* values: true, false, null, empty arrays and
* both empty and non-empty strings can be
* handled correctly by the process_parameters()
* method. When an additional default value is
* added, the relevant code in the
* process_parameters() method will need to be
* adjusted.
* 'version' => (int) The WordPress version when deprecated.
* )
* )
* );
*/
protected $target_functions = array(
'add_option' => array(
3 => array(
'value' => '',
'version' => '2.3.0',
),
),
'comments_link' => array(
1 => array(
'value' => '',
'version' => '0.72',
),
2 => array(
'value' => '',
'version' => '1.3.0',
),
),
'comments_number' => array(
4 => array(
'value' => '',
'version' => '1.3.0',
),
),
'convert_chars' => array(
2 => array(
'value' => '',
'version' => '0.71',
),
),
'discover_pingback_server_uri' => array(
2 => array(
'value' => '',
'version' => '2.7.0',
),
),
'get_category_parents' => array(
5 => array(
'value' => array(),
'version' => '4.8.0',
),
),
'get_delete_post_link' => array(
2 => array(
'value' => '',
'version' => '3.0.0',
),
),
'get_last_updated' => array(
1 => array(
'value' => '',
'version' => '3.0.0', // Was previously part of MU.
),
),
'get_the_author' => array(
1 => array(
'value' => '',
'version' => '2.1.0',
),
),
'get_user_option' => array(
3 => array(
'value' => '',
'version' => '2.3.0',
),
),
'get_wp_title_rss' => array(
1 => array(
'value' => '&#8211;',
'version' => '4.4.0',
),
),
'is_email' => array(
2 => array(
'value' => false,
'version' => '3.0.0',
),
),
'load_plugin_textdomain' => array(
2 => array(
'value' => false,
'version' => '2.7.0',
),
),
'safecss_filter_attr' => array(
2 => array(
'value' => '',
'version' => '2.8.1',
),
),
'the_attachment_link' => array(
3 => array(
'value' => false,
'version' => '2.5.0',
),
),
'the_author' => array(
1 => array(
'value' => '',
'version' => '2.1.0',
),
2 => array(
'value' => true,
'version' => '1.5.0',
),
),
'the_author_posts_link' => array(
1 => array(
'value' => '',
'version' => '2.1.0',
),
),
'trackback_rdf' => array(
1 => array(
'value' => '',
'version' => '2.5.0',
),
),
'trackback_url' => array(
1 => array(
'value' => true,
'version' => '2.5.0',
),
),
'update_blog_option' => array(
4 => array(
'value' => null,
'version' => '3.1.0',
),
),
'update_blog_status' => array(
4 => array(
'value' => null,
'version' => '3.1.0',
),
),
'update_user_status' => array(
4 => array(
'value' => null,
'version' => '3.0.2',
),
),
'unregister_setting' => array(
4 => array(
'value' => '',
'version' => '4.7.0',
),
),
'wp_get_http_headers' => array(
2 => array(
'value' => false,
'version' => '2.7.0',
),
),
'wp_get_sidebars_widgets' => array(
1 => array(
'value' => true,
'version' => '2.8.1',
),
),
'wp_install' => array(
5 => array(
'value' => '',
'version' => '2.6.0',
),
),
'wp_new_user_notification' => array(
2 => array(
'value' => null,
'version' => '4.3.1',
),
),
'wp_notify_postauthor' => array(
2 => array(
'value' => null,
'version' => '3.8.0',
),
),
'wp_title_rss' => array(
1 => array(
'value' => '&#8211;',
'version' => '4.4.0',
),
),
'wp_upload_bits' => array(
2 => array(
'value' => null,
'version' => '2.0.0',
),
),
'xfn_check' => array(
3 => array(
'value' => '',
'version' => '2.5.0',
),
),
);
/**
* Process the parameters of a matched function.
*
* @since 0.12.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return void
*/
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
$this->get_wp_version_from_cl();
$paramCount = \count( $parameters );
foreach ( $this->target_functions[ $matched_content ] as $position => $parameter_args ) {
// Check that number of parameters defined is not less than the position to check.
if ( $position > $paramCount ) {
break;
}
// The list will need to updated if the default value is not supported.
switch ( $parameters[ $position ]['raw'] ) {
case 'true':
$matched_parameter = true;
break;
case 'false':
$matched_parameter = false;
break;
case 'null':
$matched_parameter = null;
break;
case 'array()':
case '[]':
$matched_parameter = array();
break;
default:
$matched_parameter = $this->strip_quotes( $parameters[ $position ]['raw'] );
break;
}
if ( $parameter_args['value'] === $matched_parameter ) {
continue;
}
$message = 'The parameter "%s" at position #%s of %s() has been deprecated since WordPress version %s.';
$is_error = version_compare( $parameter_args['version'], $this->minimum_supported_version, '<' );
$code = $this->string_to_errorcode( ucfirst( $matched_content ) . 'Param' . $position . 'Found' );
$data = array(
$parameters[ $position ]['raw'],
$position,
$matched_content,
$parameter_args['version'],
);
if ( isset( $parameter_args['value'] ) && $position < $paramCount ) {
$message .= ' Use "%s" instead.';
$data[] = (string) $parameter_args['value'];
} else {
$message .= ' Instead do not pass the parameter.';
}
$this->addMessage( $message, $stackPtr, $is_error, $code, $data, 0 );
}
}
}
@@ -0,0 +1,217 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Warns against usage of discouraged WP CONSTANTS and recommends alternatives.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.14.0
*/
class DiscouragedConstantsSniff extends AbstractFunctionParameterSniff {
/**
* List of discouraged WP constants and their replacements.
*
* @since 0.14.0
*
* @var array
*/
protected $discouraged_constants = array(
'STYLESHEETPATH' => 'get_stylesheet_directory()',
'TEMPLATEPATH' => 'get_template_directory()',
'PLUGINDIR' => 'WP_PLUGIN_DIR',
'MUPLUGINDIR' => 'WPMU_PLUGIN_DIR',
'HEADER_IMAGE' => 'add_theme_support( \'custom-header\' )',
'NO_HEADER_TEXT' => 'add_theme_support( \'custom-header\' )',
'HEADER_TEXTCOLOR' => 'add_theme_support( \'custom-header\' )',
'HEADER_IMAGE_WIDTH' => 'add_theme_support( \'custom-header\' )',
'HEADER_IMAGE_HEIGHT' => 'add_theme_support( \'custom-header\' )',
'BACKGROUND_COLOR' => 'add_theme_support( \'custom-background\' )',
'BACKGROUND_IMAGE' => 'add_theme_support( \'custom-background\' )',
);
/**
* Array of functions to check.
*
* @since 0.14.0
*
* @var array <string function name> => <int parameter position>
*/
protected $target_functions = array(
'define' => 1,
);
/**
* Array of tokens which if found preceding the $stackPtr indicate that a T_STRING is not a constant.
*
* @var array
*/
private $preceding_tokens_to_ignore = array(
\T_NAMESPACE => true,
\T_USE => true,
\T_CLASS => true,
\T_TRAIT => true,
\T_INTERFACE => true,
\T_EXTENDS => true,
\T_IMPLEMENTS => true,
\T_NEW => true,
\T_FUNCTION => true,
\T_DOUBLE_COLON => true,
\T_OBJECT_OPERATOR => true,
\T_INSTANCEOF => true,
\T_GOTO => true,
);
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.14.0
*
* @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 ) {
if ( isset( $this->target_functions[ strtolower( $this->tokens[ $stackPtr ]['content'] ) ] ) ) {
// Disallow excluding function groups for this sniff.
$this->exclude = array();
return parent::process_token( $stackPtr );
} else {
return $this->process_arbitrary_tstring( $stackPtr );
}
}
/**
* Process an arbitrary T_STRING token to determine whether it is one of the target constants.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_arbitrary_tstring( $stackPtr ) {
$content = $this->tokens[ $stackPtr ]['content'];
if ( ! isset( $this->discouraged_constants[ $content ] ) ) {
return;
}
$next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( false !== $next && \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code'] ) {
// Function call or declaration.
return;
}
$prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
if ( false !== $prev && isset( $this->preceding_tokens_to_ignore[ $this->tokens[ $prev ]['code'] ] ) ) {
// Not the use of a constant.
return;
}
if ( $this->is_token_namespaced( $stackPtr ) === true ) {
// Namespaced constant of the same name.
return;
}
if ( false !== $prev
&& \T_CONST === $this->tokens[ $prev ]['code']
&& true === $this->is_class_constant( $prev )
) {
// Class constant of the same name.
return;
}
/*
* Deal with a number of variations of use statements.
*/
for ( $i = $stackPtr; $i > 0; $i-- ) {
if ( $this->tokens[ $i ]['line'] !== $this->tokens[ $stackPtr ]['line'] ) {
break;
}
}
$first_on_line = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
if ( false !== $first_on_line && \T_USE === $this->tokens[ $first_on_line ]['code'] ) {
$next_on_line = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $first_on_line + 1 ), null, true );
if ( false !== $next_on_line ) {
if ( ( \T_STRING === $this->tokens[ $next_on_line ]['code']
&& 'const' === $this->tokens[ $next_on_line ]['content'] )
|| \T_CONST === $this->tokens[ $next_on_line ]['code'] // Happens in some PHPCS versions.
) {
$has_ns_sep = $this->phpcsFile->findNext( \T_NS_SEPARATOR, ( $next_on_line + 1 ), $stackPtr );
if ( false !== $has_ns_sep ) {
// Namespaced const (group) use statement.
return;
}
} else {
// Not a const use statement.
return;
}
}
}
// Ok, this is really one of the discouraged constants.
$this->phpcsFile->addWarning(
'Found usage of constant "%s". Use %s instead.',
$stackPtr,
$this->string_to_errorcode( $content . 'UsageFound' ),
array(
$content,
$this->discouraged_constants[ $content ],
)
);
}
/**
* Process the parameters of a matched `define` function call.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return void
*/
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
$function_name = strtolower( $matched_content );
$target_param = $this->target_functions[ $function_name ];
// Was the target parameter passed ?
if ( ! isset( $parameters[ $target_param ] ) ) {
return;
}
$raw_content = $this->strip_quotes( $parameters[ $target_param ]['raw'] );
if ( isset( $this->discouraged_constants[ $raw_content ] ) ) {
$this->phpcsFile->addWarning(
'Found declaration of constant "%s". Use %s instead.',
$stackPtr,
$this->string_to_errorcode( $raw_content . 'DeclarationFound' ),
array(
$raw_content,
$this->discouraged_constants[ $raw_content ],
)
);
}
}
}
@@ -0,0 +1,57 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Discourages the use of various WordPress functions and suggests alternatives.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.11.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class DiscouragedFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'query_posts' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use WP_Query instead.',
'functions' => array(
'query_posts',
),
),
'wp_reset_query' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use wp_reset_postdata() instead.',
'functions' => array(
'wp_reset_query',
),
),
);
}
}
@@ -0,0 +1,223 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* This checks the enqueued 4th and 5th parameters to make sure the version and in_footer are set.
*
* If a source ($src) value is passed, then version ($ver) needs to have non-falsy value.
* If a source ($src) value is passed a check for in footer ($in_footer), warn the user if the value is falsy.
*
* @link https://developer.wordpress.org/reference/functions/wp_register_script/
* @link https://developer.wordpress.org/reference/functions/wp_enqueue_script/
* @link https://developer.wordpress.org/reference/functions/wp_register_style/
* @link https://developer.wordpress.org/reference/functions/wp_enqueue_style/
*
* @package WPCS\WordPressCodingStandards
*
* @since 1.0.0
*/
class EnqueuedResourceParametersSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 1.0.0
*
* @var string
*/
protected $group_name = 'Enqueued';
/**
* List of enqueued functions that need to be checked for use of the in_footer and version arguments.
*
* @since 1.0.0
*
* @var array <string function_name> => <bool true>
*/
protected $target_functions = array(
'wp_register_script' => true,
'wp_enqueue_script' => true,
'wp_register_style' => true,
'wp_enqueue_style' => true,
);
/**
* False + the empty tokens array.
*
* This array is enriched with the $emptyTokens array in the register() method.
*
* @var array
*/
private $false_tokens = array(
\T_FALSE => \T_FALSE,
);
/**
* Token codes which are "safe" to accept to determine whether a version would evaluate to `false`.
*
* This array is enriched with the several of the PHPCS token arrays in the register() method.
*
* @var array
*/
private $safe_tokens = array(
\T_NULL => \T_NULL,
\T_FALSE => \T_FALSE,
\T_TRUE => \T_TRUE,
\T_LNUMBER => \T_LNUMBER,
\T_DNUMBER => \T_DNUMBER,
\T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING,
\T_START_NOWDOC => \T_START_NOWDOC,
\T_NOWDOC => \T_NOWDOC,
\T_END_NOWDOC => \T_END_NOWDOC,
\T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS,
\T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS,
\T_STRING_CONCAT => \T_STRING_CONCAT,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* Overloads and calls the parent method to allow for adding additional tokens to the $safe_tokens property.
*
* @return array
*/
public function register() {
$this->false_tokens += Tokens::$emptyTokens;
$this->safe_tokens += Tokens::$emptyTokens;
$this->safe_tokens += Tokens::$assignmentTokens;
$this->safe_tokens += Tokens::$comparisonTokens;
$this->safe_tokens += Tokens::$operators;
$this->safe_tokens += Tokens::$booleanOperators;
$this->safe_tokens += Tokens::$castTokens;
return parent::register();
}
/**
* Process the parameters of a matched function.
*
* @since 1.0.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return void
*/
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
// Check to see if a source ($src) is specified.
if ( ! isset( $parameters[2] ) ) {
return;
}
/*
* Version Check: Check to make sure the version is set explicitly.
*/
if ( ! isset( $parameters[4] ) || 'null' === $parameters[4]['raw'] ) {
$type = 'script';
if ( strpos( $matched_content, '_style' ) !== false ) {
$type = 'style';
}
$this->phpcsFile->addError(
'Resource version not set in call to %s(). This means new versions of the %s will not always be loaded due to browser caching.',
$stackPtr,
'MissingVersion',
array( $matched_content, $type )
);
} else {
// The version argument should have a non-false value.
if ( $this->is_falsy( $parameters[4]['start'], $parameters[4]['end'] ) ) {
$this->phpcsFile->addError(
'Version parameter is not explicitly set or has been set to an equivalent of "false" for %s; ' .
'This means that the WordPress core version will be used which is not recommended for plugin or theme development.',
$stackPtr,
'NoExplicitVersion',
array( $matched_content )
);
}
}
/*
* In footer Check
*
* Check to make sure that $in_footer is set to true.
* It will warn the user to make sure it is intended.
*
* Only wp_register_script and wp_enqueue_script need this check,
* as this parameter is not available to wp_register_style and wp_enqueue_style.
*/
if ( 'wp_register_script' !== $matched_content && 'wp_enqueue_script' !== $matched_content ) {
return;
}
if ( ! isset( $parameters[5] ) ) {
// If in footer is not set, throw a warning about the default.
$this->phpcsFile->addWarning(
'In footer ($in_footer) is not set explicitly %s; ' .
'It is recommended to load scripts in the footer. Please set this value to `true` to load it in the footer, or explicitly `false` if it should be loaded in the header.',
$stackPtr,
'NotInFooter',
array( $matched_content )
);
}
}
/**
* Determine if a range has a falsy value.
*
* @param int $start The position to start looking from.
* @param int $end The position to stop looking (inclusive).
*
* @return bool True if the parameter is falsy.
* False if the parameter is not falsy or when it
* couldn't be reliably determined.
*/
protected function is_falsy( $start, $end ) {
// Find anything excluding the false tokens.
$has_non_false = $this->phpcsFile->findNext( $this->false_tokens, $start, ( $end + 1 ), true );
// If no non-false tokens are found, we are good.
if ( false === $has_non_false ) {
return true;
}
$code_string = '';
for ( $i = $start; $i <= $end; $i++ ) {
if ( isset( $this->safe_tokens[ $this->tokens[ $i ]['code'] ] ) === false ) {
// Function call/variable or other token which makes it neigh impossible
// to determine whether the actual value would evaluate to false.
return false;
}
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) === true ) {
continue;
}
$code_string .= $this->tokens[ $i ]['content'];
}
if ( '' === $code_string ) {
return false;
}
// Evaluate the argument to figure out the outcome is false or not.
// phpcs:ignore Squiz.PHP.Eval -- No harm here.
return ( false === eval( "return (bool) $code_string;" ) );
}
}
@@ -0,0 +1,64 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Makes sure scripts and styles are enqueued and not explicitly echo'd.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#inline-resources
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.12.0 This class now extends the WordPressCS native `Sniff` class.
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class EnqueuedResourcesSniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return Tokens::$textStringTokens;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
$token = $this->tokens[ $stackPtr ];
if ( preg_match( '# rel=\\\\?[\'"]?stylesheet\\\\?[\'"]?#', $token['content'] ) > 0 ) {
$this->phpcsFile->addError(
'Stylesheets must be registered/enqueued via wp_enqueue_style',
$stackPtr,
'NonEnqueuedStylesheet'
);
}
if ( preg_match( '#<script[^>]*(?<=src=)#', $token['content'] ) > 0 ) {
$this->phpcsFile->addError(
'Scripts must be registered/enqueued via wp_enqueue_script',
$stackPtr,
'NonEnqueuedScript'
);
}
}
}
@@ -0,0 +1,466 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Warns about overwriting WordPress native global variables.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.4.0 This class now extends the WordPressCS native `Sniff` class.
* @since 0.12.0 The $wp_globals property has been moved to the `Sniff` class.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This sniff has been moved from the `Variables` category to the `WP`
* category and renamed from `GlobalVariables` to `GlobalVariablesOverride`.
* @since 1.1.0 The sniff now also detects variables being overriden in the global namespace.
* @since 2.2.0 The sniff now also detects variable assignments via the list() construct.
*
* @uses \WordPressCS\WordPress\Sniff::$custom_test_class_whitelist
*/
class GlobalVariablesOverrideSniff extends Sniff {
/**
* Whether to treat all files as if they were included from
* within a function.
*
* This is mostly useful for projects containing views which are being
* included from within a function in another file, like themes.
*
* Note: enabling this is discouraged as there is no guarantee that
* the file will *never* be included from the global scope.
*
* @since 1.1.0
*
* @var bool
*/
public $treat_files_as_scoped = false;
/**
* Whitelist select variables from the Sniff::$wp_globals array.
*
* A few select variables in WP Core are _intended_ to be overwritten
* by themes/plugins. This sniff should not throw an error for those.
*
* @since 2.2.0
*
* @var array
*/
protected $override_allowed = array(
'content_width' => true,
'wp_cockneyreplace' => true,
);
/**
* Scoped object and function structures to skip over as
* variables will have a different scope within those.
*
* @since 1.1.0
*
* @var array
*/
private $skip_over = array(
\T_FUNCTION => true,
\T_CLOSURE => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.3.0
* @since 1.1.0 Added class tokens for improved test classes skipping.
*
* @return array
*/
public function register() {
// Add the OO scope tokens to the $skip_over property.
$this->skip_over += Tokens::$ooScopeTokens;
$targets = array(
\T_GLOBAL,
\T_VARIABLE,
\T_LIST,
\T_OPEN_SHORT_ARRAY,
);
// Only used to skip over test classes.
$targets += Tokens::$ooScopeTokens;
return $targets;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.3.0
* @since 1.1.0 Split the token specific logic off into separate methods.
*
* @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 ) {
$token = $this->tokens[ $stackPtr ];
// Ignore variable overrides in test classes.
if ( isset( Tokens::$ooScopeTokens[ $token['code'] ] ) ) {
if ( true === $this->is_test_class( $stackPtr )
&& $token['scope_condition'] === $stackPtr
&& isset( $token['scope_closer'] )
) {
// Skip forward to end of test class.
return $token['scope_closer'];
}
// Otherwise ignore the tokens as they were only registered to enable skipping over test classes.
return;
}
/*
* Examine variables within a function scope based on a `global` statement in the
* function.
* Examine variables not within a function scope, but within a list construct, based
* on that.
* Examine variables not within a function scope and access to the `$GLOBALS`
* variable based on the variable token.
*/
$in_function_scope = $this->phpcsFile->hasCondition( $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) );
if ( ( \T_LIST === $token['code'] || \T_OPEN_SHORT_ARRAY === $token['code'] )
&& false === $in_function_scope
&& false === $this->treat_files_as_scoped
) {
return $this->process_list_assignment( $stackPtr );
} elseif ( \T_VARIABLE === $token['code']
&& ( '$GLOBALS' === $token['content']
|| ( false === $in_function_scope && false === $this->treat_files_as_scoped ) )
) {
return $this->process_variable_assignment( $stackPtr );
} elseif ( \T_GLOBAL === $token['code']
&& ( true === $in_function_scope || true === $this->treat_files_as_scoped )
) {
return $this->process_global_statement( $stackPtr, $in_function_scope );
}
}
/**
* Check that global variables declared via a list construct are prefixed.
*
* @internal No need to take special measures for nested lists. Nested or not,
* each list part can only contain one variable being written to.
*
* @since 2.2.0
*
* @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.
*/
protected function process_list_assignment( $stackPtr ) {
$list_open_close = $this->find_list_open_close( $stackPtr );
if ( false === $list_open_close ) {
// Short array, not short list.
return;
}
$var_pointers = $this->get_list_variables( $stackPtr, $list_open_close );
foreach ( $var_pointers as $ptr ) {
$this->process_variable_assignment( $ptr, true );
}
// No need to re-examine these variables.
return $list_open_close['closer'];
}
/**
* Check that defined global variables are prefixed.
*
* @since 1.1.0 Logic was previously contained in the process_token() method.
*
* @param int $stackPtr The position of the current token in the stack.
* @param bool $in_list Whether or not this is a variable in a list assignment.
* Defaults to false.
*
* @return void
*/
protected function process_variable_assignment( $stackPtr, $in_list = false ) {
if ( $this->has_whitelist_comment( 'override', $stackPtr ) === true ) {
return;
}
$token = $this->tokens[ $stackPtr ];
$var_name = substr( $token['content'], 1 ); // Strip the dollar sign.
$data = array();
// Determine the variable name for `$GLOBALS['array_key']`.
if ( 'GLOBALS' === $var_name ) {
$bracketPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( false === $bracketPtr || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $bracketPtr ]['code'] || ! isset( $this->tokens[ $bracketPtr ]['bracket_closer'] ) ) {
return;
}
// Retrieve the array key and avoid getting tripped up by some simple obfuscation.
$var_name = '';
$start = ( $bracketPtr + 1 );
for ( $ptr = $start; $ptr < $this->tokens[ $bracketPtr ]['bracket_closer']; $ptr++ ) {
/*
* If the globals array key contains a variable, constant, function call
* or interpolated variable, bow out.
*/
if ( \T_VARIABLE === $this->tokens[ $ptr ]['code']
|| \T_STRING === $this->tokens[ $ptr ]['code']
|| \T_DOUBLE_QUOTED_STRING === $this->tokens[ $ptr ]['code']
) {
return;
}
if ( \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $ptr ]['code'] ) {
$var_name .= $this->strip_quotes( $this->tokens[ $ptr ]['content'] );
}
}
if ( '' === $var_name ) {
// Shouldn't happen, but just in case.
return;
}
// Set up the data for the error message.
$data[] = '$GLOBALS[\'' . $var_name . '\']';
}
/*
* Is this one of the WP global variables ?
*/
if ( isset( $this->wp_globals[ $var_name ] ) === false ) {
return;
}
/*
* Is this one of the WP global variables which are allowed to be overwritten ?
*/
if ( isset( $this->override_allowed[ $var_name ] ) === true ) {
return;
}
/*
* Check if the variable value is being changed.
*/
if ( false === $in_list
&& false === $this->is_assignment( $stackPtr )
&& false === $this->is_foreach_as( $stackPtr )
) {
return;
}
/*
* Function parameters with the same name as a WP global variable are fine,
* including when they are being assigned a default value.
*/
if ( false === $in_list && isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
foreach ( $this->tokens[ $stackPtr ]['nested_parenthesis'] as $opener => $closer ) {
if ( isset( $this->tokens[ $opener ]['parenthesis_owner'] )
&& ( \T_FUNCTION === $this->tokens[ $this->tokens[ $opener ]['parenthesis_owner'] ]['code']
|| \T_CLOSURE === $this->tokens[ $this->tokens[ $opener ]['parenthesis_owner'] ]['code'] )
) {
return;
}
}
unset( $opener, $closer );
}
/*
* Class property declarations with the same name as WP global variables are fine.
*/
if ( false === $in_list && true === $this->is_class_property( $stackPtr ) ) {
return;
}
// Still here ? In that case, the WP global variable is being tampered with.
$this->add_error( $stackPtr, $data );
}
/**
* Check that global variables imported into a function scope using a global statement
* are not being overruled.
*
* @since 1.1.0 Logic was previously contained in the process_token() method.
*
* @param int $stackPtr The position of the current token in the stack.
* @param bool $in_function_scope Whether the global statement is within a scoped function/closure.
*
* @return void
*/
protected function process_global_statement( $stackPtr, $in_function_scope ) {
/*
* Collect the variables to watch for.
*/
$search = array();
$ptr = ( $stackPtr + 1 );
while ( isset( $this->tokens[ $ptr ] ) ) {
$var = $this->tokens[ $ptr ];
// Halt the loop at end of statement.
if ( \T_SEMICOLON === $var['code'] ) {
break;
}
if ( \T_VARIABLE === $var['code'] ) {
$var_name = substr( $var['content'], 1 );
if ( isset( $this->wp_globals[ $var_name ] )
&& isset( $this->override_allowed[ $var_name ] ) === false
) {
$search[] = $var['content'];
}
}
$ptr++;
}
unset( $var );
if ( empty( $search ) ) {
return;
}
/*
* Search for assignments to the imported global variables within the relevant scope.
*/
$start = $ptr;
if ( true === $in_function_scope ) {
$function_cond = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION );
$closure_cond = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE );
$scope_cond = max( $function_cond, $closure_cond ); // If false, it will evaluate as zero, so this is fine.
if ( isset( $this->tokens[ $scope_cond ]['scope_closer'] ) === false ) {
// Live coding or parse error.
return;
}
$end = $this->tokens[ $scope_cond ]['scope_closer'];
} else {
// Global statement in the global namespace with file is being treated as scoped.
$end = $this->phpcsFile->numTokens;
}
for ( $ptr = $start; $ptr < $end; $ptr++ ) {
// Skip over nested functions, classes and the likes.
if ( isset( $this->skip_over[ $this->tokens[ $ptr ]['code'] ] ) ) {
if ( ! isset( $this->tokens[ $ptr ]['scope_closer'] ) ) {
// Live coding or parse error.
break;
}
$ptr = $this->tokens[ $ptr ]['scope_closer'];
continue;
}
// Make sure to recognize assignments to variables in a list construct.
if ( \T_LIST === $this->tokens[ $ptr ]['code']
|| \T_OPEN_SHORT_ARRAY === $this->tokens[ $ptr ]['code']
) {
$list_open_close = $this->find_list_open_close( $ptr );
if ( false === $list_open_close ) {
// Short array, not short list.
continue;
}
$var_pointers = $this->get_list_variables( $ptr, $list_open_close );
foreach ( $var_pointers as $ptr ) {
$var_name = $this->tokens[ $ptr ]['content'];
if ( '$GLOBALS' === $var_name ) {
$var_name = '$' . $this->strip_quotes( $this->get_array_access_key( $ptr ) );
}
if ( \in_array( $var_name, $search, true ) ) {
$this->process_variable_assignment( $ptr, true );
}
}
// No need to re-examine these variables.
$ptr = $list_open_close['closer'];
continue;
}
if ( \T_VARIABLE !== $this->tokens[ $ptr ]['code'] ) {
continue;
}
if ( \in_array( $this->tokens[ $ptr ]['content'], $search, true ) === false ) {
// Not one of the variables we're interested in.
continue;
}
// Don't throw false positives for static class properties.
if ( $this->is_class_object_call( $ptr ) === true ) {
continue;
}
if ( true === $this->is_assignment( $ptr ) ) {
$this->maybe_add_error( $ptr );
continue;
}
// Check if this is a variable assignment within a `foreach()` declaration.
if ( $this->is_foreach_as( $ptr ) === true ) {
$this->maybe_add_error( $ptr );
}
}
}
/**
* Add the error if there is no whitelist comment present.
*
* @since 0.11.0
* @since 1.1.0 - Visibility changed from public to protected.
* - Check for being in a test class moved to the process_token() method.
*
* @param int $stackPtr The position of the token to throw the error for.
*
* @return void
*/
protected function maybe_add_error( $stackPtr ) {
if ( $this->has_whitelist_comment( 'override', $stackPtr ) === false ) {
$this->add_error( $stackPtr );
}
}
/**
* Add the error.
*
* @since 1.1.0
*
* @param int $stackPtr The position of the token to throw the error for.
* @param array $data Optional. Array containing one entry holding the
* name of the variable being overruled.
* Defaults to the 'content' of the $stackPtr token.
*
* @return void
*/
protected function add_error( $stackPtr, $data = array() ) {
if ( empty( $data ) ) {
$data[] = $this->tokens[ $stackPtr ]['content'];
}
$this->phpcsFile->addError(
'Overriding WordPress globals is prohibited. Found assignment to %s',
$stackPtr,
'Prohibited',
$data
);
}
}
@@ -0,0 +1,789 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
use WordPressCS\WordPress\PHPCSHelper;
use PHP_CodeSniffer\Util\Tokens;
/**
* Makes sure WP internationalization functions are used properly.
*
* @link https://make.wordpress.org/core/handbook/best-practices/internationalization/
* @link https://developer.wordpress.org/plugins/internationalization/
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0
* @since 0.11.0 - Now also checks for translators comments.
* - Now has the ability to handle text domain set via the command-line
* as a comma-delimited list.
* `phpcs --runtime-set text_domain my-slug,default`
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This class now extends the WordPressCS native
* `AbstractFunctionRestrictionSniff` class.
* The parent `exclude` property is, however, disabled as it
* would disable the whole sniff.
*/
class I18nSniff extends AbstractFunctionRestrictionsSniff {
/**
* These Regexes copied from http://php.net/manual/en/function.sprintf.php#93552
* and adjusted for better precision and updated specs.
*/
const SPRINTF_PLACEHOLDER_REGEX = '/(?:
(?<!%) # Don\'t match a literal % (%%).
(
% # Start of placeholder.
(?:[0-9]+\$)? # Optional ordering of the placeholders.
[+-]? # Optional sign specifier.
(?:
(?:0|\'.)? # Optional padding specifier - excluding the space.
-? # Optional alignment specifier.
[0-9]* # Optional width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
| # Only recognize the space as padding in combination with a width specifier.
(?:[ ])? # Optional space padding specifier.
-? # Optional alignment specifier.
[0-9]+ # Width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
)
[bcdeEfFgGosuxX] # Type specifier.
)
)/x';
/**
* "Unordered" means there's no position specifier: '%s', not '%2$s'.
*/
const UNORDERED_SPRINTF_PLACEHOLDER_REGEX = '/(?:
(?<!%) # Don\'t match a literal % (%%).
% # Start of placeholder.
[+-]? # Optional sign specifier.
(?:
(?:0|\'.)? # Optional padding specifier - excluding the space.
-? # Optional alignment specifier.
[0-9]* # Optional width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
| # Only recognize the space as padding in combination with a width specifier.
(?:[ ])? # Optional space padding specifier.
-? # Optional alignment specifier.
[0-9]+ # Width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
)
[bcdeEfFgGosuxX] # Type specifier.
)/x';
/**
* Text domain.
*
* @todo Eventually this should be able to be auto-supplied via looking at $this->phpcsFile->getFilename()
* @link https://youtrack.jetbrains.com/issue/WI-17740
*
* @var string[]|string
*/
public $text_domain;
/**
* The I18N functions in use in WP.
*
* @since 0.10.0
* @since 0.11.0 Changed visibility from public to protected.
*
* @var array <string function name> => <string function type>
*/
protected $i18n_functions = array(
'translate' => 'simple',
'__' => 'simple',
'esc_attr__' => 'simple',
'esc_html__' => 'simple',
'_e' => 'simple',
'esc_attr_e' => 'simple',
'esc_html_e' => 'simple',
'translate_with_gettext_context' => 'context',
'_x' => 'context',
'_ex' => 'context',
'esc_attr_x' => 'context',
'esc_html_x' => 'context',
'_n' => 'number',
'_nx' => 'number_context',
'_n_noop' => 'noopnumber',
'_nx_noop' => 'noopnumber_context',
);
/**
* Toggle whether or not to check for translators comments for text string containing placeholders.
*
* Intended to make this part of the sniff unit testable, but can be used by end-users too,
* though they can just as easily disable this via the sniff code.
*
* @since 0.11.0
*
* @var bool
*/
public $check_translator_comments = true;
/**
* Whether or not the `default` text domain is one of the allowed text domains.
*
* @since 0.14.0
*
* @var bool
*/
private $text_domain_contains_default = false;
/**
* Whether or not the `default` text domain is the only allowed text domain.
*
* @since 0.14.0
*
* @var bool
*/
private $text_domain_is_default = false;
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'i18n' => array(
'functions' => array_keys( $this->i18n_functions ),
),
'typos' => array(
'functions' => array(
'_',
),
),
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 1.0.0 Defers to the abstractFunctionRestriction sniff for determining
* whether something is a function call. The logic after that has
* been split off to the `process_matched_token()` method.
*
* @param int $stack_ptr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stack_ptr ) {
// Reset defaults.
$this->text_domain_contains_default = false;
$this->text_domain_is_default = false;
// Allow overruling the text_domain set in a ruleset via the command line.
$cl_text_domain = trim( PHPCSHelper::get_config_data( 'text_domain' ) );
if ( ! empty( $cl_text_domain ) ) {
$this->text_domain = array_filter( array_map( 'trim', explode( ',', $cl_text_domain ) ) );
}
$this->text_domain = $this->merge_custom_array( $this->text_domain, array(), false );
if ( ! empty( $this->text_domain ) ) {
if ( \in_array( 'default', $this->text_domain, true ) ) {
$this->text_domain_contains_default = true;
if ( \count( $this->text_domain ) === 1 ) {
$this->text_domain_is_default = true;
}
}
}
// Prevent exclusion of the i18n group.
$this->exclude = array();
parent::process_token( $stack_ptr );
}
/**
* Process a matched token.
*
* @since 1.0.0 Logic split off from the `process_token()` method.
*
* @param int $stack_ptr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_matched_token( $stack_ptr, $group_name, $matched_content ) {
$func_open_paren_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stack_ptr + 1 ), null, true );
if ( false === $func_open_paren_token
|| \T_OPEN_PARENTHESIS !== $this->tokens[ $func_open_paren_token ]['code']
|| ! isset( $this->tokens[ $func_open_paren_token ]['parenthesis_closer'] )
) {
// Live coding, parse error or not a function call.
return;
}
if ( 'typos' === $group_name && '_' === $matched_content ) {
$this->phpcsFile->addError( 'Found single-underscore "_()" function when double-underscore expected.', $stack_ptr, 'SingleUnderscoreGetTextFunction' );
return;
}
if ( \in_array( $matched_content, array( 'translate', 'translate_with_gettext_context' ), true ) ) {
$this->phpcsFile->addWarning( 'Use of the "%s()" function is reserved for low-level API usage.', $stack_ptr, 'LowLevelTranslationFunction', array( $matched_content ) );
}
$arguments_tokens = array();
$argument_tokens = array();
$tokens = $this->tokens;
// Look at arguments.
for ( $i = ( $func_open_paren_token + 1 ); $i < $this->tokens[ $func_open_paren_token ]['parenthesis_closer']; $i++ ) {
$this_token = $this->tokens[ $i ];
$this_token['token_index'] = $i;
if ( isset( Tokens::$emptyTokens[ $this_token['code'] ] ) ) {
continue;
}
if ( \T_COMMA === $this_token['code'] ) {
$arguments_tokens[] = $argument_tokens;
$argument_tokens = array();
continue;
}
// Merge consecutive single or double quoted strings (when they span multiple lines).
if ( isset( Tokens::$textStringTokens[ $this_token['code'] ] ) ) {
for ( $j = ( $i + 1 ); $j < $this->tokens[ $func_open_paren_token ]['parenthesis_closer']; $j++ ) {
if ( $this_token['code'] === $this->tokens[ $j ]['code'] ) {
$this_token['content'] .= $this->tokens[ $j ]['content'];
$i = $j;
} else {
break;
}
}
}
$argument_tokens[] = $this_token;
// Include everything up to and including the parenthesis_closer if this token has one.
if ( ! empty( $this_token['parenthesis_closer'] ) ) {
for ( $j = ( $i + 1 ); $j <= $this_token['parenthesis_closer']; $j++ ) {
$tokens[ $j ]['token_index'] = $j;
$argument_tokens[] = $tokens[ $j ];
}
$i = $this_token['parenthesis_closer'];
}
}
if ( ! empty( $argument_tokens ) ) {
$arguments_tokens[] = $argument_tokens;
}
unset( $argument_tokens );
$argument_assertions = array();
if ( 'simple' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'text',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
} elseif ( 'context' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'text',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'context',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
} elseif ( 'number' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'single',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'plural',
'tokens' => array_shift( $arguments_tokens ),
);
array_shift( $arguments_tokens );
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
} elseif ( 'number_context' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'single',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'plural',
'tokens' => array_shift( $arguments_tokens ),
);
array_shift( $arguments_tokens );
$argument_assertions[] = array(
'arg_name' => 'context',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
} elseif ( 'noopnumber' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'single',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'plural',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
} elseif ( 'noopnumber_context' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'single',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'plural',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'context',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
}
if ( ! empty( $arguments_tokens ) ) {
$this->phpcsFile->addError( 'Too many arguments for function "%s".', $func_open_paren_token, 'TooManyFunctionArgs', array( $matched_content ) );
}
foreach ( $argument_assertions as $argument_assertion_context ) {
if ( empty( $argument_assertion_context['tokens'][0] ) ) {
$argument_assertion_context['stack_ptr'] = $func_open_paren_token;
} else {
$argument_assertion_context['stack_ptr'] = $argument_assertion_context['tokens'][0]['token_index'];
}
$this->check_argument_tokens( $argument_assertion_context );
}
/*
* For _n*() calls, compare the singular and plural strings.
* If either of the arguments is missing, empty or has more than 1 token, skip out.
* An error for that will already have been reported via the `check_argument_tokens()` method.
*/
if ( false !== strpos( $this->i18n_functions[ $matched_content ], 'number' )
&& isset( $argument_assertions[0]['tokens'], $argument_assertions[1]['tokens'] )
&& count( $argument_assertions[0]['tokens'] ) === 1
&& count( $argument_assertions[1]['tokens'] ) === 1
) {
$single_context = $argument_assertions[0];
$plural_context = $argument_assertions[1];
$this->compare_single_and_plural_arguments( $stack_ptr, $single_context, $plural_context );
}
if ( true === $this->check_translator_comments ) {
$this->check_for_translator_comment( $stack_ptr, $argument_assertions );
}
}
/**
* Check if supplied tokens represent a translation text string literal.
*
* @param array $context Context (@todo needs better description).
* @return bool
*/
protected function check_argument_tokens( $context ) {
$stack_ptr = $context['stack_ptr'];
$tokens = $context['tokens'];
$arg_name = $context['arg_name'];
$is_error = empty( $context['warning'] );
$content = isset( $tokens[0] ) ? $tokens[0]['content'] : '';
if ( empty( $tokens ) || 0 === \count( $tokens ) ) {
$code = $this->string_to_errorcode( 'MissingArg' . ucfirst( $arg_name ) );
if ( 'domain' !== $arg_name ) {
$this->addMessage( 'Missing $%s arg.', $stack_ptr, $is_error, $code, array( $arg_name ) );
return false;
}
// Ok, we're examining a text domain, now deal correctly with the 'default' text domain.
if ( true === $this->text_domain_is_default ) {
return true;
}
if ( true === $this->text_domain_contains_default ) {
$this->phpcsFile->addWarning(
'Missing $%s arg. If this text string is supposed to use a WP Core translation, use the "default" text domain.',
$stack_ptr,
$code . 'Default',
array( $arg_name )
);
} elseif ( ! empty( $this->text_domain ) ) {
$this->addMessage( 'Missing $%s arg.', $stack_ptr, $is_error, $code, array( $arg_name ) );
}
return false;
}
if ( \count( $tokens ) > 1 ) {
$contents = '';
foreach ( $tokens as $token ) {
$contents .= $token['content'];
}
$code = $this->string_to_errorcode( 'NonSingularStringLiteral' . ucfirst( $arg_name ) );
$this->addMessage( 'The $%s arg must be a single string literal, not "%s".', $stack_ptr, $is_error, $code, array( $arg_name, $contents ) );
return false;
}
if ( \in_array( $arg_name, array( 'text', 'single', 'plural' ), true ) ) {
$this->check_text( $context );
}
if ( \T_DOUBLE_QUOTED_STRING === $tokens[0]['code'] || \T_HEREDOC === $tokens[0]['code'] ) {
$interpolated_variables = $this->get_interpolated_variables( $content );
foreach ( $interpolated_variables as $interpolated_variable ) {
$code = $this->string_to_errorcode( 'InterpolatedVariable' . ucfirst( $arg_name ) );
$this->addMessage( 'The $%s arg must not contain interpolated variables. Found "$%s".', $stack_ptr, $is_error, $code, array( $arg_name, $interpolated_variable ) );
}
if ( ! empty( $interpolated_variables ) ) {
return false;
}
}
if ( isset( Tokens::$textStringTokens[ $tokens[0]['code'] ] ) ) {
if ( 'domain' === $arg_name && ! empty( $this->text_domain ) ) {
$stripped_content = $this->strip_quotes( $content );
if ( ! \in_array( $stripped_content, $this->text_domain, true ) ) {
$this->addMessage(
'Mismatched text domain. Expected \'%s\' but got %s.',
$stack_ptr,
$is_error,
'TextDomainMismatch',
array( implode( "' or '", $this->text_domain ), $content )
);
return false;
}
if ( true === $this->text_domain_is_default && 'default' === $stripped_content ) {
$fixable = false;
$error = 'No need to supply the text domain when the only accepted text domain is "default".';
$error_code = 'SuperfluousDefaultTextDomain';
if ( $tokens[0]['token_index'] === $stack_ptr ) {
$prev = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $stack_ptr - 1 ), null, true );
if ( false !== $prev && \T_COMMA === $this->tokens[ $prev ]['code'] ) {
$fixable = true;
}
}
if ( false === $fixable ) {
$this->phpcsFile->addWarning( $error, $stack_ptr, $error_code );
return false;
}
$fix = $this->phpcsFile->addFixableWarning( $error, $stack_ptr, $error_code );
if ( true === $fix ) {
// Remove preceeding comma, whitespace and the text domain token.
$this->phpcsFile->fixer->beginChangeset();
for ( $i = $prev; $i <= $stack_ptr; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
return false;
}
}
return true;
}
$code = $this->string_to_errorcode( 'NonSingularStringLiteral' . ucfirst( $arg_name ) );
$this->addMessage( 'The $%s arg must be a single string literal, not "%s".', $stack_ptr, $is_error, $code, array( $arg_name, $content ) );
return false;
}
/**
* Check for inconsistencies between single and plural arguments.
*
* @param int $stack_ptr The position of the current token in the stack.
* @param array $single_context Single context (@todo needs better description).
* @param array $plural_context Plural context (@todo needs better description).
* @return void
*/
protected function compare_single_and_plural_arguments( $stack_ptr, $single_context, $plural_context ) {
$single_content = $single_context['tokens'][0]['content'];
$plural_content = $plural_context['tokens'][0]['content'];
preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $single_content, $single_placeholders );
$single_placeholders = $single_placeholders[0];
preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $plural_content, $plural_placeholders );
$plural_placeholders = $plural_placeholders[0];
// English conflates "singular" with "only one", described in the codex:
// https://codex.wordpress.org/I18n_for_WordPress_Developers#Plurals .
if ( \count( $single_placeholders ) < \count( $plural_placeholders ) ) {
$error_string = 'Missing singular placeholder, needed for some languages. See https://codex.wordpress.org/I18n_for_WordPress_Developers#Plurals';
$single_index = $single_context['tokens'][0]['token_index'];
$this->phpcsFile->addError( $error_string, $single_index, 'MissingSingularPlaceholder' );
}
// Reordering is fine, but mismatched placeholders is probably wrong.
sort( $single_placeholders );
sort( $plural_placeholders );
if ( $single_placeholders !== $plural_placeholders ) {
$this->phpcsFile->addWarning( 'Mismatched placeholders is probably an error', $stack_ptr, 'MismatchedPlaceholders' );
}
}
/**
* Check the string itself for problems.
*
* @param array $context Context (@todo needs better description).
* @return void
*/
protected function check_text( $context ) {
$stack_ptr = $context['stack_ptr'];
$arg_name = $context['arg_name'];
$content = $context['tokens'][0]['content'];
$is_error = empty( $context['warning'] );
// UnorderedPlaceholders: Check for multiple unordered placeholders.
$unordered_matches_count = preg_match_all( self::UNORDERED_SPRINTF_PLACEHOLDER_REGEX, $content, $unordered_matches );
$unordered_matches = $unordered_matches[0];
$all_matches_count = preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $content, $all_matches );
if ( $unordered_matches_count > 0 && $unordered_matches_count !== $all_matches_count && $all_matches_count > 1 ) {
$code = $this->string_to_errorcode( 'MixedOrderedPlaceholders' . ucfirst( $arg_name ) );
$this->phpcsFile->addError(
'Multiple placeholders should be ordered. Mix of ordered and non-ordered placeholders found. Found: %s.',
$stack_ptr,
$code,
array( implode( ', ', $all_matches[0] ) )
);
} elseif ( $unordered_matches_count >= 2 ) {
$code = $this->string_to_errorcode( 'UnorderedPlaceholders' . ucfirst( $arg_name ) );
$suggestions = array();
$replace_regexes = array();
$replacements = array();
for ( $i = 0; $i < $unordered_matches_count; $i++ ) {
$to_insert = ( $i + 1 );
$to_insert .= ( '"' !== $content[0] ) ? '$' : '\$';
$suggestions[ $i ] = substr_replace( $unordered_matches[ $i ], $to_insert, 1, 0 );
// Prepare the strings for use a regex.
$replace_regexes[ $i ] = '`\Q' . $unordered_matches[ $i ] . '\E`';
// Note: the initial \\ is a literal \, the four \ in the replacement translate to also to a literal \.
$replacements[ $i ] = str_replace( '\\', '\\\\', $suggestions[ $i ] );
// Note: the $ needs escaping to prevent numeric sequences after the $ being interpreted as match replacements.
$replacements[ $i ] = str_replace( '$', '\\$', $replacements[ $i ] );
}
$fix = $this->addFixableMessage(
'Multiple placeholders should be ordered. Expected \'%s\', but got %s.',
$stack_ptr,
$is_error,
$code,
array( implode( ', ', $suggestions ), implode( ', ', $unordered_matches ) )
);
if ( true === $fix ) {
$fixed_str = preg_replace( $replace_regexes, $replacements, $content, 1 );
$this->phpcsFile->fixer->replaceToken( $stack_ptr, $fixed_str );
}
}
/*
* NoEmptyStrings.
*
* Strip placeholders and surrounding quotes.
*/
$content_without_quotes = trim( $this->strip_quotes( $content ) );
$non_placeholder_content = preg_replace( self::SPRINTF_PLACEHOLDER_REGEX, '', $content_without_quotes );
if ( '' === $non_placeholder_content ) {
$this->phpcsFile->addError( 'Strings should have translatable content', $stack_ptr, 'NoEmptyStrings' );
return;
}
/*
* NoHtmlWrappedStrings
*
* Strip surrounding quotes.
*/
$reader = new \XMLReader();
$reader->XML( $content_without_quotes, 'UTF-8', LIBXML_NOERROR | LIBXML_ERR_NONE | LIBXML_NOWARNING );
// Is the first node an HTML element?
if ( ! $reader->read() || \XMLReader::ELEMENT !== $reader->nodeType ) {
return;
}
// If the opening HTML element includes placeholders in its attributes, we don't warn.
// E.g. '<option id="%1$s" value="%2$s">Translatable option name</option>'.
$i = 0;
while ( $attr = $reader->getAttributeNo( $i ) ) {
if ( preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $attr ) === 1 ) {
return;
}
++$i;
}
// We don't flag strings wrapped in `<a href="...">...</a>`, as the link target might actually need localization.
if ( 'a' === $reader->name && $reader->getAttribute( 'href' ) ) {
return;
}
// Does the entire string only consist of this HTML node?
if ( $reader->readOuterXml() === $content_without_quotes ) {
$this->phpcsFile->addWarning( 'Strings should not be wrapped in HTML', $stack_ptr, 'NoHtmlWrappedStrings' );
}
}
/**
* Check for the presence of a translators comment if one of the text strings contains a placeholder.
*
* @param int $stack_ptr The position of the gettext call token in the stack.
* @param array $args The function arguments.
* @return void
*/
protected function check_for_translator_comment( $stack_ptr, $args ) {
foreach ( $args as $arg ) {
if ( false === \in_array( $arg['arg_name'], array( 'text', 'single', 'plural' ), true ) ) {
continue;
}
if ( empty( $arg['tokens'] ) ) {
continue;
}
foreach ( $arg['tokens'] as $token ) {
if ( empty( $token['content'] ) ) {
continue;
}
if ( preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $token['content'], $placeholders ) < 1 ) {
// No placeholders found.
continue;
}
$previous_comment = $this->phpcsFile->findPrevious( Tokens::$commentTokens, ( $stack_ptr - 1 ) );
if ( false !== $previous_comment ) {
/*
* Check that the comment is either on the line before the gettext call or
* if it's not, that there is only whitespace between.
*/
$correctly_placed = false;
if ( ( $this->tokens[ $previous_comment ]['line'] + 1 ) === $this->tokens[ $stack_ptr ]['line'] ) {
$correctly_placed = true;
} else {
$next_non_whitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $previous_comment + 1 ), $stack_ptr, true );
if ( false === $next_non_whitespace || $this->tokens[ $next_non_whitespace ]['line'] === $this->tokens[ $stack_ptr ]['line'] ) {
// No non-whitespace found or next non-whitespace is on same line as gettext call.
$correctly_placed = true;
}
unset( $next_non_whitespace );
}
/*
* Check that the comment starts with 'translators:'.
*/
if ( true === $correctly_placed ) {
if ( \T_COMMENT === $this->tokens[ $previous_comment ]['code'] ) {
$comment_text = trim( $this->tokens[ $previous_comment ]['content'] );
// If it's multi-line /* */ comment, collect all the parts.
if ( '*/' === substr( $comment_text, -2 ) && '/*' !== substr( $comment_text, 0, 2 ) ) {
for ( $i = ( $previous_comment - 1 ); 0 <= $i; $i-- ) {
if ( \T_COMMENT !== $this->tokens[ $i ]['code'] ) {
break;
}
$comment_text = trim( $this->tokens[ $i ]['content'] ) . $comment_text;
}
}
if ( true === $this->is_translators_comment( $comment_text ) ) {
// Comment is ok.
return;
}
} elseif ( \T_DOC_COMMENT_CLOSE_TAG === $this->tokens[ $previous_comment ]['code'] ) {
// If it's docblock comment (wrong style) make sure that it's a translators comment.
$db_start = $this->phpcsFile->findPrevious( \T_DOC_COMMENT_OPEN_TAG, ( $previous_comment - 1 ) );
$db_first_text = $this->phpcsFile->findNext( \T_DOC_COMMENT_STRING, ( $db_start + 1 ), $previous_comment );
if ( true === $this->is_translators_comment( $this->tokens[ $db_first_text ]['content'] ) ) {
$this->phpcsFile->addWarning(
'A "translators:" comment must be a "/* */" style comment. Docblock comments will not be picked up by the tools to generate a ".pot" file.',
$stack_ptr,
'TranslatorsCommentWrongStyle'
);
return;
}
}
}
}
// Found placeholders but no translators comment.
$this->phpcsFile->addWarning(
'A gettext call containing placeholders was found, but was not accompanied by a "translators:" comment on the line above to clarify the meaning of the placeholders.',
$stack_ptr,
'MissingTranslatorsComment'
);
return;
}
}
}
/**
* Check if a (collated) comment string starts with 'translators:'.
*
* @since 0.11.0
*
* @param string $content Comment string content.
* @return bool
*/
private function is_translators_comment( $content ) {
if ( preg_match( '`^(?:(?://|/\*{1,2}) )?translators:`i', $content, $matches ) === 1 ) {
return true;
}
return false;
}
}
@@ -0,0 +1,78 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff;
/**
* Flag returning high or infinite posts_per_page.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#no-limit-queries
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 0.14.0 Added the posts_per_page property.
* @since 1.0.0 This sniff has been split into two, with the check for high pagination
* limit being part of the WP category, and the check for pagination
* disabling being part of the VIP category.
*/
class PostsPerPageSniff extends AbstractArrayAssignmentRestrictionsSniff {
/**
* Posts per page property
*
* Posts per page limit to check against.
*
* @since 0.14.0
*
* @var int
*/
public $posts_per_page = 100;
/**
* Groups of variables to restrict.
*
* @return array
*/
public function getGroups() {
return array(
'posts_per_page' => array(
'type' => 'warning',
'keys' => array(
'posts_per_page',
'numberposts',
),
),
);
}
/**
* Callback to process each confirmed key, to check value.
*
* @param string $key Array index / key.
* @param mixed $val Assigned value.
* @param int $line Token line.
* @param array $group Group definition.
* @return mixed FALSE if no match, TRUE if matches, STRING if matches
* with custom error message passed to ->process().
*/
public function callback( $key, $val, $line, $group ) {
$this->posts_per_page = (int) $this->posts_per_page;
if ( $val > $this->posts_per_page ) {
return 'Detected high pagination limit, `%s` is set to `%s`';
}
return false;
}
}
@@ -0,0 +1,88 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\Sniffs\DateTime\RestrictedFunctionsSniff;
/**
* Disallow the changing of timezone.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#manipulating-the-timezone-server-side
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.11.0 Extends the WordPressCS native `AbstractFunctionRestrictionsSniff`
* class instead of the upstream `Generic.PHP.ForbiddenFunctions` sniff.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This sniff has been moved from the `VIP` category to the `WP` category.
*
* @deprecated 2.2.0 Use the `WordPress.DateTime.RestrictedFunctions` sniff instead.
* This `WordPress.WP.TimezoneChange` sniff will be removed in WPCS 3.0.0.
*/
class TimezoneChangeSniff extends RestrictedFunctionsSniff {
/**
* Keep track of whether the warnings have been thrown to prevent
* the messages being thrown for every token triggering the sniff.
*
* @since 2.2.0
*
* @var array
*/
private $thrown = array(
'DeprecatedSniff' => false,
'FoundPropertyForDeprecatedSniff' => false,
);
/**
* Don't use.
*
* @deprecated 2.2.0
*
* @return array
*/
public function getGroups() {
$groups = parent::getGroups();
return array( 'timezone_change' => $groups['timezone_change'] );
}
/**
* Don't use.
*
* @since 2.2.0 Added to allow for throwing the deprecation notices.
* @deprecated 2.2.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void|int
*/
public function process_token( $stackPtr ) {
if ( false === $this->thrown['DeprecatedSniff'] ) {
$this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning(
'The "WordPress.WP.TimezoneChange" sniff has been deprecated. Use the "WordPress.DateTime.RestrictedFunctions" sniff instead. Please update your custom ruleset.',
0,
'DeprecatedSniff'
);
}
if ( ! empty( $this->exclude )
&& false === $this->thrown['FoundPropertyForDeprecatedSniff']
) {
$this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning(
'The "WordPress.WP.TimezoneChange" sniff has been deprecated. Use the "WordPress.DateTime.RestrictedFunctions" sniff instead. "exclude" property setting found. Please update your custom ruleset.',
0,
'FoundPropertyForDeprecatedSniff'
);
}
return parent::process_token( $stackPtr );
}
}