Issues (3882)

Security Analysis    39 potential vulnerabilities

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting (9)
Response Splitting can be used to send arbitrary responses.
  File Manipulation (2)
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure (7)
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection (13)
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting (8)
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

app/AutoAssign.php (7 issues)

1
<?php
2
/**
3
 * Auto Assign file.
4
 *
5
 * The file is part of the paid functionality. Using the file is allowed only after purchasing a subscription.
6
 * File modification allowed only with the consent of the system producer.
7
 *
8
 * @package App
9
 *
10
 * @copyright YetiForce S.A.
11
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
12
 * @author    Radosław Skrzypczak <[email protected]>
13
 * @author    Mariusz Krzaczkowski <[email protected]>
14
 */
15
16
namespace App;
17
18
/**
19
 * Auto Assign class.
20
 */
21
class AutoAssign extends Base
22
{
23
	/** @var string Basic table name */
24
	public const TABLE_NAME = 's_#__auto_assign';
25
	/** @var array Members tables */
26
	public const MEMBERS_TABLES = ['s_#__auto_assign_users' => 'id', 's_#__auto_assign_groups' => 'id', 's_#__auto_assign_roles' => 'id'];
27
	/** @var string Round robin table name */
28
	public const ROUND_ROBIN_TABLE = 'u_#__auto_assign_rr';
29
30
	/** @var int Status inactive */
31
	public const STATUS_INACTIVE = 0;
32
	/** @var int Status active */
33
	public const STATUS_ACTIVE = 1;
34
35
	/** @var int Manual mode */
36
	public const MODE_MANUAL = 1;
37
	/** @var int Handler mode */
38
	public const MODE_HANDLER = 2;
39
	/** @var int Workflow mode */
40
	public const MODE_WORKFLOW = 4;
41
42
	/** @var int Load balance method */
43
	public const METHOD_LOAD_BALANCE = 0;
44
	/** @var int Round robin method */
45
	public const METHOD_ROUND_ROBIN = 1;
46
47
	/**
48
	 * Get all auto assign entries for module.
49
	 *
50
	 * @param string $moduleName
51
	 * @param int    $mode       A bitmask of one or more of the mode flags
52
	 * @param int    $state
53
	 *
54
	 * @return array
55
	 */
56
	public static function getByModule(string $moduleName, int $mode = self::MODE_HANDLER | self::MODE_WORKFLOW | self::MODE_MANUAL, int $state = self::STATUS_ACTIVE): array
57
	{
58
		$query = (new Db\Query())->from(self::TABLE_NAME)
59
			->where(['tabid' => Module::getModuleId($moduleName), 'state' => $state]);
60
		$mods = ['or'];
61
		foreach ([self::MODE_MANUAL => 'gui', self::MODE_HANDLER => 'handler', self::MODE_WORKFLOW => 'workflow'] as $key => $column) {
62
			if ($mode & $key) {
63
				$mods[] = [$column => 1];
64
			}
65
		}
66
		$query->andWhere($mods);
67
68
		return $query->all();
69
	}
70
71
	/**
72
	 * Get all auto assign instances for module.
73
	 *
74
	 * @param string   $moduleName
75
	 * @param int|null $mode       A bitmask of one or more of the mode flags
76
	 *
77
	 * @return array
78
	 */
79
	public static function getInstancesByModule(string $moduleName, int $mode = null): array
80
	{
81
		$instances = [];
82
		foreach (self::getByModule($moduleName, $mode) as $autoAssignData) {
83
			$instances[$autoAssignData['id']] = self::getInstance($autoAssignData);
84
		}
85
		return $instances;
86
	}
87
88
	/**
89
	 * Get auto assign instance for record.
90
	 *
91
	 * @param \Vtiger_Record_Model $recordModel
92
	 * @param int|null             $mode        A bitmask of one or more of the mode flags
93
	 *
94
	 * @return self|null
95
	 */
96
	public static function getAutoAssignForRecord(\Vtiger_Record_Model $recordModel, int $mode = null): ?self
97
	{
98
		$autoAssignInstance = null;
99
		if (!\App\YetiForce\Shop::check('YetiForceAutoAssignment')) {
100
			return $autoAssignInstance;
101
		}
102
		foreach (self::getByModule($recordModel->getModuleName(), $mode) as $autoAssignData) {
103
			$conditions = Json::isEmpty($autoAssignData['conditions']) ? [] : Json::decode($autoAssignData['conditions']);
104
			if (Condition::checkConditions($conditions, $recordModel)) {
105
				$autoAssignInstance = self::getInstance($autoAssignData);
106
				break;
107
			}
108
		}
109
		return $autoAssignInstance;
110
	}
111
112
	/**
113
	 * Get auto assign instance by ID.
114
	 *
115
	 * @param int $id
116
	 *
117
	 * @return self|null
118
	 */
119
	public static function getInstanceById(int $id): ?self
120
	{
121
		$data = (new Db\Query())->from(self::TABLE_NAME)->where(['id' => $id])->one();
122
		return $data ? (new self())->setData($data) : null;
123
	}
124
125
	/**
126
	 * Get auto assign instance by data.
127
	 *
128
	 * @param array $data
129
	 *
130
	 * @return self|null
131
	 */
132
	public static function getInstance(array $data): ?self
133
	{
134
		return $data ? (new self())->setData($data) : null;
135
	}
136
137
	/**
138
	 * Function to get the Id.
139
	 *
140
	 * @return int
141
	 */
142
	public function getId(): int
143
	{
144
		return $this->get('id');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('id') could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
145
	}
146
147
	/**
148
	 * Get name of auto assign instance.
149
	 *
150
	 * @param bool $encode
151
	 *
152
	 * @return string
153
	 */
154
	public function getName(bool $encode = true): string
155
	{
156
		return Language::translate($this->get('subject'), 'Settings:AutomaticAssignment', false, $encode);
0 ignored issues
show
false of type false is incompatible with the type null|string expected by parameter $language of App\Language::translate(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

156
		return Language::translate($this->get('subject'), 'Settings:AutomaticAssignment', /** @scrutinizer ignore-type */ false, $encode);
Loading history...
157
	}
158
159
	/**
160
	 * Get module name.
161
	 *
162
	 * @return string
163
	 */
164
	public function getModuleName(): string
165
	{
166
		return Module::getModuleName($this->get('tabid'));
0 ignored issues
show
Bug Best Practice introduced by
The expression return App\Module::getMo...me($this->get('tabid')) could return the type boolean which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
167
	}
168
169
	/**
170
	 * Check conditions for record.
171
	 *
172
	 * @param \Vtiger_Record_Model $recordModel
173
	 *
174
	 * @return bool
175
	 */
176
	public function checkConditionForRecord(\Vtiger_Record_Model $recordModel): bool
177
	{
178
		$conditions = Json::isEmpty($this->get('conditions')) ? [] : Json::decode($this->get('conditions'));
179
		return Condition::checkConditions($conditions, $recordModel);
180
	}
181
182
	/**
183
	 * Check if the instance is active in a given mode.
184
	 *
185
	 * @param int $mode
186
	 *
187
	 * @return bool
188
	 */
189
	public function isActive(int $mode): bool
190
	{
191
		switch ($mode) {
192
			case self::MODE_MANUAL:
193
				$result = !$this->isEmpty('gui');
194
				break;
195
			case self::MODE_HANDLER:
196
				$result = !$this->isEmpty('handler');
197
				break;
198
			case self::MODE_WORKFLOW:
199
				$result = !$this->isEmpty('workflow');
200
				break;
201
			default:
202
				$result = false;
203
				break;
204
		}
205
		return $result && self::STATUS_ACTIVE === (int) $this->get('state');
206
	}
207
208
	/**
209
	 * Get an automatic selected user ID.
210
	 *
211
	 * @return int
212
	 */
213
	public function getOwner(): ?int
214
	{
215
		switch ($this->get('method')) {
216
			case self::METHOD_LOAD_BALANCE:
217
				$owner = $this->getQueryByLoadBalance()->scalar() ?: null;
218
				break;
219
			case self::METHOD_ROUND_ROBIN:
220
				$owner = $this->getQueryByRoundRobin()->scalar() ?: null;
221
				break;
222
			default:
223
				$owner = null;
224
				break;
225
		}
226
227
		return $owner ? $owner : $this->getDefaultOwner();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $owner ? $owner : $this->getDefaultOwner() could return the type string which is incompatible with the type-hinted return integer|null. Consider adding an additional type-check to rule them out.
Loading history...
228
	}
229
230
	/**
231
	 * Get automatic selected users.
232
	 *
233
	 * @return array
234
	 */
235
	public function getOwners(): array
236
	{
237
		switch ($this->get('method')) {
238
			case self::METHOD_LOAD_BALANCE:
239
				$owner = $this->getQueryByLoadBalance()->all();
240
				break;
241
			case self::METHOD_ROUND_ROBIN:
242
				$owner = $this->getQueryByRoundRobin()->all();
243
				break;
244
			default:
245
				$owner = [];
246
				break;
247
		}
248
249
		return $owner;
250
	}
251
252
	/**
253
	 * Get default owner.
254
	 *
255
	 * @return int
256
	 */
257
	public function getDefaultOwner(): ?int
258
	{
259
		$owner = null;
260
		$defaultOwner = (int) $this->get('default_assign');
261
		$ownerModel = Fields\Owner::getInstance($this->getModuleName());
262
263
		$type = $defaultOwner ? Fields\Owner::getType($defaultOwner) : null;
264
		if ('Users' === $type) {
0 ignored issues
show
The condition 'Users' === $type is always false.
Loading history...
265
			$owner = User::isExists($defaultOwner) ? $defaultOwner : $owner;
266
		} elseif ($type) {
267
			$owner = \array_key_exists($defaultOwner, $ownerModel->getAccessibleGroupForModule()) ? $defaultOwner : $owner;
268
		}
269
270
		return $owner;
271
	}
272
273
	/**
274
	 * Query object for users allowed to be assigned by load balanced method.
275
	 *
276
	 * In order to correctly balance the entries attribution
277
	 * we need ot randomize the order in which they are returned.
278
	 * Otherwise, when multiple users have the same amount of entries
279
	 * it is always the first one in the results who will be assigned to new entry.
280
	 *
281
	 * @return Db\Query
282
	 */
283
	public function getQueryByLoadBalance(): Db\Query
284
	{
285
		return $this->getQuery()->orderBy(['count' => SORT_ASC, new \yii\db\Expression('RAND()')]);
286
	}
287
288
	/**
289
	 * Query object for users allowed to be assigned by round robin.
290
	 *
291
	 * @return Db\Query
292
	 */
293
	public function getQueryByRoundRobin(): Db\Query
294
	{
295
		$robinTable = self::ROUND_ROBIN_TABLE;
296
		$columnName = "{$robinTable}.datetime";
297
		$id = $this->getId();
298
299
		return $this->getQuery()->leftJoin($robinTable, "vtiger_users.id = {$robinTable}.user AND {$robinTable}.id={$id}")
300
			->addSelect([$columnName])
301
			->addGroupBy($columnName)
302
			->orderBy([$columnName => SORT_ASC]);
303
	}
304
305
	/**
306
	 * Query object for users allowed for assignment.
307
	 *
308
	 * @return Db\Query
309
	 */
310
	public function getQuery(): Db\Query
311
	{
312
		$ownerFieldName = 'assigned_user_id';
313
		$queryGeneratorUsers = $this->getAvailableUsersQuery();
314
315
		$queryGenerator = (new QueryGenerator($this->getModuleName()));
316
		$queryGenerator->permissions = false;
317
		$conditions = Json::isEmpty($this->get('record_limit_conditions')) ? [] : Json::decode($this->get('record_limit_conditions'));
318
		$queryGenerator->setFields([$ownerFieldName])
319
			->setCustomColumn(['count' => new \yii\db\Expression('COUNT(*)')])
320
			->setConditions($conditions)
321
			->setGroup($ownerFieldName)
322
			->addNativeCondition([$queryGenerator->getColumnName($ownerFieldName) => $queryGeneratorUsers->createQuery()]);
323
		$subQuery = $queryGenerator->createQuery();
324
325
		$recordLimit = (int) $this->get('record_limit');
326
		if (0 === $recordLimit) {
327
			$queryGeneratorUsers->setCustomColumn(['temp_limit' => $queryGeneratorUsers->getColumnName('records_limit')]);
328
		} else {
329
			$queryGeneratorUsers->setCustomColumn(['temp_limit' => new \yii\db\Expression($recordLimit)]);
330
		}
331
		$queryGeneratorUsers->setGroup('id')->setCustomGroup(['temp_limit', 'count']);
332
		$query = $queryGeneratorUsers->createQuery(true);
333
		$query->leftJoin(['crm_data_temp_table' => $subQuery], "crm_data_temp_table.{$ownerFieldName}={$queryGeneratorUsers->getColumnName('id')}");
334
		$query->addSelect(['crm_data_temp_table.count']);
335
		$query->andHaving(['or', ['<', 'count', new \yii\db\Expression('temp_limit')], ['temp_limit' => 0], ['count' => null]]);
336
337
		return $query;
338
	}
339
340
	/**
341
	 * Query generator object of available users.
342
	 *
343
	 * @return QueryGenerator
344
	 */
345
	public function getAvailableUsersQuery(): QueryGenerator
346
	{
347
		$queryGenerator = (new QueryGenerator('Users'))
348
			->setFields(['id'])
349
			->addCondition('status', 'Active', 'e')
350
			->addCondition('available', 1, 'e')
351
			->addCondition('auto_assign', 1, 'e');
352
		$columnName = $queryGenerator->getColumnName('id');
353
354
		$condition = ['or'];
355
		foreach ($this->getMembers() as $member) {
356
			[$type, $id] = explode(':', $member);
357
			switch ($type) {
358
				case PrivilegeUtil::MEMBER_TYPE_USERS:
359
					$condition[$type][$columnName][] = (int) $id;
360
					break;
361
				case PrivilegeUtil::MEMBER_TYPE_GROUPS:
362
					$condition[] = [$columnName => (new Db\Query())->select(['userid'])->from(["condition_{$type}_{$id}_" . Layout::getUniqueId() => PrivilegeUtil::getQueryToUsersByGroup((int) $id)])];
363
					break;
364
				case PrivilegeUtil::MEMBER_TYPE_ROLES:
365
					$condition[] = [$columnName => PrivilegeUtil::getQueryToUsersByRole($id)];
366
					break;
367
				case PrivilegeUtil::MEMBER_TYPE_ROLE_AND_SUBORDINATES:
368
					$condition[] = [$columnName => PrivilegeUtil::getQueryToUsersByRoleAndSubordinate($id)];
369
					break;
370
				default:
371
					break;
372
			}
373
		}
374
		if (1 === \count($condition)) {
375
			$condition = [$columnName => 0];
376
		}
377
		$queryGenerator->addNativeCondition($condition);
378
379
		return $queryGenerator;
380
	}
381
382
	/**
383
	 * Get members.
384
	 *
385
	 * @return array
386
	 */
387
	public function getMembers(): array
388
	{
389
		if (!$this->has('members')) {
390
			$queryAll = null;
391
			foreach (self::MEMBERS_TABLES as $tableName => $index) {
392
				$query = (new Db\Query())
393
					->select(['member' => new \yii\db\Expression('CONCAT(type,\':\', member)')])
394
					->from($tableName)
395
					->where(["{$tableName}.{$index}" => $this->getId()]);
396
				if ($queryAll) {
397
					$queryAll->union($query, true);
398
				} else {
399
					$queryAll = $query;
400
				}
401
			}
402
			$members = $queryAll->column();
0 ignored issues
show
The method column() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

402
			/** @scrutinizer ignore-call */ 
403
   $members = $queryAll->column();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
403
			$this->set('members', $members);
404
		}
405
		return $this->get('members');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('members') could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
406
	}
407
408
	/**
409
	 * Post process action.
410
	 *
411
	 * @param int $userId
412
	 *
413
	 * @return void
414
	 */
415
	public function postProcess(int $userId)
416
	{
417
		$dbCommand = Db::getInstance()->createCommand();
418
		if ($userId && self::METHOD_ROUND_ROBIN === (int) $this->get('method')) {
419
			$params = ['id' => $this->getId(), 'user' => $userId];
420
			$isExists = (new Db\Query())->from(self::ROUND_ROBIN_TABLE)->where($params)->exists();
421
			if ($isExists) {
422
				$dbCommand->update(self::ROUND_ROBIN_TABLE, ['datetime' => (new \DateTime())->format('Y-m-d H:i:s.u')], $params)->execute();
423
			} elseif (\App\User::isExists($userId, false)) {
424
				$params['datetime'] = (new \DateTime())->format('Y-m-d H:i:s.u');
425
				$dbCommand->insert(self::ROUND_ROBIN_TABLE, $params)->execute();
426
			}
427
		}
428
	}
429
}
430