Skip to content

Commit

Permalink
Merge pull request #585 from jrfnl/feature/issue-580-alternative-open…
Browse files Browse the repository at this point in the history
…tags

Add sniff to disallow alternative PHP open tags.
  • Loading branch information
JDGrimes authored Jul 25, 2016
2 parents 0a5d6f4 + 34d436f commit 6759229
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 0 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ matrix:
- env: PHPCS_BRANCH=3.0

before_script:
- if [[ $TRAVIS_PHP_VERSION != "7.0" && $TRAVIS_PHP_VERSION != "nightly" && $TRAVIS_PHP_VERSION != "hhvm" ]]; then phpenv config-add myconfig.ini; fi
- export PHPCS_DIR=/tmp/phpcs
- export PHPCS_BIN=$(if [[ $PHPCS_BRANCH == 3.0 ]]; then echo $PHPCS_DIR/bin/phpcs; else echo $PHPCS_DIR/scripts/phpcs; fi)
- mkdir -p $PHPCS_DIR && git clone --depth 1 https://github.com/squizlabs/PHP_CodeSniffer.git -b $PHPCS_BRANCH $PHPCS_DIR
Expand Down
1 change: 1 addition & 0 deletions WordPress-Core/ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

<!-- http://make.wordpress.org/core/handbook/coding-standards/php/#no-shorthand-php-tags -->
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
<rule ref="WordPress.PHP.DisallowAlternativePHPTags"/>

<!-- important to prevent issues with content being sent before headers -->
<rule ref="Generic.Files.ByteOrderMark"/>
Expand Down
223 changes: 223 additions & 0 deletions WordPress/Sniffs/PHP/DisallowAlternativePHPTagsSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
<?php
/**
* WordPress Coding Standard.
*
* @category PHP
* @package PHP\CodeSniffer\WordPress-Coding-Standards
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/
*/

/**
* Verifies that no alternative PHP open tags are used.
*
* If alternative PHP open tags are found, this sniff can fix both the open and close tags.
*
* @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/580
*
* @category PHP
* @package PHP\CodeSniffer\WordPress-Coding-Standards
* @author Juliette Reinders Folmer <[email protected]>
*/
class WordPress_Sniffs_PHP_DisallowAlternativePHPTagsSniff implements PHP_CodeSniffer_Sniff {

/**
* Whether ASP tags are enabled or not.
*
* @var bool
*/
private $asp_tags = false;

/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
if ( version_compare( PHP_VERSION, '7.0.0alpha1', '<' ) ) {
$this->asp_tags = (bool) ini_get( 'asp_tags' );
}

return array(
T_OPEN_TAG,
T_OPEN_TAG_WITH_ECHO,
T_INLINE_HTML,
);

} // end register()

/**
* Processes this test, when one of its tokens is encountered.
*
* @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();
$openTag = $tokens[ $stackPtr ];
$content = $openTag['content'];

if ( '' === trim( $content ) ) {
return;
}

if ( T_OPEN_TAG === $openTag['code'] ) {

if ( '<%' === $content ) {
$error = 'ASP style opening tag used; expected "<?php" but found "%s"';
$closer = $this->find_closing_tag( $phpcsFile, $tokens, $stackPtr, '%>' );
$error_id = 'ASPOpenTagFound';

} elseif ( false !== strpos( $content, '<script ' ) ) {
$error = 'Script style opening tag used; expected "<?php" but found "%s"';
$closer = $this->find_closing_tag( $phpcsFile, $tokens, $stackPtr, '</script>' );
$error_id = 'ScriptOpenTagFound';
}

if ( isset( $error, $closer, $error_id ) ) {
$data = array( $content );

if ( false === $closer ) {
$phpcsFile->addError( $error, $stackPtr, $error_id, $data );
} else {
$fix = $phpcsFile->addFixableError( $error, $stackPtr, $error_id, $data );
if ( true === $fix ) {
$this->add_changeset( $phpcsFile, $tokens, $stackPtr, $closer );
}
}
}

return;
}

if ( T_OPEN_TAG_WITH_ECHO === $openTag['code'] && '<%=' === $content ) {
$error = 'ASP style opening tag used with echo; expected "<?php echo %s ..." but found "%s %s ..."';
$nextVar = $phpcsFile->findNext( T_WHITESPACE, ( $stackPtr + 1 ), null, true );
$snippet = $this->get_snippet( $tokens[ $nextVar ]['content'] );
$data = array(
$snippet,
$content,
$snippet,
);

$closer = $this->find_closing_tag( $phpcsFile, $tokens, $stackPtr, '%>' );

if ( false === $closer ) {
$phpcsFile->addError( $error, $stackPtr, 'ASPShortOpenTagFound', $data );
} else {
$fix = $phpcsFile->addFixableError( $error, $stackPtr, 'ASPShortOpenTagFound', $data );
if ( true === $fix ) {
$this->add_changeset( $phpcsFile, $tokens, $stackPtr, $closer, true );
}
}

return;
}

// Account for incorrect script open tags. The "(?:<s)?" in the regex is to work-around a bug in PHP 5.2.
if ( T_INLINE_HTML === $openTag['code'] && 1 === preg_match( '`((?:<s)?cript (?:[^>]+)?language=[\'"]?php[\'"]?(?:[^>]+)?>)`i', $content, $match ) ) {
$error = 'Script style opening tag used; expected "<?php" but found "%s"';
$snippet = $this->get_snippet( $content, $match[1] );
$data = array( $match[1] . $snippet );

$phpcsFile->addError( $error, $stackPtr, 'ScriptOpenTagFound', $data );

return;
}

if ( T_INLINE_HTML === $openTag['code'] && false === $this->asp_tags ) {
if ( false !== strpos( $content, '<%=' ) ) {
$error = 'Possible use of ASP style short opening tags detected. Needs manual inspection. Found: %s';
$snippet = $this->get_snippet( $content, '<%=' );
$data = array( '<%=' . $snippet );

$phpcsFile->addWarning( $error, $stackPtr, 'MaybeASPShortOpenTagFound', $data );

} elseif ( false !== strpos( $content, '<%' ) ) {
$error = 'Possible use of ASP style opening tags detected. Needs manual inspection. Found: %s';
$snippet = $this->get_snippet( $content, '<%' );
$data = array( '<%' . $snippet );

$phpcsFile->addWarning( $error, $stackPtr, 'MaybeASPOpenTagFound', $data );
}
}
} // end process()

/**
* Get a snippet from a HTML token.
*
* @param string $content The content of the HTML token.
* @param string $start_at Partial string to use as a starting point for the snippet.
* @param int $length The target length of the snippet to get. Defaults to 40.
* @return string
*/
private function get_snippet( $content, $start_at = '', $length = 40 ) {
$start_pos = 0;

if ( '' !== $start_at ) {
$start_pos = strpos( $content, $start_at );
if ( false !== $start_pos ) {
$start_pos += strlen( $start_at );
}
}

$snippet = substr( $content, $start_pos, $length );
if ( ( strlen( $content ) - $start_pos ) > $length ) {
$snippet .= '...';
}

return $snippet;
}

/**
* Try and find a matching PHP closing tag.
*
* @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param array $tokens The token stack.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
* @param string $content The expected content of the closing tag to match the opener.
* @return int|false Pointer to the position in the stack for the closing tag or false if not found.
*/
private function find_closing_tag( PHP_CodeSniffer_File $phpcsFile, $tokens, $stackPtr, $content ) {
$closer = $phpcsFile->findNext( T_CLOSE_TAG, ( $stackPtr + 1 ) );

if ( false !== $closer && trim( $tokens[ $closer ]['content'] ) === $content ) {
return $closer;
}

return false;
}

/**
* Add a changeset to replace the alternative PHP tags.
*
* @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param array $tokens The token stack.
* @param int $open_tag_pointer Stack pointer to the PHP open tag.
* @param int $close_tag_pointer Stack pointer to the PHP close tag.
* @param bool $echo Whether to add 'echo' or not.
*/
private function add_changeset( $phpcsFile, $tokens, $open_tag_pointer, $close_tag_pointer, $echo = false ) {
// Build up the open tag replacement and make sure there's always whitespace behind it.
$open_replacement = '<?php';
if ( true === $echo ) {
$open_replacement .= ' echo';
}
if ( T_WHITESPACE !== $tokens[ ( $open_tag_pointer + 1 ) ]['code'] ) {
$open_replacement .= ' ';
}

// Make sure we don't remove any line breaks after the closing tag.
$regex = '`' . preg_quote( trim( $tokens[ $close_tag_pointer ]['content'] ) ) . '`';
$close_replacement = preg_replace( $regex, '?>', $tokens[ $close_tag_pointer ]['content'] );

$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->replaceToken( $open_tag_pointer, $open_replacement );
$phpcsFile->fixer->replaceToken( $close_tag_pointer, $close_replacement );
$phpcsFile->fixer->endChangeset();
}

} // End class.
18 changes: 18 additions & 0 deletions WordPress/Tests/PHP/DisallowAlternativePHPTagsUnitTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div>
<?php echo $var; ?>
Some content here.
<% echo $var; %>
<p>Some text <% echo $var; %> and some more text</p>
<%= $var . ' and some more text to make sure the snippet works'; %>
<p>Some text <%= $var %> and some more text</p>
<script language="php">
echo $var;
</script>
<script language='php'>echo $var;</script>
<script type="text/php" language="php">
echo $var;
</script>
<script language='PHP' type='text/php'>
echo $var;
</script>
</div>
18 changes: 18 additions & 0 deletions WordPress/Tests/PHP/DisallowAlternativePHPTagsUnitTest.inc.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div>
<?php echo $var; ?>
Some content here.
<?php echo $var; ?>
<p>Some text <?php echo $var; ?> and some more text</p>
<?php echo $var . ' and some more text to make sure the snippet works'; ?>
<p>Some text <?php echo $var ?> and some more text</p>
<?php
echo $var;
?>
<?php echo $var;?>
<script type="text/php" language="php">
echo $var;
</script>
<script language='PHP' type='text/php'>
echo $var;
</script>
</div>
98 changes: 98 additions & 0 deletions WordPress/Tests/PHP/DisallowAlternativePHPTagsUnitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php
/**
* Unit test class for WordPress Coding Standard.
*
* @category PHP
* @package PHP\CodeSniffer\WordPress-Coding-Standards
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/
*/

/**
* Unit test class for the DisallowAlternativePHPTags sniff.
*
* A sniff unit test checks a .inc file for expected violations of a single
* coding standard. Expected errors and warnings are stored in this class.
*
* @category PHP
* @package PHP\CodeSniffer\WordPress-Coding-Standards
* @author Juliette Reinders Folmer <[email protected]>
*/
class WordPress_Tests_PHP_DisallowAlternativePHPTagsUnitTest extends AbstractSniffUnitTest {

/**
* Whether ASP tags are enabled or not.
*
* @var bool
*/
private $asp_tags = false;

/**
* Get the ini values only once.
*/
protected function setUp() {
parent::setUp();

if ( version_compare( PHP_VERSION, '7.0.0alpha1', '<' ) ) {
$this->asp_tags = (bool) ini_get( 'asp_tags' );
}
}

/**
* Skip this test on HHVM.
*
* @return bool Whether to skip this test.
*/
protected function shouldSkipTest() {
return defined( 'HHVM_VERSION' );
}

/**
* Returns the lines where errors should occur.
*
* The key of the array should represent the line number and the value
* should represent the number of errors that should occur on that line.
*
* @return array<int, int>
*/
public function getErrorList() {
$errors = array(
8 => 1,
11 => 1,
12 => 1,
15 => 1,
);

if ( true === $this->asp_tags ) {
$errors[4] = 1;
$errors[5] = 1;
$errors[6] = 1;
$errors[7] = 1;
}

return $errors;
}

/**
* Returns the lines where warnings should occur.
*
* The key of the array should represent the line number and the value
* should represent the number of warnings that should occur on that line.
*
* @return array<int, int>
*/
public function getWarningList() {
$warnings = array();

if ( false === $this->asp_tags ) {
$warnings = array(
4 => 1,
5 => 1,
6 => 1,
7 => 1,
);
}

return $warnings;
}

} // End class.
3 changes: 3 additions & 0 deletions myconfig.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
; Allow ASP-style <% %> tags.
; http://php.net/asp-tags
asp_tags = On

0 comments on commit 6759229

Please sign in to comment.