1+ <?php
2+ /**
3+ * StellarWP Coding Standards.
4+ *
5+ * @package StellarWP\Sniffs\Security
6+ * @since TBD
7+ */
8+
9+ namespace TEC \Sniffs \Security ;
10+
11+ use PHP_CodeSniffer \Files \File ;
12+ use PHP_CodeSniffer \Sniffs \Sniff ;
13+ use PHP_CodeSniffer \Util \Tokens ;
14+
15+ /**
16+ * Checks that functions which require exit or die after are not left without them.
17+ *
18+ * @since TBD
19+ */
20+ class ExitAfterRedirectSniff implements Sniff {
21+ /**
22+ * Functions that need to be followed by an exit.
23+ *
24+ * @since TBD
25+ *
26+ * @var array<string>
27+ */
28+ public $ functions = [
29+ 'wp_redirect ' ,
30+ 'wp_safe_redirect ' ,
31+ 'wp_doing_ajax ' ,
32+ ];
33+
34+ /**
35+ * Returns an array of tokens this test wants to listen for.
36+ *
37+ * @since TBD
38+ *
39+ * @return array<int|string>
40+ */
41+ public function register () {
42+ return [ T_STRING ];
43+ }
44+
45+ /**
46+ * Processes this test, when one of its tokens is encountered.
47+ *
48+ * @since TBD
49+ *
50+ * @param File $phpcsFile The file being scanned.
51+ * @param int $stackPtr The position of the current token in the stack.
52+ *
53+ * @return void
54+ */
55+ public function process ( File $ phpcsFile , $ stackPtr ) {
56+ $ tokens = $ phpcsFile ->getTokens ();
57+
58+ // Find the function call.
59+ $ name = $ tokens [ $ stackPtr ]['content ' ];
60+ $ function_name = strtolower ( $ name );
61+
62+ if ( ! in_array ( $ function_name , $ this ->functions , true ) ) {
63+ return ;
64+ }
65+
66+ // Find the opening and closing parenthesis of the function call.
67+ $ open_paren = $ phpcsFile ->findNext ( Tokens::$ emptyTokens , $ stackPtr + 1 , null , true );
68+ if ( ! isset ( $ tokens [ $ open_paren ] ) || $ tokens [ $ open_paren ]['code ' ] !== T_OPEN_PARENTHESIS ) {
69+ return ;
70+ }
71+
72+ // Check if the function call is followed by a semicolon (end of statement).
73+ $ close_paren = $ tokens [ $ open_paren ]['parenthesis_closer ' ];
74+ $ next_token = $ phpcsFile ->findNext ( Tokens::$ emptyTokens , $ close_paren + 1 , null , true );
75+
76+ // If the next non-empty token is a semicolon, we need to check if an exit follows.
77+ if ( isset ( $ tokens [ $ next_token ] ) && $ tokens [ $ next_token ]['code ' ] === T_SEMICOLON ) {
78+ // Check if exit follows in the current scope.
79+ $ exit_found = false ;
80+ $ start = $ next_token + 1 ;
81+ $ end = $ phpcsFile ->numTokens ;
82+
83+ // If we're in a function or method, only search until the end of the function.
84+ if ( isset ( $ tokens [ $ stackPtr ]['conditions ' ] ) ) {
85+ foreach ( $ tokens [ $ stackPtr ]['conditions ' ] as $ scope => $ type ) {
86+ if ( in_array ( $ type , [ T_FUNCTION , T_CLOSURE , T_ANON_CLASS ], true ) ) {
87+ if ( isset ( $ tokens [ $ scope ]['scope_closer ' ] ) ) {
88+ $ end = $ tokens [ $ scope ]['scope_closer ' ];
89+ }
90+ break ;
91+ }
92+ }
93+ }
94+
95+ // Search for exit or die statements.
96+ for ( $ i = $ start ; $ i < $ end ; $ i ++ ) {
97+ // Check for exit or die calls
98+ if ( isset ( $ tokens [ $ i ] ) ) {
99+ $ token_code = $ tokens [ $ i ]['code ' ];
100+ $ token_content = isset ( $ tokens [ $ i ]['content ' ] ) ? strtolower ( $ tokens [ $ i ]['content ' ] ) : '' ;
101+
102+ // Check for exit, die, or return statements
103+ if (
104+ $ token_code === T_EXIT
105+ || ( $ token_code === T_STRING && in_array ( $ token_content , [ 'die ' , 'tribe_exit ' , 'tec_exit ' ], true ) )
106+ || $ token_code === T_RETURN
107+ ) {
108+ $ exit_found = true ;
109+ break ;
110+ }
111+ }
112+ }
113+
114+ if ( ! $ exit_found ) {
115+ $ phpcsFile ->addError (
116+ '%s() should be followed by a call to exit; for proper redirection. ' ,
117+ $ stackPtr ,
118+ 'NoExit ' ,
119+ [ $ name ]
120+ );
121+ }
122+ }
123+ }
124+ }
0 commit comments