基础代码

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,69 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\CoreFunctionality;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
/**
* Check if a theme uses include(_once) or require(_once) when get_template_part() should be used.
*
* @link https://make.wordpress.org/themes/handbook/review/required/#core-functionality-and-features
*
* @since 0.1.0
*/
class FileIncludeSniff implements Sniff {
/**
* A list of files to skip.
*
* @var array
*/
protected $file_whitelist = [
'functions.php' => true,
];
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return Tokens::$includeTokens;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $stackPtr The position of the current token
* in the stack.
*
* @return void
*/
public function process( File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();
$token = $tokens[ $stackPtr ];
$file_name = basename( $phpcsFile->getFileName() );
if ( ! isset( $this->file_whitelist[ $file_name ] ) ) {
$phpcsFile->addWarning(
'Check that %s is not being used to load template files. "get_template_part()" should be used to load template files.',
$stackPtr,
'FileIncludeFound',
[ $token['content'] ]
);
}
}
}
@@ -0,0 +1,234 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\CoreFunctionality;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Forbids deregistering of core scripts (javascript).
*
* @link https://make.wordpress.org/themes/handbook/review/required/#stylesheets-and-scripts
*
* @since 0.1.0
*/
class NoDeregisterCoreScriptSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 0.1.0
*
* @var string
*/
protected $group_name = 'wp_deregister_script';
/**
* Array of function and script handles used in core.
*
* {@internal List was probably based on the documentation of the wp_enqueue_script function.
* List needs further verification and update.
*
* @link https://developer.wordpress.org/reference/functions/wp_enqueue_script/}}
*
* @since 0.1.0
*
* @var array
*/
protected $target_functions = [
'wp_deregister_script' => [
'jcrop' => true,
'swfobject' => true,
'swfupload' => true,
'swfupload-degrade' => true,
'swfupload-queue' => true,
'swfupload-handlers' => true,
'jquery' => true,
'jquery-form' => true,
'jquery-color' => true,
'jquery-masonry' => true,
'masonry' => true,
'jquery-ui-core' => true,
'jquery-ui-widget' => true,
'jquery-ui-accordion' => true,
'jquery-ui-autocomplete' => true,
'jquery-ui-button' => true,
'jquery-ui-datepicker' => true,
'jquery-ui-dialog' => true,
'jquery-ui-draggable' => true,
'jquery-ui-droppable' => true,
'jquery-ui-menu' => true,
'jquery-ui-mouse' => true,
'jquery-ui-position' => true,
'jquery-ui-progressbar' => true,
'jquery-ui-selectable' => true,
'jquery-ui-resizable' => true,
'jquery-ui-selectmenu' => true,
'jquery-ui-sortable' => true,
'jquery-ui-slider' => true,
'jquery-ui-spinner' => true,
'jquery-ui-tooltip' => true,
'jquery-ui-tabs' => true,
'jquery-effects-core' => true,
'jquery-effects-blind' => true,
'jquery-effects-bounce' => true,
'jquery-effects-clip' => true,
'jquery-effects-drop' => true,
'jquery-effects-explode' => true,
'jquery-effects-fade' => true,
'jquery-effects-fold' => true,
'jquery-effects-highlight' => true,
'jquery-effects-pulsate' => true,
'jquery-effects-scale' => true,
'jquery-effects-shake' => true,
'jquery-effects-slide' => true,
'jquery-effects-transfer' => true,
'wp-mediaelement' => true,
'schedule' => true,
'suggest' => true,
'thickbox' => true,
'hoverIntent' => true,
'jquery-hotkeys' => true,
'sack' => true,
'quicktags' => true,
'iris' => true,
'farbtastic' => true,
'colorpicker' => true,
'tiny_mce' => true,
'autosave' => true,
'wp-ajax-response' => true,
'wp-lists' => true,
'common' => true,
'editorremov' => true,
'editor-functions' => true,
'ajaxcat' => true,
'admin-categories' => true,
'admin-tags' => true,
'admin-custom-fields' => true,
'password-strength-meter' => true,
'admin-comments' => true,
'admin-users' => true,
'admin-forms' => true,
'xfn' => true,
'upload' => true,
'postbox' => true,
'slug' => true,
'post' => true,
'page' => true,
'link' => true,
'comment' => true,
'comment-reply' => true,
'admin-gallery' => true,
'media-upload' => true,
'admin-widgets' => true,
'word-count' => true,
'theme-preview' => true,
'json2' => true,
'plupload' => true,
'plupload-all' => true,
'plupload-html4' => true,
'plupload-html5' => true,
'plupload-flash' => true,
'plupload-silverlight' => true,
'underscore' => true,
'backbone' => true,
'imagesloaded' => true,
],
];
/**
* Process the parameters of a matched function.
*
* @since 0.1.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param array $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 ) {
if ( ! isset( $parameters[1] ) ) {
return;
}
$matched_parameter = $this->strip_quotes( $parameters[1]['raw'] );
$first_param_token = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$parameters[1]['start'],
( $parameters[1]['end'] + 1 ),
true
);
if ( isset( $this->target_functions[ $matched_content ][ $matched_parameter ] ) ) {
$this->throw_prohibited_error( $first_param_token, [ $matched_parameter ] );
return;
}
// More extensive check to prevent people circumventing the sniff.
$text = '';
$found_variable_token = false;
for ( $i = $parameters[1]['start']; $i <= $parameters[1]['end']; $i++ ) {
if ( isset( Tokens::$stringTokens[ $this->tokens[ $i ]['code'] ] ) === false ) {
if ( T_VARIABLE === $this->tokens[ $i ]['code']
|| T_STRING === $this->tokens[ $i ]['code']
) {
$found_variable_token = true;
}
continue;
}
$text .= $this->strip_quotes( $this->tokens[ $i ]['content'] );
}
if ( isset( $this->target_functions[ $matched_content ][ $text ] ) ) {
$this->throw_prohibited_error( $first_param_token, [ $text ] );
return;
}
if ( true === $found_variable_token ) {
$this->throw_variable_handle_warning( $first_param_token, [ $matched_content, $matched_parameter ] );
}
}
/**
* Throw the error for deregistering a core script.
*
* @param int $stackPtr The position of the first non-empty parameter token in the stack.
* @param array $data Optional input for the data replacements.
*/
public function throw_prohibited_error( $stackPtr, $data = [] ) {
$this->phpcsFile->addError(
'Deregistering core script "%s" is prohibited.',
$stackPtr,
'Found',
$data
);
}
/**
* Throw a warning when a variable handle for deregistering a script is detected.
*
* @param int $stackPtr The position of the first non-empty parameter token in the stack.
* @param array $data Optional input for the data replacements.
*/
public function throw_variable_handle_warning( $stackPtr, $data = [] ) {
$this->phpcsFile->addWarning(
'Deregistering core scripts is prohibited. A variable script handle was found. Inspection of the %s() call needed. Found: %s',
$stackPtr,
'VariableHandleFound',
$data
);
}
}
@@ -0,0 +1,117 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\CoreFunctionality;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
/**
* Check for hardcoded favicons instead of using core implementation.
*
* @link https://make.wordpress.org/themes/handbook/review/required/#core-functionality-and-features
*
* @since 0.1.0
*/
class NoFaviconSniff implements Sniff {
/**
* Regex template.
*
* Will be parsed together with the attribute blacklist in the register() method.
*
* @var string
*/
const REGEX_TEMPLATE = '` (?:%s)`i';
/**
* (Partial) Regex template for recognizing a HTML attribute.
*
* Will be parsed together with the attribute blacklist in the register() method.
*
* @var string
*/
const REGEX_ATTR_TEMPLATE = '%1$s=[\'"](?:%2$s)[\'"]';
/**
* List of link and meta attributes that are blacklisted.
*
* @var array
*/
protected $attribute_blacklist = [
'rel' => [
'icon',
'shortcut icon',
'bookmark icon',
'apple-touch-icon',
'apple-touch-icon-precomposed',
],
'name' => [
'msapplication-config',
'msapplication-TileImage',
'msapplication-square70x70logo',
'msapplication-square150x150logo',
'msapplication-wide310x150logo',
'msapplication-square310x310logo',
],
];
/**
* The regex to catch the blacklisted attributes.
*
* @var string
*/
protected $favicon_regex;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
// Build the regex to be used only once.
$regex_parts = [];
foreach ( $this->attribute_blacklist as $key => $values ) {
$values = array_map( 'preg_quote', $values, array_fill( 0, count( $values ), '`' ) );
$values = implode( '|', $values );
$regex_parts[] = sprintf( self::REGEX_ATTR_TEMPLATE, preg_quote( $key, '`' ), $values );
}
$this->favicon_regex = sprintf( self::REGEX_TEMPLATE, implode( '|', $regex_parts ) );
return Tokens::$textStringTokens;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $stackPtr The position of the current token
* in the stack.
*
* @return void
*/
public function process( File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();
$token = $tokens[ $stackPtr ];
if ( preg_match( $this->favicon_regex, $token['content'] ) > 0 ) {
$phpcsFile->addError(
'Code for favicon found. Favicons are handled by the "Site Icon" setting in the customizer since WP version 4.3.',
$stackPtr,
'NoFavicon'
);
}
}
}
@@ -0,0 +1,103 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\CoreFunctionality;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
/**
* Restricts the use of the <title> tag, unless it is within a <svg> tag.
*
* @link https://make.wordpress.org/themes/handbook/review/required/
*
* @since 0.1.0
*/
class NoTitleTagSniff implements Sniff {
/**
* Property to keep track of whether a <svg> open tag has been encountered.
*
* @var array
*/
private $in_svg;
/**
* 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 \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $stackPtr The position of the current token
* in the stack.
*
* @return void
*/
public function process( File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();
$content = $tokens[ $stackPtr ]['content'];
$filename = $phpcsFile->getFileName();
// Set to false if it is the first time this sniff is run on a file.
if ( ! isset( $this->in_svg[ $filename ] ) ) {
$this->in_svg[ $filename ] = false;
}
// No need to check an empty string.
if ( '' === trim( $content ) ) {
return;
}
// Are we in a <svg> tag ?
if ( true === $this->in_svg[ $filename ] ) {
if ( false === strpos( $content, '</svg>' ) ) {
return;
} else {
// Make sure we check any content on this line after the closing svg tag.
$this->in_svg[ $filename ] = false;
$content = trim( substr( $content, ( strpos( $content, '</svg>' ) + 6 ) ) );
}
}
// We're not in svg, so check if it there's a <svg> open tag on this line.
if ( false !== strpos( $content, '<svg' ) ) {
if ( false === strpos( $content, '</svg>' ) ) {
// Skip the next lines until the closing svg tag, but do check any content
// on this line before the svg tag.
$this->in_svg[ $filename ] = true;
$content = trim( substr( $content, 0, ( strpos( $content, '<svg' ) ) ) );
} else {
// Ok, we have open and close svg tag on the same line with possibly content before and/or after.
$before = trim( substr( $content, 0, ( strpos( $content, '<svg' ) ) ) );
$after = trim( substr( $content, ( strpos( $content, '</svg>' ) + 6 ) ) );
$content = $before . $after;
}
}
// Now let's do the check for the <title> tag.
if ( false !== strpos( $content, '<title' ) ) {
$phpcsFile->addError(
"The title tag must not be used. Use add_theme_support( 'title-tag' ) instead.",
$stackPtr,
'TagFound'
);
}
}
}
@@ -0,0 +1,51 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\CoreFunctionality;
use WordPressCS\WordPress\Sniffs\WP\PostsPerPageSniff as WPCSPostsPerPageSniff;
/**
* Flag returning high or infinite posts_per_page.
*
* This sniff extends the upstream WPCS PostsPerPageSniff. The difference is that this
* sniff will not only warn against a high pagination limit, but will also warn against
* using -1 as posts_per_page setting while querying posts, due to detrimental effects
* it has on query speed.
*
* @link https://github.com/WPTRT/WPThemeReview/issues/147
*
* @since 0.2.0
*/
class PostsPerPageSniff extends WPCSPostsPerPageSniff {
/**
* 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 ( '-1' === $val ) {
return 'Disabling pagination is not advised, avoid setting `%s` to `%s`';
}
if ( $val > $this->posts_per_page ) {
return 'Detected high pagination limit, `%s` is set to `%s`';
}
return false;
}
}
@@ -0,0 +1,191 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\CoreFunctionality;
use WordPressCS\WordPress\Sniffs\NamingConventions\PrefixAllGlobalsSniff as WPCSPrefixAllGlobalsSniff;
/**
* Verify that everything defined in the global namespace is prefixed with a theme specific prefix.
*
* This sniff extends the upstream WPCS PrefixAllGlobalsSniff. The differences are:
* - For non-prefixed global variables, an error will only be thrown when the variable
* is created outside of a theme template file.
*
* @link https://github.com/WPTRT/WPThemeReview/issues/205
* @link https://github.com/WPTRT/WPThemeReview/issues/201
* @link https://github.com/WPTRT/WPThemeReview/issues/200
*
* The sniff does not currently allow for mimetype sublevel only file names,
* such as `plain.php`.
*
* @since 0.2.0
*/
class PrefixAllGlobalsSniff extends WPCSPrefixAllGlobalsSniff {
/**
* Regex to recognize more complex typical theme template file names.
*
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#single-post
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#custom-taxonomies
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#custom-post-types
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#embeds
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#attachment
* @link https://developer.wordpress.org/themes/template-files-section/partial-and-miscellaneous-template-files/#content-slug-php
* @link https://wphierarchy.com/
* @link https://en.wikipedia.org/wiki/Media_type#Naming
*
* @since 0.2.0
*
* @var string
*/
const COMPLEX_THEME_TEMPLATE_NAME_REGEX = '`
^ # Anchor to the beginning of the string.
(?:
# Template file prefixes with subtype.
(?:archive|author|category|content|embed|page|single|tag|taxonomy)
-[^\.]+ # These need to be followed by a dash and some chars.
|
(?:application|audio|example|font|image|message|model|multipart|text|video) #Top-level mime-types
(?:_[^\.]+)? # Optionally followed by an underscore and a sub-type.
)\.php$ # End in .php and anchor to the end of the string.
`Dx';
/**
* The list of allowed folders to check the file path against.
*
* The WPThemereview standards contains a base set for this property in the ruleset.xml.
* This array can be extended in a custom ruleset.
*
* @since 0.2.0
*
* @var array
*/
public $allowed_folders = [];
/**
* List of plain template file names.
*
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/
* @link https://developer.wordpress.org/themes/template-files-section/partial-and-miscellaneous-template-files/#content-slug-php
* @link https://wphierarchy.com/
* @link https://en.wikipedia.org/wiki/Media_type#Naming
*
* @since 0.2.0
*
* @var array
*/
protected $simple_theme_template_file_names = [
// Plain primary template file names.
'404.php' => true,
'archive.php' => true,
'home.php' => true,
'index.php' => true,
'page.php' => true,
'search.php' => true,
'single.php' => true,
'singular.php' => true,
// Plain secondary template file names.
'attachment.php' => true,
'author.php' => true,
'category.php' => true,
'date.php' => true,
'embed.php' => true,
'front-page.php' => true,
'single-post.php' => true,
'tag.php' => true,
'taxonomy.php' => true,
// Plain partial and miscellaneous template file names.
'comments.php' => true,
'footer.php' => true,
'header.php' => true,
'sidebar.php' => true,
// Top-leve mime types.
'application.php' => true,
'audio.php' => true,
'example.php' => true,
'font.php' => true,
'image.php' => true,
'message.php' => true,
'model.php' => true,
'multipart.php' => true,
'text.php' => true,
'video.php' => true,
];
/**
* Check that defined global variables are prefixed.
*
* This overloads the parent method to allow for non-prefixed variables to be declared
* in template files as those are included from within a function and therefore would be
* local to that function.
*
* @since 0.2.0
* @since 0.2.1 Added $in_list parameter as introduced in WPCS 2.2.0.
*
* @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 int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
protected function process_variable_assignment( $stackPtr, $in_list = false ) {
// Usage of `strip_quotes` is to ensure `stdin_path` passed by IDEs does not include quotes.
$file = $this->strip_quotes( $this->phpcsFile->getFileName() );
$fileName = basename( $file );
if ( \defined( '\PHP_CODESNIFFER_IN_TESTS' ) ) {
$fileName = str_replace( '.inc', '.php', $fileName );
}
// Don't process in case of a template file or folder.
if ( isset( $this->simple_theme_template_file_names[ $fileName ] ) === true ) {
return;
}
if ( preg_match( self::COMPLEX_THEME_TEMPLATE_NAME_REGEX, $fileName ) === 1 ) {
return;
}
if ( $this->is_from_allowed_folder( $file ) ) {
return;
}
// Not a typical template file name, defer to the prefix checking in the parent sniff.
return parent::process_variable_assignment( $stackPtr, $in_list );
}
/**
* Checks if the given file path is located in the $allowed_folders array.
*
* @since 0.2.0
*
* @param string $path Full path of the sniffed file.
* @return boolean
*/
private function is_from_allowed_folder( $path ) {
if ( empty( $this->allowed_folders ) || ! is_array( $this->allowed_folders ) ) {
return false;
}
foreach ( $this->allowed_folders as $folder ) {
if ( strrpos( $path, DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR ) !== false ) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,417 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\PluginTerritory;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Discourages removal of the admin bar.
*
* @link https://make.wordpress.org/themes/handbook/review/required/#core-functionality-and-features
*
* @since WPCS 0.3.0
* @since WPCS 0.11.0 - Extends the WPCS native `AbstractFunctionParameterSniff` class.
* - Added the $remove_only property.
* - Now also sniffs for manipulation of the admin bar visibility through CSS.
* @since WPCS 0.13.0 Class name changed: this class is now namespaced.
*
* @since TRTCS 0.1.0 As this sniff will be removed from WPCS in version 2.0, the
* sniff has been cherry-picked into the WPThemeReview standard.
*/
class AdminBarRemovalSniff extends AbstractFunctionParameterSniff {
/**
* A list of tokenizers this sniff supports.
*
* @since WPCS 0.11.0
*
* @var array
*/
public $supportedTokenizers = [ 'PHP', 'CSS' ];
/**
* Whether or not the sniff only checks for removal of the admin bar
* or any manipulation to the visibility of the admin bar.
*
* Defaults to true: only check for removal of the admin bar.
* Set to false to check for any form of manipulation of the visibility
* of the admin bar.
*
* @since WPCS 0.11.0
*
* @var bool
*/
public $remove_only = true;
/**
* Functions this sniff is looking for.
*
* @since WPCS 0.11.0
*
* @var array
*/
protected $target_functions = [
'show_admin_bar' => true,
'add_filter' => true,
];
/**
* CSS properties this sniff is looking for.
*
* @since WPCS 0.11.0
*
* @var array
*/
protected $target_css_properties = [
'visibility' => [
'type' => '!=',
'value' => 'hidden',
],
'display' => [
'type' => '!=',
'value' => 'none',
],
'opacity' => [
'type' => '>',
'value' => 0.3,
],
];
/**
* CSS selectors this sniff is looking for.
*
* @since WPCS 0.11.0
*
* @var array
*/
protected $target_css_selectors = [
'.show-admin-bar',
'#wpadminbar',
];
/**
* Regex template for use with the CSS selectors in combination with PHP text strings.
*
* @since WPCS 0.11.0
*
* @var string
*/
private $target_css_selectors_regex = '`(?:%s).*?\{(.*)$`';
/**
* Property to keep track of whether a <style> open tag has been encountered.
*
* @since WPCS 0.11.0
*
* @var array
*/
private $in_style;
/**
* Property to keep track of whether a one of the target selectors has been encountered.
*
* @since WPCS 0.11.0
*
* @var array
*/
private $in_target_selector;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
$targets = Tokens::$textStringTokens;
// Add CSS style target.
$targets[] = \T_STYLE;
// Set the target selectors regex only once.
$selectors = array_map(
'preg_quote',
$this->target_css_selectors,
array_fill( 0, \count( $this->target_css_selectors ), '`' )
);
// Parse the selectors array into the regex string.
$this->target_css_selectors_regex = sprintf( $this->target_css_selectors_regex, implode( '|', $selectors ) );
// Add function call targets.
$parent = parent::register();
if ( ! empty( $parent ) ) {
$targets[] = \T_STRING;
}
return $targets;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
$file_name = $this->phpcsFile->getFileName();
$file_extension = substr( strrchr( $file_name, '.' ), 1 );
if ( 'css' === $file_extension ) {
if ( \T_STYLE === $this->tokens[ $stackPtr ]['code'] ) {
return $this->process_css_style( $stackPtr );
}
} elseif ( isset( Tokens::$textStringTokens[ $this->tokens[ $stackPtr ]['code'] ] ) ) {
/*
* Set $in_style && $in_target_selector to false if it is the first time
* this sniff is run on a file.
*/
if ( ! isset( $this->in_style[ $file_name ] ) ) {
$this->in_style[ $file_name ] = false;
}
if ( ! isset( $this->in_target_selector[ $file_name ] ) ) {
$this->in_target_selector[ $file_name ] = false;
}
return $this->process_text_for_style( $stackPtr, $file_name );
} else {
return parent::process_token( $stackPtr );
}
}
/**
* Process the parameters of a matched function.
*
* @since WPCS 0.11.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param array $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 ) {
$error = false;
switch ( $matched_content ) {
case 'show_admin_bar':
$error = true;
if ( true === $this->remove_only ) {
if ( 'true' === $parameters[1]['raw'] ) {
$error = false;
}
}
break;
case 'add_filter':
$filter_name = $this->strip_quotes( $parameters[1]['raw'] );
if ( 'show_admin_bar' !== $filter_name ) {
break;
}
$error = true;
if ( true === $this->remove_only && isset( $parameters[2]['raw'] ) ) {
if ( '__return_true' === $this->strip_quotes( $parameters[2]['raw'] ) ) {
$error = false;
}
}
break;
default:
// Left empty on purpose.
break;
}
if ( true === $error ) {
$this->phpcsFile->addError( 'Removal of admin bar is prohibited.', $stackPtr, 'RemovalDetected' );
}
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since WPCS 0.11.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $file_name The file name of the current file being processed.
*
* @return void
*/
public function process_text_for_style( $stackPtr, $file_name ) {
$content = trim( $this->tokens[ $stackPtr ]['content'] );
// No need to check an empty string.
if ( '' === $content ) {
return;
}
// Are we in a <style> tag ?
if ( true === $this->in_style[ $file_name ] ) {
if ( false !== strpos( $content, '</style>' ) ) {
// Make sure we check any content on this line before the closing style tag.
$this->in_style[ $file_name ] = false;
$content = trim( substr( $content, 0, strpos( $content, '</style>' ) ) );
}
} elseif ( false !== strpos( $content, '<style' ) ) {
// Ok, found a <style> open tag.
if ( false === strpos( $content, '</style>' ) ) {
// Make sure we check any content on this line after the opening style tag.
$this->in_style[ $file_name ] = true;
$content = trim( substr( $content, ( strpos( $content, '<style' ) + 6 ) ) );
} else {
// Ok, we have open and close style tag on the same line with possibly content within.
$start = ( strpos( $content, '<style' ) + 6 );
$end = strpos( $content, '</style>' );
$content = trim( substr( $content, $start, ( $end - $start ) ) );
unset( $start, $end );
}
} else {
return;
}
// Are we in one of the target selectors ?
if ( true === $this->in_target_selector[ $file_name ] ) {
if ( false !== strpos( $content, '}' ) ) {
// Make sure we check any content on this line before the selector closing brace.
$this->in_target_selector[ $file_name ] = false;
$content = trim( substr( $content, 0, strpos( $content, '}' ) ) );
}
} elseif ( preg_match( $this->target_css_selectors_regex, $content, $matches ) > 0 ) {
// Ok, found a new target selector.
$content = '';
if ( isset( $matches[1] ) && '' !== $matches[1] ) {
if ( false === strpos( $matches[1], '}' ) ) {
// Make sure we check any content on this line before the closing brace.
$this->in_target_selector[ $file_name ] = true;
$content = trim( $matches[1] );
} else {
// Ok, we have the selector open and close brace on the same line.
$content = trim( substr( $matches[1], 0, strpos( $matches[1], '}' ) ) );
}
} else {
$this->in_target_selector[ $file_name ] = true;
}
} else {
return;
}
unset( $matches );
// Now let's do the check for the CSS properties.
if ( ! empty( $content ) ) {
foreach ( $this->target_css_properties as $property => $requirements ) {
if ( false !== strpos( $content, $property ) ) {
$error = true;
if ( true === $this->remove_only ) {
// Check the value of the CSS property.
if ( preg_match( '`' . preg_quote( $property, '`' ) . '\s*:\s*(.+?)\s*(?:!important)?;`', $content, $matches ) > 0 ) {
$value = trim( $matches[1] );
$valid = $this->validate_css_property_value( $value, $requirements['type'], $requirements['value'] );
if ( true === $valid ) {
$error = false;
}
}
}
if ( true === $error ) {
$this->phpcsFile->addError( 'Hiding of the admin bar is not allowed.', $stackPtr, 'HidingDetected' );
}
}
}
}
}
/**
* Processes this test for T_STYLE tokens in CSS files.
*
* @since WPCS 0.11.0
*
* @param int $stackPtr The position of the current token in the stack passed in $tokens.
*
* @return void
*/
protected function process_css_style( $stackPtr ) {
if ( ! isset( $this->target_css_properties[ $this->tokens[ $stackPtr ]['content'] ] ) ) {
// Not one of the CSS properties we're interested in.
return;
}
$css_property = $this->target_css_properties[ $this->tokens[ $stackPtr ]['content'] ];
// Check if the CSS selector matches.
$opener = $this->phpcsFile->findPrevious( \T_OPEN_CURLY_BRACKET, $stackPtr );
if ( false !== $opener ) {
for ( $i = ( $opener - 1 ); $i >= 0; $i-- ) {
if ( isset( Tokens::$commentTokens[ $this->tokens[ $i ]['code'] ] )
|| \T_CLOSE_CURLY_BRACKET === $this->tokens[ $i ]['code']
) {
break;
}
}
$start = ( $i + 1 );
$selector = trim( $this->phpcsFile->getTokensAsString( $start, ( $opener - $start ) ) );
unset( $i );
foreach ( $this->target_css_selectors as $target_selector ) {
if ( false !== strpos( $selector, $target_selector ) ) {
$error = true;
if ( true === $this->remove_only ) {
// Check the value of the CSS property.
$valuePtr = $this->phpcsFile->findNext( [ \T_COLON, \T_WHITESPACE ], ( $stackPtr + 1 ), null, true );
$value = $this->tokens[ $valuePtr ]['content'];
$valid = $this->validate_css_property_value( $value, $css_property['type'], $css_property['value'] );
if ( true === $valid ) {
$error = false;
}
}
if ( true === $error ) {
$this->phpcsFile->addError( 'Hiding of the admin bar is not allowed.', $stackPtr, 'HidingDetected' );
}
}
}
}
}
/**
* Verify if a CSS property value complies with an expected value.
*
* {@internal This is a method stub, doing only what is needed for this sniff.
* If at some point in the future other sniff would need similar functionality,
* this method should be moved to the WordPress_Sniff class and expanded to cover
* all types of comparisons.}}
*
* @since WPCS 0.11.0
*
* @param mixed $value The value of CSS property.
* @param string $compare_type The type of comparison to use for the validation.
* @param string $compare_value The value to compare against.
*
* @return bool True if the property value complies, false otherwise.
*/
protected function validate_css_property_value( $value, $compare_type, $compare_value ) {
switch ( $compare_type ) {
case '!=':
return $value !== $compare_value;
case '>':
return $value > $compare_value;
default:
return false;
}
}
}
@@ -0,0 +1,75 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\PluginTerritory;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Restricts the use of various functions that are plugin territory.
*
* @link https://make.wordpress.org/themes/handbook/review/required/#presentation-vs-functionality
*
* @since 0.1.0
* @since 0.2.0 Added the `editor-blocks` group.
* Added the `cron-functionality` group.
*/
class ForbiddenFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => [
* 'lambda' => [
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => [ 'file_get_contents', 'create_function' ],
* ]
* ]
*
* @return array
*/
public function getGroups() {
return [
'plugin-territory' => [
'type' => 'error',
'message' => 'Function %s() is not allowed because it is plugin territory.',
'functions' => [
'register_post_type',
'register_taxonomy',
'add_shortcode',
'register_taxonomy_for_object_type',
'flush_rewrite_rules',
],
],
'editor-blocks' => [
'type' => 'error',
'message' => 'Registering and deregistering editor blocks should be done in a plugin, not in a theme. Found %s().',
'functions' => [
'register_block_*',
'unregister_block_*',
],
],
'cron-functionality' => [
'type' => 'error',
'message' => 'Themes should not be running regular (Cron) tasks. Found %s().',
'functions' => [
'wp_clear_scheduled_hook',
'wp_cron',
'wp_reschedule_event',
'wp_schedule_*',
'wp_unschedule_*',
],
],
];
}
}
@@ -0,0 +1,66 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\PluginTerritory;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Forbids the use of add_..._page() functions within Themes with the exception of `add_theme_page()`.
*
* @link https://make.wordpress.org/themes/handbook/review/required/theme-check-plugin/#admin-menu
*
* @since 0.1.0
*/
class NoAddAdminPagesSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => [
* 'lambda' => [
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => [ 'file_get_contents', 'create_function' ],
* ]
* ]
*
* @return array
*/
public function getGroups() {
return [
'add_menu_pages' => [
'type' => 'error',
'message' => 'Themes should use add_theme_page() for adding admin pages. Found %s.',
'functions' => [
// Menu Pages.
'add_menu_page',
'add_object_page',
'add_utility_page',
// SubMenu Pages.
'add_submenu_page',
// WordPress Administration Menus.
'add_dashboard_page',
'add_posts_page',
'add_links_page',
'add_media_page',
'add_pages_page',
'add_comments_page',
'add_plugins_page',
'add_users_page',
'add_management_page',
'add_options_page',
],
],
];
}
}
@@ -0,0 +1,79 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\PluginTerritory;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Discourages the use of session functions.
*
* @link https://make.wordpress.org/themes/handbook/review/...... @todo
*
* @since WPCS 0.3.0
* @since WPCS 0.11.0 Extends the WPCS native `AbstractFunctionRestrictionsSniff` class
* instead of the upstream `Generic.PHP.ForbiddenFunctions` sniff.
* @since WPCS 0.13.0 Class name changed: this class is now namespaced.
*
* @since TRTCS 0.1.0 As this sniff will be removed from WPCS in version 2.0, the
* sniff has been cherry-picked into the WPThemeReview standard.
*/
class SessionFunctionsUsageSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => [
* 'lambda' => [
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => [ 'file_get_contents', 'create_function' ],
* ]
* ]
*
* @return array
*/
public function getGroups() {
return [
'session' => [
'type' => 'error',
'message' => 'The use of PHP session function %s() is prohibited.',
'functions' => [
'session_abort',
'session_cache_expire',
'session_cache_limiter',
'session_commit',
'session_create_id',
'session_decode',
'session_destroy',
'session_encode',
'session_gc',
'session_get_cookie_params',
'session_id',
'session_is_registered',
'session_module_name',
'session_name',
'session_regenerate_id',
'session_register_shutdown',
'session_register',
'session_reset',
'session_save_path',
'session_set_cookie_params',
'session_set_save_handler',
'session_start',
'session_status',
'session_unregister',
'session_unset',
'session_write_close',
],
],
];
}
}
@@ -0,0 +1,66 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\PluginTerritory;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
/**
* Discourages the use of the session variable.
* Creating a session writes a file to the server and is unreliable in a multi-server environment.
*
* @link https://make.wordpress.org/themes/handbook/review/...... @todo
*
* @since WPCS 0.3.0
* @since WPCS 0.10.0 The sniff no longer needlessly extends the Generic_Sniffs_PHP_ForbiddenFunctionsSniff
* which it didn't use.
* @since WPCS 0.12.0 This class now extends WordPress_Sniff.
* @since WPCS 0.13.0 Class name changed: this class is now namespaced.
*
* @since TRTCS 0.1.0 As this sniff will be removed from WPCS in version 2.0, the
* sniff has been cherry-picked into the WPThemeReview standard.
*/
class SessionVariableUsageSniff implements Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return [
\T_VARIABLE,
];
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $stackPtr The position of the current token
* in the stack.
*
* @return void
*/
public function process( File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();
if ( '$_SESSION' === $tokens[ $stackPtr ]['content'] ) {
$phpcsFile->addError(
'Usage of $_SESSION variable is prohibited.',
$stackPtr,
'SessionVarsProhibited'
);
}
}
}
@@ -0,0 +1,546 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\Plugins;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* WordPress_Sniffs_Theme_CorrectTGMPAVersionSniff.
*
* Verifies that if the TGM Plugin Activation library is included, the correct version is used.
* - Check whether the version included is up to date.
* - Check whether the version included is downloaded via the Custom Generator with
* the correct settings.
* - Check against a persistent manual search & replace error made by theme authors.
*
* @link https://make.wordpress.org/themes/handbook/review/...... @todo
*
* @since 0.1.0
*
* {@internal This sniff currently has not (yet) been detached from the WordPressCS\WordPress\Sniff class
* as the `detect_manual_editing()` method uses the WPCS `get_function_call_parameter()` and
* `strip_quotes()` methods.
* If/when WPCS re-organizes the generic methods into traits as is being discussed in
* {@link https://github.com/WordPress/WordPress-Coding-Standards/issues/1465},
* this sniff could be decoupled and use the trait(s) instead.}}
*/
class CorrectTGMPAVersionSniff extends Sniff {
const GITHUB_TGMPA_API_URL = 'https://api.github.com/repos/TGMPA/TGM-Plugin-Activation/releases/latest';
const GITHUB_API_OAUTH_QUERY = '?access_token=%s';
/**
* TGMPA flavour required.
*
* By default, any flavour is ok. If this property is set in a custom ruleset an additional
* check will be run to verify if the correct flavour of TGMPA is used.
*
* Note: The Custom Generator only has flavours for themes, so this property is only
* relevant when checking a theme.
*
* Valid values:
* - 'wporg'
* - 'themeforest'
*
* @var string
*/
public $required_flavour = '';
/**
* GitHub oAuth token.
*
* Intended to be set in the ruleset.
*
* This is to prevent issues with rate limiting if a lot of requests are made from the same server.
*
* "Normal" users generaly won't need to set this, but if the sniffs are run for all themes
* uploaded to wordpress.org, that IP address might run into the rate limit of 60 calls per hour.
* Setting a oauth token in the custom ruleset used will prevent this.
*
* Alternatively, the token can also be set via an environment key called `GITHUB_OAUTH_TOKEN`.
*
* @var string
*/
public $github_oauth_token = '';
/**
* Fall-back for the latest version number for if the API call fails.
*
* @var string
*/
private $current_version = '2.6.1';
/**
* Whether or not a call has been made to the GitHub API to retrieve the current TGMPA version number.
*
* Note: an API call will only be made once per PHPCS run and only when the TGMPA library
* has been positively identified.
* The API call will be skipped when running the unit tests.
*
* @var bool
*/
private $gh_call_made = false;
/**
* Classes and functions declared by TGMPA.
*
* @var array
*/
private $tgmpa_classes_functions = [
// Classes.
'TGM_Plugin_Activation' => true,
'TGMPA_List_Table' => true,
'TGM_Bulk_Installer' => true,
'TGM_Bulk_Installer_Skin' => true,
'TGMPA_Bulk_Installer' => true,
'TGMPA_Bulk_Installer_Skin' => true,
'TGMPA_Utils' => true,
// Functions.
'load_tgm_plugin_activation' => true,
'tgmpa_initialize' => true, // New in v 2.6.2.
'tgmpa' => true,
'tgmpa_load_bulk_installer' => true,
];
/**
* List of available TGMPA flavours - other than the default.
*
* Used to validate `$required_flavour` property.
*
* Keys are the allowed values for the flavour property.
* Value is how this is expanded in the @version tag used by TGMPA.
*
* @var array
*/
private $valid_flavours = [
'wporg' => 'WordPress.org',
'themeforest' => 'ThemeForest',
];
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return [
T_OPEN_TAG,
];
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
$has_class_function = $this->phpcsFile->findNext( [ T_CLASS, T_FUNCTION ], ( $stackPtr + 1 ) );
if ( false === $has_class_function ) {
// No class, function or constant declaration found, definitely not TGMPA file.
// Skip this file from further checks.
return ( $this->phpcsFile->numTokens + 1 );
}
$is_tgmpa = false;
// First check based on filename.
$file_name = basename( $this->phpcsFile->getFileName() );
if ( false !== $file_name ) {
$file_name = strtolower( $file_name );
if ( strpos( $file_name, 'class-tgm-plugin-activation.php' ) !== false ) {
$is_tgmpa = true;
} elseif ( defined( 'PHP_CODESNIFFER_IN_TESTS' ) && preg_match( '`class-tgm-plugin-activation[^.]+\.inc`', $file_name, $discard ) > 0 ) {
$is_tgmpa = true;
}
}
/*
* Otherwise, check whether any of the TGMPA classes or function names are encountered.
*
* Will detect TGMPA, even when:
* - the class and function prefix has been changed
* - the file has been split up into several files
* - the file is combined with other code
*/
if ( false === $is_tgmpa ) {
while ( false !== $has_class_function ) {
$name = $this->phpcsFile->getDeclarationName( $has_class_function );
if ( ! empty( $name ) ) {
if ( isset( $this->tgmpa_classes_functions[ $name ] ) ) {
$is_tgmpa = true;
break;
} elseif ( strpos( $name, '_Plugin_Activation' ) !== false ) {
// This may be TGMPA with renamed prefixes, so look for typical class comment.
$prev = $this->phpcsFile->findPrevious( T_WHITESPACE, ( $has_class_function - 1 ), null, true );
while ( false !== $prev && isset( Tokens::$commentTokens[ $this->tokens[ $prev ]['code'] ] ) ) {
if ( ( T_COMMENT === $this->tokens[ $prev ]['code']
|| T_DOC_COMMENT_STRING === $this->tokens[ $prev ]['code'] )
&& ( strpos( $this->tokens[ $prev ]['content'], 'Automatic plugin installation and activation library' ) !== false
|| strpos( $this->tokens[ $prev ]['content'], 'Automatic plugin installation and activation class' ) !== false )
) {
$is_tgmpa = true;
break;
}
$prev = $this->phpcsFile->findPrevious( T_WHITESPACE, ( $prev - 1 ), null, true );
}
}
}
$start = ( $has_class_function + 1 );
if ( isset( $this->tokens[ $has_class_function ]['scope_condition'], $this->tokens[ $stackPtr ]['scope_closer'] )
&& $this->tokens[ $has_class_function ]['scope_condition'] === $has_class_function
) {
// Skip past anything within the class or function.
$start = $this->tokens[ $stackPtr ]['scope_closer'];
}
$has_class_function = $this->phpcsFile->findNext( [ T_CLASS, T_FUNCTION ], $start );
}
}
// If we're still not 100% sure this is TGMPA, exclude the file from further checks.
if ( false === $is_tgmpa ) {
return ( $this->phpcsFile->numTokens + 1 );
}
// Check whether the correct version of TGMPA is being used.
$version = $this->uses_latest_version();
// Check for typical manual search & replace error.
$this->detect_manual_editing( $version );
// No need to check the same file again.
return ( $this->phpcsFile->numTokens + 1 );
}
/**
* Check whether the latest version of TGMPA is being used.
*/
protected function uses_latest_version() {
if ( false === $this->gh_call_made ) {
// Get the current version number for TGMPA from GitHub.
$this->update_current_version();
$this->gh_call_made = true;
}
/*
* Walk the doc block comments to find if this is the correct version of TGMPA.
* Normally this will be in the first doc block encountered, so this is not as 'heavy' as it looks.
*/
$next_doc_block = 0;
$version = false;
$pcre_esc_flavours = array_map(
'preg_quote',
$this->valid_flavours,
array_fill( 0, count( $this->valid_flavours ), '`' )
);
$pcre_flavours = implode( '|', $pcre_esc_flavours );
do {
$next_doc_block = $this->phpcsFile->findNext( T_DOC_COMMENT_OPEN_TAG, ( $next_doc_block + 1 ) );
if ( false === $next_doc_block ) {
break;
}
$tags = $this->get_docblock_tags( $next_doc_block );
if ( empty( $tags ) ) {
continue;
}
if ( ! isset( $tags['package'], $tags['version'] ) || 'TGM-Plugin-Activation' !== $tags['package'] ) {
continue;
}
if ( preg_match( '`^([0-9\.]+(?:-(?:alpha|beta|RC)(?:[0-9])?)?)`', $tags['version'], $matches ) > 0 ) {
$version = $matches[1];
$this->phpcsFile->recordMetric( 0, 'Version', $version );
if ( true === version_compare( $this->current_version, $version, '>' ) ) {
$error = 'Upgrade of the included TGM plugin activation library required. Current version: %s. Found version: %s';
$data = [
$this->current_version,
$version,
];
$this->phpcsFile->addError( $error, 0, 'upgradeRequired', $data );
} elseif ( true === version_compare( $this->current_version, $version, '<' ) ) {
$error = 'Non-stable version of the TGM plugin activation library found. The current version is %s. Found version: %s';
$data = [
$this->current_version,
$version,
];
$this->phpcsFile->addError( $error, 0, 'unstableVersion', $data );
}
unset( $matches, $error, $data );
if ( strpos( $tags['version'], 'for parent theme' ) !== false ) {
$this->phpcsFile->recordMetric( 0, 'Used in', 'parent theme' );
} elseif ( strpos( $tags['version'], 'for child theme' ) !== false ) {
$this->phpcsFile->recordMetric( 0, 'Used in', 'child theme' );
} else {
$this->phpcsFile->recordMetric( 0, 'Used in', 'unknown' );
}
if ( preg_match( '`for publication on (' . $pcre_flavours . ')`i', $tags['version'], $flavour_match ) > 0 ) {
$this->phpcsFile->recordMetric( 0, 'Publication Channel', $flavour_match[1] );
} else {
$this->phpcsFile->recordMetric( 0, 'Publication Channel', 'n/a' );
}
if ( ! empty( $this->required_flavour ) && isset( $this->valid_flavours[ $this->required_flavour ] ) ) {
$flavour_phrase = sprintf( 'for publication on %s', $this->valid_flavours[ $this->required_flavour ] );
if ( strpos( $tags['version'], $flavour_phrase ) === false ) {
$warning = 'You are required to use a version of the TGM Plugin Activation library downloaded through the Custom TGMPA Generator. Download a fresh copy and make sure you select "%s" as your publication channel to get the correct version. http://tgmpluginactivation.com/download/';
$this->phpcsFile->addWarning(
$warning,
0,
'wrongVersion',
[ $this->valid_flavours[ $this->required_flavour ] ]
);
}
}
}
break;
} while ( false !== $next_doc_block );
// The file was recognized as TGMPA, but no valid file doc block for TGMPA was found.
if ( false === $version ) {
$this->phpcsFile->recordMetric( 0, 'Version', 'unknown' );
$error = 'The TGM Plugin Activation library was detected, but the version could not be determined. Ensure you use the latest stable release of the TGM Plugin Activation library (%s). Download a fresh copy now using the Custom TGMPA Generator at http://tgmpluginactivation.com/download/';
$data = [ $this->current_version ];
$this->phpcsFile->addError( $error, 0, 'versionUndetermined', $data );
$has_error = true;
}
return $version;
}
/**
* Check against a typical manual search & replace error often encountered.
*
* In that case the `tgmpa` in `if ( ! function_exists( 'tgmpa' ) )` has been replaced
* with the theme slug causing fatal errors when an end-user also uses a plugin using TGMPA.
*
* @param string $version The version of TGMPA found.
*/
protected function detect_manual_editing( $version ) {
// Skip this check for TGMPA versions which didn't have the `tgmpa()` function or
// didn't have the `function_exists()` wrapper.
if ( false === $version || true === version_compare( $version, '2.2.0', '<' ) ) {
return;
}
$checkTokens = [
// This is what we're looking for.
T_FUNCTION => true,
// These are just here to be able to skip as much as we can.
T_CLASS => true,
T_ARRAY => true,
T_OPEN_SHORT_ARRAY => true,
];
for ( $ptr = 0; $ptr < $this->phpcsFile->numTokens; $ptr++ ) {
if ( ! isset( $checkTokens[ $this->tokens[ $ptr ]['code'] ] ) ) {
continue;
}
// Skip as much as we can.
if ( T_CLASS === $this->tokens[ $ptr ]['code'] && isset( $this->tokens[ $ptr ]['scope_closer'] ) ) {
$ptr = $this->tokens[ $ptr ]['scope_closer'];
continue;
} elseif ( T_OPEN_SHORT_ARRAY === $this->tokens[ $ptr ]['code'] && isset( $this->tokens[ $ptr ]['bracket_closer'] ) ) {
$ptr = $this->tokens[ $ptr ]['bracket_closer'];
continue;
} elseif ( T_ARRAY === $this->tokens[ $ptr ]['code'] && isset( $this->tokens[ $ptr ]['parenthesis_closer'] ) ) {
$ptr = $this->tokens[ $ptr ]['parenthesis_closer'];
continue;
}
// Detect whether this is the `tgmpa()` function declaration.
if ( T_FUNCTION === $this->tokens[ $ptr ]['code'] ) {
$function_name = $this->phpcsFile->getDeclarationName( $ptr );
if ( 'tgmpa' !== $function_name ) {
if ( isset( $this->tokens[ $ptr ]['scope_closer'] ) ) {
// Skip the rest of the function.
$ptr = $this->tokens[ $ptr ]['scope_closer'];
}
continue;
}
// Ok, found the tgmpa function declaration. Now let's check for the typical
// manual text-domain replacement error.
$function_exists = $this->phpcsFile->findPrevious( T_STRING, ( $ptr - 1 ), null, false, 'function_exists' );
$param = false;
if ( false !== $function_exists ) {
$param = $this->get_function_call_parameter( $function_exists, 1 );
$param = $this->strip_quotes( $param['raw'] );
}
if ( false === $function_exists || 'tgmpa' !== $param ) {
$this->phpcsFile->recordMetric( 0, 'Manual editing detected', 'yes' );
$this->phpcsFile->addError(
'Manual editing of the TGM Plugin Activation file detected. Your edit will cause fatal errors for end-users. Download an official copy using the Custom TGMPA Generator. http://tgmpluginactivation.com/download/',
0,
'ManualEditDetected',
[],
9
);
} else {
$this->phpcsFile->recordMetric( 0, 'Manual editing detected', 'no' );
}
break;
}
}
}
/**
* Retrieve an array with the doc block tags from a T_DOC_COMMENT_OPEN_TAG.
*
* @param int $comment_opener The position of the comment opener.
*
* @return array
*/
protected function get_docblock_tags( $comment_opener ) {
$tags = [];
$opener = $this->tokens[ $comment_opener ];
if ( ! isset( $opener['comment_tags'] ) ) {
return $tags;
}
$closer = null;
if ( isset( $opener['comment_closer'] ) ) {
$closer = $opener['comment_closer'];
}
$tag_count = count( $opener['comment_tags'] );
for ( $i = 0; $i < $tag_count; $i++ ) {
$tag_token = $opener['comment_tags'][ $i ];
$tag = trim( $this->tokens[ $tag_token ]['content'], '@' );
$search_end = $closer;
if ( ( $i + 1 ) < $tag_count ) {
$search_end = $opener['comment_tags'][ ( $i + 1 ) ];
}
$value_token = $this->phpcsFile->findNext( T_DOC_COMMENT_STRING, ( $tag_token + 1 ), $search_end );
$tags[ $tag ] = trim( $this->tokens[ $value_token ]['content'] );
unset( $tag_token, $tag, $search_end, $value );
}
return $tags;
}
/**
* Get the version number (tag_name) of the latest TGMPA release from the GitHub API.
*/
protected function update_current_version() {
if ( defined( 'PHP_CODESNIFFER_IN_TESTS' ) || true === $this->gh_call_made ) {
return;
}
$api_url = self::GITHUB_TGMPA_API_URL;
$oauth_token = false;
if ( '' !== $this->github_oauth_token && is_string( $this->github_oauth_token ) ) {
$oauth_token = $this->github_oauth_token;
} elseif ( false !== getenv( 'GITHUB_OAUTH_TOKEN' ) ) {
$oauth_token = getenv( 'GITHUB_OAUTH_TOKEN' );
}
if ( false !== $oauth_token ) {
$api_url .= sprintf( self::GITHUB_API_OAUTH_QUERY, $oauth_token );
}
$stream_options = [
'http' => [
'method' => 'GET',
'user_agent' => 'WordPress-Coding-Standards/Theme-Review-Sniffs',
'protocol_version' => 1.1,
],
];
$stream_context = stream_context_create( $stream_options );
$response = file_get_contents( $api_url, false, $stream_context );
$headers = $this->parse_response_headers( $http_response_header );
// Check for invalid oAuth token response.
if ( 401 === $headers['response_code'] && false !== $oauth_token ) {
$this->phpcsFile->addWarning(
'The GITHUB_OAUTH_TOKEN you provided is invalid. Please update the token in your custom ruleset or environment properties.',
0,
'githubOauthTokenInvalid'
);
$this->oauth_error = false;
return;
}
// Check for rate limit error response.
if ( 403 === $headers['response_code'] && '0' === $headers['X-RateLimit-Remaining'] ) {
// @todo Add link to GH wiki page documenting the properties.
$this->phpcsFile->addWarning(
'You are running PHPCS more than 60 times per hour. You may want to consider setting the `github_oauth_token` property in your custom ruleset for Theme Review. For more information see: ... (GH wiki page).',
0,
'githubRateLimitReached'
);
$this->rate_limit_error = false;
return;
}
if ( 200 !== $headers['response_code'] ) {
// Something unexpected going on, just ignore it.
return;
}
// Ok, we have received a valid response.
$response = json_decode( $response );
if ( ! empty( $response->tag_name ) && ( ! isset( $response->prerelease ) || false === $response->prerelease ) ) {
// Should there be a check for `v` at the start of a version number ?
$this->current_version = $response->tag_name;
}
}
/**
* Parse HTTP response headers array to a more usable format.
*
* Based on http://php.net/manual/en/reserved.variables.httpresponseheader.php#117203
*
* @param array $headers HTTP response headers array.
*
* @return array
*/
private function parse_response_headers( $headers ) {
$head = [];
foreach ( $headers as $key => $value ) {
$tag = explode( ':', $value, 2 );
if ( isset( $tag[1] ) ) {
$head[ trim( $tag[0] ) ] = trim( $tag[1] );
} else {
$head[] = $value;
if ( preg_match( '`HTTP/[0-9\.]+\s+([0-9]+)`', $value, $out ) ) {
$head['response_code'] = intval( $out[1] );
}
}
}
return $head;
}
}
@@ -0,0 +1,150 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\Privacy;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
/**
* Detect the use of shortened URLs.
*
* Detection is based on a list of banned URL shortener services.
*
* @link https://make.wordpress.org/themes/handbook/review/required/#privacy
*
* @since 0.2.0
*/
class ShortenedURLsSniff implements Sniff {
/**
* Error message template.
*
* @since 0.2.0
*
* @var string
*/
const ERROR_MSG = 'Shortened URLs are not allowed in the theme. Found: "%s".';
/**
* Regex template.
*
* Will be parsed together with the url_shorteners blacklist in the register() method.
*
* @since 0.2.0
*
* @var string
*/
const REGEX_TEMPLATE = '`(?:%s)/[^\s\'"]+`i';
/**
* Supported Tokenizers.
*
* @since 0.2.0
*
* @var array
*/
public $supportedTokenizers = [
'PHP',
'CSS',
'JS',
];
/**
* Regex pattern.
*
* @since 0.2.0
*
* @var string
*/
private $regex = '';
/**
* List of url shorteners.
*
* @since 0.2.0
*
* @var array
*/
protected $url_shorteners = [
'bit.do',
'bit.ly',
'df.ly',
'goo.gl',
'is.gd',
'lc.chat',
'ow.ly',
'polr.me',
's2r.co',
'soo.gd',
'tiny.cc',
'tinyurl.com',
];
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.2.0
*
* @return array
*/
public function register() {
// Create the regex only once.
$urls = array_map(
'preg_quote',
$this->url_shorteners,
array_fill( 0, count( $this->url_shorteners ), '`' )
);
$this->regex = sprintf(
self::REGEX_TEMPLATE,
implode( '|', $urls )
);
return Tokens::$textStringTokens + [
T_COMMENT,
T_DOC_COMMENT_STRING,
T_DOC_COMMENT,
];
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.2.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $stackPtr The position of the current token
* in the stack.
*
* @return void
*/
public function process( File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();
$content = $tokens[ $stackPtr ]['content'];
if ( stripos( $content, '.' ) === false ) {
return;
}
if ( preg_match_all( $this->regex, $content, $matches ) > 0 ) {
foreach ( $matches[0] as $matched_url ) {
$phpcsFile->addError(
self::ERROR_MSG,
$stackPtr,
'Found',
[ $matched_url ]
);
}
}
}
}
@@ -0,0 +1,170 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\Templates;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
/**
* Check if the template file is using a prefix which would cause WP to interpret it as a specialised template
* meant to apply to only one page on the site.
*
* You can check the documentation of each of the functions used in determining the template hierarchy.
*
* @link https://developer.wordpress.org/reference/functions/get_category_template
* @link https://developer.wordpress.org/reference/functions/get_author_template
* @link https://developer.wordpress.org/reference/functions/get_page_template
* @link https://developer.wordpress.org/reference/functions/get_tag_template
*
* Or you can check the in depth documentation about the template hierarchy.
*
* @link https://developer.wordpress.org/themes/basics/template-hierarchy
*
* @since 0.2.0
*/
class ReservedFileNamePrefixSniff implements Sniff {
/**
* Error message template.
*
* @since 0.2.0
*
* @var string
*/
const ERROR_MSG = 'Template files should not be slug specific. The file name used for this template will be interpreted by WP as %1$s-%2$s and only applied when a %1$s with the slug "%2$s" is loaded.';
/**
* Found prefix in a file.
*
* @since 0.2.0
*
* @var string
*/
protected $prefix;
/**
* File slug.
*
* @since 0.2.0
*
* @var string
*/
protected $slug;
/**
* List of reserved template file names.
*
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/
* @link https://developer.wordpress.org/themes/template-files-section/partial-and-miscellaneous-template-files/#content-slug-php
* @link https://wphierarchy.com/
* @link https://en.wikipedia.org/wiki/Media_type#Naming
*
* @since 0.2.0
*
* @var array
*/
protected $reserved_file_name_prefixes = [
'author' => true,
'category' => true,
'page' => true,
'tag' => true,
];
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.2.0
*
* @return array
*/
public function register() {
return [
\T_OPEN_TAG,
\T_OPEN_TAG_WITH_ECHO,
];
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.2.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $stackPtr The position of the current token
* in the stack.
*
* @return int StackPtr to the end of the file, this sniff needs to only
* check each file once.
*/
public function process( File $phpcsFile, $stackPtr ) {
// Usage of `strip_quotes` is to ensure `stdin_path` passed by IDEs does not include quotes.
$file = $this->strip_quotes( $phpcsFile->getFileName() );
$fileName = basename( $file );
$fileName = str_replace( [ '.inc', '.php' ], '', $fileName );
// Check if the current file has a prefix in the reserved list.
if ( ! $this->is_reserved_template_name_used( $fileName ) ) {
return ( $phpcsFile->numTokens + 1 );
}
$phpcsFile->addError(
self::ERROR_MSG,
0,
'ReservedTemplatePrefixFound',
[ $this->prefix, $this->slug ]
);
return ( $phpcsFile->numTokens + 1 );
}
/**
* Checks if the given file name starts with one of the reserved prefixes.
*
* @since 0.2.0
*
* @param string $file File name to check.
* @return boolean
*/
private function is_reserved_template_name_used( $file ) {
$file_parts = explode( '-', $file, 2 );
if ( empty( $file_parts[0] ) || empty( $file_parts[1] ) ) {
// No dash or dash at start or end of filename, i.e. `-something.php`.
return false;
}
if ( isset( $this->reserved_file_name_prefixes[ strtolower( $file_parts[0] ) ] ) ) {
$this->prefix = $file_parts[0];
$this->slug = $file_parts[1];
return true;
}
return false;
}
/**
* Strip quotes surrounding an arbitrary string.
*
* Intended for use with the contents of a T_CONSTANT_ENCAPSED_STRING / T_DOUBLE_QUOTED_STRING.
*
* Copied from the WordPressCS\WordPress\Sniff abstract class.
*
* @since 0.2.0
*
* @param string $string The raw string.
* @return string String without quotes around it.
*/
private function strip_quotes( $string ) {
return preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $string );
}
}
@@ -0,0 +1,67 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\ThouShallNotUse;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
/**
* Check for use of <iframe>. Often used for malicious code.
*
* @link https://make.wordpress.org/themes/handbook/review/required/theme-check-plugin/#info
*
* @since 0.1.0
*/
class ForbiddenIframeSniff implements Sniff {
/**
* The regex to catch usage of <iframe ...>.
*
* This regex will prevent matches being made on `<iframe>` without attributes.
*
* @var string
*/
const IFRAME_REGEX = '`(<iframe\s+[^>]+>)`i';
/**
* 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 \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $stackPtr The position of the current token
* in the stack.
*
* @return void
*/
public function process( File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();
if ( preg_match( self::IFRAME_REGEX, $tokens[ $stackPtr ]['content'], $matches ) > 0 ) {
$phpcsFile->addError(
'Usage of the iframe HTML element is prohibited. Found: %s',
$stackPtr,
'Found',
[ $matches[1] ]
);
}
}
}
@@ -0,0 +1,99 @@
<?php
/**
* WPThemeReview Coding Standard.
*
* @package WPTRT\WPThemeReview
* @link https://github.com/WPTRT/WPThemeReview
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WPThemeReview\Sniffs\ThouShallNotUse;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
/**
* Check for auto generated themes.
*
* @since 0.1.0
*/
class NoAutoGenerateSniff implements Sniff {
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = [
'PHP',
'CSS',
];
/**
* A list of strings found in the generated themes.
*
* @var array
*/
protected $generated_themes = [
'artisteer', // Artisteer.
'themler', // Themler.
'lubith', // Lubith.
'templatetoaster', // TemplateToaster.
'wpthemegenerator', // WP Theme Generator.
'pinegrow', // Pinegrow.
];
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
$tokens = Tokens::$textStringTokens;
$tokens[] = T_STRING; // Functions named after or prefixed with the generator name.
$tokens[] = T_COMMENT;
$tokens[] = T_DOC_COMMENT_STRING;
return $tokens;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $stackPtr The position of the current token
* in the stack.
*
* @return void
*/
public function process( File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();
$token = $tokens[ $stackPtr ];
$content = trim( strtolower( $token['content'] ) );
// No need to check an empty string.
if ( '' === $content ) {
return;
}
foreach ( $this->generated_themes as $generated_theme ) {
/*
* The check can have false positives like artisteers but chances of that happening
* in a valid theme is low.
*/
if ( false === strpos( $content, $generated_theme ) ) {
continue;
}
$phpcsFile->addError(
'Auto generated themes are not allowed in the theme directory. Found: %s',
$stackPtr,
'AutoGeneratedFound',
[ $generated_theme ]
);
}
}
}