88
99namespace WordPressVIPMinimum \Sniffs \Security ;
1010
11+ use PHP_CodeSniffer \Util \Tokens ;
1112use WordPressVIPMinimum \Sniffs \Sniff ;
1213
1314/**
1718 */
1819class UnderscorejsSniff extends Sniff {
1920
21+ /**
22+ * Regex to match unescaped output notations containing variable interpolation
23+ * and retrieve a code snippet.
24+ *
25+ * @var string
26+ */
27+ const UNESCAPED_INTERPOLATE_REGEX = '`<%=\s*(?:.+?%>|$)` ' ;
28+
29+ /**
30+ * Regex to match execute notations containing a print command
31+ * and retrieve a code snippet.
32+ *
33+ * @var string
34+ */
35+ const UNESCAPED_PRINT_REGEX = '`<%\s*(?:print\s*\(.+?\)\s*;|__p\s*\+=.+?)\s*%>` ' ;
36+
37+ /**
38+ * Regex to match the "interpolate" keyword when used to overrule the ERB-style delimiters.
39+ *
40+ * @var string
41+ */
42+ const INTERPOLATE_KEYWORD_REGEX = '`(?:templateSettings\.interpolate|\.interpolate\s*=\s*/|interpolate\s*:\s*/)` ' ;
43+
2044 /**
2145 * A list of tokenizers this sniff supports.
2246 *
@@ -30,12 +54,11 @@ class UnderscorejsSniff extends Sniff {
3054 * @return array
3155 */
3256 public function register () {
33- return [
34- T_CONSTANT_ENCAPSED_STRING ,
35- T_PROPERTY ,
36- T_INLINE_HTML ,
37- T_HEREDOC ,
38- ];
57+ $ targets = Tokens::$ textStringTokens ;
58+ $ targets [] = T_PROPERTY ;
59+ $ targets [] = T_STRING ;
60+
61+ return $ targets ;
3962 }
4063
4164 /**
@@ -46,15 +69,91 @@ public function register() {
4669 * @return void
4770 */
4871 public function process_token ( $ stackPtr ) {
72+ /*
73+ * Ignore Gruntfile.js files as they are configuration, not code.
74+ */
75+ $ file_name = $ this ->strip_quotes ( $ this ->phpcsFile ->getFileName () );
76+ $ file_name = strtolower ( basename ( $ file_name ) );
77+
78+ if ( $ file_name === 'gruntfile.js ' ) {
79+ return ;
80+ }
81+
82+ /*
83+ * Check for delimiter change in JS files.
84+ */
85+ if ( $ this ->tokens [ $ stackPtr ]['code ' ] === T_STRING
86+ || $ this ->tokens [ $ stackPtr ]['code ' ] === T_PROPERTY
87+ ) {
88+ if ( $ this ->phpcsFile ->tokenizerType !== 'JS ' ) {
89+ // These tokens are only relevant for JS files.
90+ return ;
91+ }
92+
93+ if ( $ this ->tokens [ $ stackPtr ]['content ' ] !== 'interpolate ' ) {
94+ return ;
95+ }
96+
97+ // Check the context to prevent false positives.
98+ if ( $ this ->tokens [ $ stackPtr ]['code ' ] === T_STRING ) {
99+ $ prev = $ this ->phpcsFile ->findPrevious ( Tokens::$ emptyTokens , ( $ stackPtr - 1 ), null , true );
100+ if ( $ prev === false || $ this ->tokens [ $ prev ]['code ' ] !== T_OBJECT_OPERATOR ) {
101+ return ;
102+ }
103+
104+ $ prevPrev = $ this ->phpcsFile ->findPrevious ( Tokens::$ emptyTokens , ( $ stackPtr - 1 ), null , true );
105+ $ next = $ this ->phpcsFile ->findNext ( Tokens::$ emptyTokens , ( $ stackPtr + 1 ), null , true );
106+ if ( ( $ prevPrev === false
107+ || $ this ->tokens [ $ prevPrev ]['code ' ] !== T_STRING
108+ || $ this ->tokens [ $ prevPrev ]['content ' ] !== 'templateSettings ' )
109+ && ( $ next === false
110+ || $ this ->tokens [ $ next ]['code ' ] !== T_EQUAL )
111+ ) {
112+ return ;
113+ }
114+ }
115+
116+ // Underscore.js delimiter change.
117+ $ message = 'Found Underscore.js delimiter change notation. ' ;
118+ $ this ->phpcsFile ->addWarning ( $ message , $ stackPtr , 'InterpolateFound ' );
119+
120+ return ;
121+ }
122+
123+ $ content = $ this ->strip_quotes ( $ this ->tokens [ $ stackPtr ]['content ' ] );
124+
125+ $ match_count = preg_match_all ( self ::UNESCAPED_INTERPOLATE_REGEX , $ content , $ matches );
126+ if ( $ match_count > 0 ) {
127+ foreach ( $ matches [0 ] as $ match ) {
128+ if ( strpos ( $ match , '_.escape( ' ) !== false ) {
129+ continue ;
130+ }
131+
132+ // Underscore.js unescaped output.
133+ $ message = 'Found Underscore.js unescaped output notation: "%s". ' ;
134+ $ data = [ $ match ];
135+ $ this ->phpcsFile ->addWarning ( $ message , $ stackPtr , 'OutputNotation ' , $ data );
136+ }
137+ }
138+
139+ $ match_count = preg_match_all ( self ::UNESCAPED_PRINT_REGEX , $ content , $ matches );
140+ if ( $ match_count > 0 ) {
141+ foreach ( $ matches [0 ] as $ match ) {
142+ if ( strpos ( $ match , '_.escape( ' ) !== false ) {
143+ continue ;
144+ }
49145
50- if ( strpos ( $ this ->tokens [ $ stackPtr ]['content ' ], '<%= ' ) !== false ) {
51- // Underscore.js unescaped output.
52- $ message = 'Found Underscore.js unescaped output notation: "<%=". ' ;
53- $ this ->phpcsFile ->addWarning ( $ message , $ stackPtr , 'OutputNotation ' );
146+ // Underscore.js unescaped output.
147+ $ message = 'Found Underscore.js unescaped print execution: "%s". ' ;
148+ $ data = [ $ match ];
149+ $ this ->phpcsFile ->addWarning ( $ message , $ stackPtr , 'PrintExecution ' , $ data );
150+ }
54151 }
55152
56- if ( strpos ( $ this ->tokens [ $ stackPtr ]['content ' ], 'interpolate ' ) !== false ) {
57- // Underscore.js unescaped output.
153+ if ( $ this ->phpcsFile ->tokenizerType !== 'JS '
154+ && preg_match ( self ::INTERPOLATE_KEYWORD_REGEX , $ content ) > 0
155+ ) {
156+ // Underscore.js delimiter change.
58157 $ message = 'Found Underscore.js delimiter change notation. ' ;
59158 $ this ->phpcsFile ->addWarning ( $ message , $ stackPtr , 'InterpolateFound ' );
60159 }
0 commit comments