1<?php
2/**
3 * Generic_Sniffs_PHP_DisallowAlternativePHPTagsSniff.
4 *
5 * PHP version 5
6 *
7 * @category  PHP
8 * @package   PHP_CodeSniffer
9 * @author    Greg Sherwood <gsherwood@squiz.net>
10 * @author    Marc McIntyre <mmcintyre@squiz.net>
11 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
12 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
13 * @link      http://pear.php.net/package/PHP_CodeSniffer
14 */
15
16/**
17 * Generic_Sniffs_PHP_DisallowAlternativePHPTagsSniff.
18 *
19 * Verifies that no alternative PHP tags are used.
20 *
21 * If alternative PHP open tags are found, this sniff can fix both the open and close tags.
22 *
23 * @category  PHP
24 * @package   PHP_CodeSniffer
25 * @author    Greg Sherwood <gsherwood@squiz.net>
26 * @author    Marc McIntyre <mmcintyre@squiz.net>
27 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
28 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
29 * @version   Release: @package_version@
30 * @link      http://pear.php.net/package/PHP_CodeSniffer
31 */
32class Generic_Sniffs_PHP_DisallowAlternativePHPTagsSniff implements PHP_CodeSniffer_Sniff
33{
34
35
36    /**
37     * Whether ASP tags are enabled or not.
38     *
39     * @var bool
40     */
41    private $_aspTags = false;
42
43    /**
44     * The current PHP version.
45     *
46     * @var integer
47     */
48    private $_phpVersion = null;
49
50
51    /**
52     * Returns an array of tokens this test wants to listen for.
53     *
54     * @return array
55     */
56    public function register()
57    {
58        if ($this->_phpVersion === null) {
59            $this->_phpVersion = PHP_CodeSniffer::getConfigData('php_version');
60            if ($this->_phpVersion === null) {
61                $this->_phpVersion = PHP_VERSION_ID;
62            }
63        }
64
65        if ($this->_phpVersion < 70000) {
66            $this->_aspTags = (boolean) ini_get('asp_tags');
67        }
68
69        return array(
70                T_OPEN_TAG,
71                T_OPEN_TAG_WITH_ECHO,
72                T_INLINE_HTML,
73               );
74
75    }//end register()
76
77
78    /**
79     * Processes this test, when one of its tokens is encountered.
80     *
81     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
82     * @param int                  $stackPtr  The position of the current token
83     *                                        in the stack passed in $tokens.
84     *
85     * @return void
86     */
87    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
88    {
89        $tokens  = $phpcsFile->getTokens();
90        $openTag = $tokens[$stackPtr];
91        $content = $openTag['content'];
92
93        if (trim($content) === '') {
94            return;
95        }
96
97        if ($openTag['code'] === T_OPEN_TAG) {
98            if ($content === '<%') {
99                $error     = 'ASP style opening tag used; expected "<?php" but found "%s"';
100                $closer    = $this->findClosingTag($phpcsFile, $tokens, $stackPtr, '%>');
101                $errorCode = 'ASPOpenTagFound';
102            } else if (strpos($content, '<script ') !== false) {
103                $error     = 'Script style opening tag used; expected "<?php" but found "%s"';
104                $closer    = $this->findClosingTag($phpcsFile, $tokens, $stackPtr, '</script>');
105                $errorCode = 'ScriptOpenTagFound';
106            }
107
108            if (isset($error, $closer, $errorCode) === true) {
109                $data = array($content);
110
111                if ($closer === false) {
112                    $phpcsFile->addError($error, $stackPtr, $errorCode, $data);
113                } else {
114                    $fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data);
115                    if ($fix === true) {
116                        $this->addChangeset($phpcsFile, $tokens, $stackPtr, $closer);
117                    }
118                }
119            }
120
121            return;
122        }//end if
123
124        if ($openTag['code'] === T_OPEN_TAG_WITH_ECHO && $content === '<%=') {
125            $error   = 'ASP style opening tag used with echo; expected "<?php echo %s ..." but found "%s %s ..."';
126            $nextVar = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
127            $snippet = $this->getSnippet($tokens[$nextVar]['content']);
128            $data    = array(
129                        $snippet,
130                        $content,
131                        $snippet,
132                       );
133
134            $closer = $this->findClosingTag($phpcsFile, $tokens, $stackPtr, '%>');
135
136            if ($closer === false) {
137                $phpcsFile->addError($error, $stackPtr, 'ASPShortOpenTagFound', $data);
138            } else {
139                $fix = $phpcsFile->addFixableError($error, $stackPtr, 'ASPShortOpenTagFound', $data);
140                if ($fix === true) {
141                    $this->addChangeset($phpcsFile, $tokens, $stackPtr, $closer, true);
142                }
143            }
144
145            return;
146        }//end if
147
148        // Account for incorrect script open tags.
149        // The "(?:<s)?" in the regex is to work-around a bug in PHP 5.2.
150        if ($openTag['code'] === T_INLINE_HTML
151            && preg_match('`((?:<s)?cript (?:[^>]+)?language=[\'"]?php[\'"]?(?:[^>]+)?>)`i', $content, $match) === 1
152        ) {
153            $error   = 'Script style opening tag used; expected "<?php" but found "%s"';
154            $snippet = $this->getSnippet($content, $match[1]);
155            $data    = array($match[1].$snippet);
156
157            $phpcsFile->addError($error, $stackPtr, 'ScriptOpenTagFound', $data);
158            return;
159        }
160
161        if ($openTag['code'] === T_INLINE_HTML && $this->_aspTags === false) {
162            if (strpos($content, '<%=') !== false) {
163                $error   = 'Possible use of ASP style short opening tags detected; found: %s';
164                $snippet = $this->getSnippet($content, '<%=');
165                $data    = array('<%='.$snippet);
166
167                $phpcsFile->addWarning($error, $stackPtr, 'MaybeASPShortOpenTagFound', $data);
168            } else if (strpos($content, '<%') !== false) {
169                $error   = 'Possible use of ASP style opening tags detected; found: %s';
170                $snippet = $this->getSnippet($content, '<%');
171                $data    = array('<%'.$snippet);
172
173                $phpcsFile->addWarning($error, $stackPtr, 'MaybeASPOpenTagFound', $data);
174            }
175        }
176
177    }//end process()
178
179
180    /**
181     * Get a snippet from a HTML token.
182     *
183     * @param string $content The content of the HTML token.
184     * @param string $start   Partial string to use as a starting point for the snippet.
185     * @param int    $length  The target length of the snippet to get. Defaults to 40.
186     *
187     * @return string
188     */
189    protected function getSnippet($content, $start='', $length=40)
190    {
191        $startPos = 0;
192
193        if ($start !== '') {
194            $startPos = strpos($content, $start);
195            if ($startPos !== false) {
196                $startPos += strlen($start);
197            }
198        }
199
200        $snippet = substr($content, $startPos, $length);
201        if ((strlen($content) - $startPos) > $length) {
202            $snippet .= '...';
203        }
204
205        return $snippet;
206
207    }//end getSnippet()
208
209
210    /**
211     * Try and find a matching PHP closing tag.
212     *
213     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
214     * @param array                $tokens    The token stack.
215     * @param int                  $stackPtr  The position of the current token
216     *                                        in the stack passed in $tokens.
217     * @param string               $content   The expected content of the closing tag to match the opener.
218     *
219     * @return int|false Pointer to the position in the stack for the closing tag or false if not found.
220     */
221    protected function findClosingTag(PHP_CodeSniffer_File $phpcsFile, $tokens, $stackPtr, $content)
222    {
223        $closer = $phpcsFile->findNext(T_CLOSE_TAG, ($stackPtr + 1));
224
225        if ($closer !== false && $content === trim($tokens[$closer]['content'])) {
226            return $closer;
227        }
228
229        return false;
230
231    }//end findClosingTag()
232
233
234    /**
235     * Add a changeset to replace the alternative PHP tags.
236     *
237     * @param PHP_CodeSniffer_File $phpcsFile         The file being scanned.
238     * @param array                $tokens            The token stack.
239     * @param int                  $open_tag_pointer  Stack pointer to the PHP open tag.
240     * @param int                  $close_tag_pointer Stack pointer to the PHP close tag.
241     * @param bool                 $echo              Whether to add 'echo' or not.
242     *
243     * @return void
244     */
245    protected function addChangeset(PHP_CodeSniffer_File $phpcsFile, $tokens, $open_tag_pointer, $close_tag_pointer, $echo = false)
246    {
247        // Build up the open tag replacement and make sure there's always whitespace behind it.
248        $open_replacement = '<?php';
249        if ($echo === true) {
250            $open_replacement .= ' echo';
251        }
252
253        if ($tokens[($open_tag_pointer + 1)]['code'] !== T_WHITESPACE) {
254            $open_replacement .= ' ';
255        }
256
257        // Make sure we don't remove any line breaks after the closing tag.
258        $regex = '`'.preg_quote(trim($tokens[$close_tag_pointer]['content'])).'`';
259        $close_replacement = preg_replace($regex, '?>', $tokens[$close_tag_pointer]['content']);
260
261        $phpcsFile->fixer->beginChangeset();
262        $phpcsFile->fixer->replaceToken($open_tag_pointer, $open_replacement);
263        $phpcsFile->fixer->replaceToken($close_tag_pointer, $close_replacement);
264        $phpcsFile->fixer->endChangeset();
265
266    }//end addChangeset()
267
268
269}//end class
270