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