Completed
Pull Request — master (#32767)
by
unknown
09:29
created

NodeVisitor::checkBlackList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 3
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Joas Schilling <[email protected]>
4
 * @author Morris Jobke <[email protected]>
5
 * @author Thomas Müller <[email protected]>
6
 *
7
 * @copyright Copyright (c) 2018, ownCloud GmbH
8
 * @license AGPL-3.0
9
 *
10
 * This code is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License, version 3,
12
 * as published by the Free Software Foundation.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License, version 3,
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21
 *
22
 */
23
24
namespace OC\App\CodeChecker;
25
26
use PhpParser\Node;
27
use PhpParser\Node\Name;
28
use PhpParser\NodeVisitorAbstract;
29
30
class NodeVisitor extends NodeVisitorAbstract {
31
	/** @var ICheck */
32
	protected $list;
33
34
	/** @var string */
35
	protected $blackListDescription;
36
	/** @var string[] */
37
	protected $blackListedClassNames;
38
	/** @var string[] */
39
	protected $blackListedConstants;
40
	/** @var string[] */
41
	protected $blackListedFunctions;
42
	/** @var string[] */
43
	protected $blackListedMethods;
44
	/** @var bool */
45
	protected $checkEqualOperatorUsage;
46
	/** @var string[] */
47
	protected $errorMessages;
48
49
	/**
50
	 * @param ICheck $list
51
	 */
52
	public function __construct(ICheck $list) {
53
		$this->list = $list;
54
55
		$this->blackListedClassNames = [];
56
		foreach ($list->getClasses() as $class => $blackListInfo) {
57
			if (\is_numeric($class) && \is_string($blackListInfo)) {
58
				$class = $blackListInfo;
59
				$blackListInfo = null;
60
			}
61
62
			$class = \strtolower($class);
63
			$this->blackListedClassNames[$class] = $class;
64
		}
65
66
		$this->blackListedConstants = [];
67
		foreach ($list->getConstants() as $constantName => $blackListInfo) {
68
			$constantName = \strtolower($constantName);
69
			$this->blackListedConstants[$constantName] = $constantName;
70
		}
71
72
		$this->blackListedFunctions = [];
73
		foreach ($list->getFunctions() as $functionName => $blackListInfo) {
74
			$functionName = \strtolower($functionName);
75
			$this->blackListedFunctions[$functionName] = $functionName;
76
		}
77
78
		$this->blackListedMethods = [];
79
		foreach ($list->getMethods() as $functionName => $blackListInfo) {
80
			$functionName = \strtolower($functionName);
81
			$this->blackListedMethods[$functionName] = $functionName;
82
		}
83
84
		$this->checkEqualOperatorUsage = $list->checkStrongComparisons();
85
86
		$this->errorMessages = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array(\OC\App\CodeChecke...ED => 'is discouraged') of type array<string|integer,string> is incompatible with the declared type array<integer,string> of property $errorMessages.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
87
			CodeChecker::CLASS_EXTENDS_NOT_ALLOWED => "%s class must not be extended",
88
			CodeChecker::CLASS_IMPLEMENTS_NOT_ALLOWED => "%s interface must not be implemented",
89
			CodeChecker::STATIC_CALL_NOT_ALLOWED => "Static method of %s class must not be called",
90
			CodeChecker::CLASS_CONST_FETCH_NOT_ALLOWED => "Constant of %s class must not not be fetched",
91
			CodeChecker::CLASS_NEW_NOT_ALLOWED => "%s class must not be instantiated",
92
			CodeChecker::CLASS_USE_NOT_ALLOWED => "%s class must not be imported with a use statement",
93
			CodeChecker::CLASS_METHOD_CALL_NOT_ALLOWED => "Method of %s class must not be called",
94
95
			CodeChecker::OP_OPERATOR_USAGE_DISCOURAGED => "is discouraged",
96
		];
97
	}
98
99
	/** @var array */
100
	public $errors = [];
101
102
	public function enterNode(Node $node) {
103 View Code Duplication
		if ($this->checkEqualOperatorUsage && $node instanceof Node\Expr\BinaryOp\Equal) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
104
			$this->errors[]= [
105
				'disallowedToken' => '==',
106
				'errorCode' => CodeChecker::OP_OPERATOR_USAGE_DISCOURAGED,
107
				'line' => $node->getLine(),
108
				'reason' => $this->buildReason('==', CodeChecker::OP_OPERATOR_USAGE_DISCOURAGED)
109
			];
110
		}
111 View Code Duplication
		if ($this->checkEqualOperatorUsage && $node instanceof Node\Expr\BinaryOp\NotEqual) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
112
			$this->errors[]= [
113
				'disallowedToken' => '!=',
114
				'errorCode' => CodeChecker::OP_OPERATOR_USAGE_DISCOURAGED,
115
				'line' => $node->getLine(),
116
				'reason' => $this->buildReason('!=', CodeChecker::OP_OPERATOR_USAGE_DISCOURAGED)
117
			];
118
		}
119
		if ($node instanceof Node\Stmt\Class_) {
120
			if ($node->extends !== null) {
121
				$this->checkBlackList($node->extends->toString(), CodeChecker::CLASS_EXTENDS_NOT_ALLOWED, $node);
122
			}
123
			foreach ($node->implements as $implements) {
124
				$this->checkBlackList($implements->toString(), CodeChecker::CLASS_IMPLEMENTS_NOT_ALLOWED, $node);
125
			}
126
		}
127
		if ($node instanceof Node\Expr\StaticCall) {
128
			if ($node->class !== null) {
129
				if ($node->class instanceof Name) {
130
					$this->checkBlackList($node->class->toString(), CodeChecker::STATIC_CALL_NOT_ALLOWED, $node);
0 ignored issues
show
Bug introduced by
The method toString does only exist in PhpParser\Node\Name, but not in PhpParser\Node\Expr.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
131
132
					$this->checkBlackListFunction($node->class->toString(), $node->name, $node);
133
					$this->checkBlackListMethod($node->class->toString(), $node->name, $node);
134
				}
135
			}
136
		}
137
		if ($node instanceof Node\Expr\ClassConstFetch) {
138
			if ($node->class !== null) {
139
				if ($node->class instanceof Name) {
140
					$this->checkBlackList($node->class->toString(), CodeChecker::CLASS_CONST_FETCH_NOT_ALLOWED, $node);
141
				}
142
				if ($node->class instanceof Node\Expr\Variable) {
143
					/**
144
					 * TODO: find a way to detect something like this:
145
					 *       $c = "OC_API";
146
					 *       $n = $i::ADMIN_AUTH;
147
					 */
148
				} else {
149
					$this->checkBlackListConstant($node->class->toString(), $node->name, $node);
150
				}
151
			}
152
		}
153
		if ($node instanceof Node\Expr\New_) {
154
			if ($node->class !== null) {
155
				if ($node->class instanceof Name) {
156
					$this->checkBlackList($node->class->toString(), CodeChecker::CLASS_NEW_NOT_ALLOWED, $node);
157
				}
158
			}
159
		}
160
		if ($node instanceof Node\Stmt\UseUse) {
161
			$this->checkBlackList($node->name->toString(), CodeChecker::CLASS_USE_NOT_ALLOWED, $node);
162
			if ($node->alias) {
163
				$this->addUseNameToBlackList($node->name->toString(), $node->alias);
164
			} else {
165
				$this->addUseNameToBlackList($node->name->toString(), $node->name->getLast());
166
			}
167
		}
168
	}
169
170
	/**
171
	 * Check whether an alias was introduced for a namespace of a blacklisted class
172
	 *
173
	 * Example:
174
	 * - Blacklist entry:      OCP\AppFramework\IApi
175
	 * - Name:                 OCP\AppFramework
176
	 * - Alias:                OAF
177
	 * =>  new blacklist entry:  OAF\IApi
178
	 *
179
	 * @param string $name
180
	 * @param string $alias
181
	 */
182
	private function addUseNameToBlackList($name, $alias) {
183
		$name = \strtolower($name);
184
		$alias = \strtolower($alias);
185
186
		foreach ($this->blackListedClassNames as $blackListedAlias => $blackListedClassName) {
187
			if (\strpos($blackListedClassName, $name . '\\') === 0) {
188
				$aliasedClassName = \str_replace($name, $alias, $blackListedClassName);
189
				$this->blackListedClassNames[$aliasedClassName] = $blackListedClassName;
190
			}
191
		}
192
193 View Code Duplication
		foreach ($this->blackListedConstants as $blackListedAlias => $blackListedConstant) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
194
			if (\strpos($blackListedConstant, $name . '\\') === 0 || \strpos($blackListedConstant, $name . '::') === 0) {
195
				$aliasedConstantName = \str_replace($name, $alias, $blackListedConstant);
196
				$this->blackListedConstants[$aliasedConstantName] = $blackListedConstant;
197
			}
198
		}
199
200 View Code Duplication
		foreach ($this->blackListedFunctions as $blackListedAlias => $blackListedFunction) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
201
			if (\strpos($blackListedFunction, $name . '\\') === 0 || \strpos($blackListedFunction, $name . '::') === 0) {
202
				$aliasedFunctionName = \str_replace($name, $alias, $blackListedFunction);
203
				$this->blackListedFunctions[$aliasedFunctionName] = $blackListedFunction;
204
			}
205
		}
206
207 View Code Duplication
		foreach ($this->blackListedMethods as $blackListedAlias => $blackListedMethod) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
208
			if (\strpos($blackListedMethod, $name . '\\') === 0 || \strpos($blackListedMethod, $name . '::') === 0) {
209
				$aliasedMethodName = \str_replace($name, $alias, $blackListedMethod);
210
				$this->blackListedMethods[$aliasedMethodName] = $blackListedMethod;
211
			}
212
		}
213
	}
214
215
	private function checkBlackList($name, $errorCode, Node $node) {
216
		$lowerName = \strtolower($name);
217
218
		if (isset($this->blackListedClassNames[$lowerName])) {
219
			$this->errors[]= [
220
				'disallowedToken' => $name,
221
				'errorCode' => $errorCode,
222
				'line' => $node->getLine(),
223
				'reason' => $this->buildReason($this->blackListedClassNames[$lowerName], $errorCode)
224
			];
225
		}
226
	}
227
228 View Code Duplication
	private function checkBlackListConstant($class, $constantName, Node $node) {
229
		$name = $class . '::' . $constantName;
230
		$lowerName = \strtolower($name);
231
232
		if (isset($this->blackListedConstants[$lowerName])) {
233
			$this->errors[]= [
234
				'disallowedToken' => $name,
235
				'errorCode' => CodeChecker::CLASS_CONST_FETCH_NOT_ALLOWED,
236
				'line' => $node->getLine(),
237
				'reason' => $this->buildReason($this->blackListedConstants[$lowerName], CodeChecker::CLASS_CONST_FETCH_NOT_ALLOWED)
238
			];
239
		}
240
	}
241
242 View Code Duplication
	private function checkBlackListFunction($class, $functionName, Node $node) {
243
		$name = $class . '::' . $functionName;
244
		$lowerName = \strtolower($name);
245
246
		if (isset($this->blackListedFunctions[$lowerName])) {
247
			$this->errors[]= [
248
				'disallowedToken' => $name,
249
				'errorCode' => CodeChecker::STATIC_CALL_NOT_ALLOWED,
250
				'line' => $node->getLine(),
251
				'reason' => $this->buildReason($this->blackListedFunctions[$lowerName], CodeChecker::STATIC_CALL_NOT_ALLOWED)
252
			];
253
		}
254
	}
255
256 View Code Duplication
	private function checkBlackListMethod($class, $functionName, Node $node) {
257
		$name = $class . '::' . $functionName;
258
		$lowerName = \strtolower($name);
259
260
		if (isset($this->blackListedMethods[$lowerName])) {
261
			$this->errors[]= [
262
				'disallowedToken' => $name,
263
				'errorCode' => CodeChecker::CLASS_METHOD_CALL_NOT_ALLOWED,
264
				'line' => $node->getLine(),
265
				'reason' => $this->buildReason($this->blackListedMethods[$lowerName], CodeChecker::CLASS_METHOD_CALL_NOT_ALLOWED)
266
			];
267
		}
268
	}
269
270
	private function buildReason($name, $errorCode) {
271
		if (isset($this->errorMessages[$errorCode])) {
272
			$desc = $this->list->getDescription($errorCode, $name);
273
			return \sprintf($this->errorMessages[$errorCode], $desc);
274
		}
275
276
		return "$name usage not allowed - error: $errorCode";
277
	}
278
}
279