Passed
Push — developer ( 762b64...a1cff0 )
by Radosław
20:19
created

Condition::getOperatorLabels()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 14
ccs 0
cts 10
cp 0
rs 9.9666
c 0
b 0
f 0
cc 4
nc 4
nop 1
crap 20
1
<?php
2
3
/**
4
 * Condition main class.
5
 *
6
 * @package App
7
 *
8
 * @copyright YetiForce S.A.
9
 * @license   YetiForce Public License 5.0 (licenses/LicenseEN.txt or yetiforce.com)
10
 * @author    Tomasz Kur <[email protected]>
11
 * @author    Mariusz Krzaczkowski <[email protected]>
12
 * @author    Radosław Skrzypczak <[email protected]>
13
 */
14
15
namespace App;
16
17
/**
18
 * Condition main class.
19
 */
20
class Condition
21
{
22
	/**
23
	 * @var array Data filter list.
24
	 */
25
	const DATE_OPERATORS = [
26
		'smallerthannow' => ['label' => 'LBL_SMALLER_THAN_NOW'],
27
		'greaterthannow' => ['label' => 'LBL_GREATER_THAN_NOW'],
28
		'prevfy' => ['label' => 'LBL_PREVIOUS_FY'],
29
		'thisfy' => ['label' => 'LBL_CURRENT_FY'],
30
		'nextfy' => ['label' => 'LBL_NEXT_FY'],
31
		'prevfq' => ['label' => 'LBL_PREVIOUS_FQ'],
32
		'thisfq' => ['label' => 'LBL_CURRENT_FQ'],
33
		'nextfq' => ['label' => 'LBL_NEXT_FQ'],
34
		'previousworkingday' => ['label' => 'LBL_PREVIOUS_WORKING_DAY'],
35
		'nextworkingday' => ['label' => 'LBL_NEXT_WORKING_DAY'],
36
		'yesterday' => ['label' => 'LBL_YESTERDAY'],
37
		'today' => ['label' => 'LBL_TODAY'],
38
		'untiltoday' => ['label' => 'LBL_UNTIL_TODAY'],
39
		'tomorrow' => ['label' => 'LBL_TOMORROW'],
40
		'lastweek' => ['label' => 'LBL_LAST_WEEK'],
41
		'thisweek' => ['label' => 'LBL_CURRENT_WEEK'],
42
		'nextweek' => ['label' => 'LBL_NEXT_WEEK'],
43
		'lastmonth' => ['label' => 'LBL_LAST_MONTH'],
44
		'thismonth' => ['label' => 'LBL_CURRENT_MONTH'],
45
		'nextmonth' => ['label' => 'LBL_NEXT_MONTH'],
46
		'last7days' => ['label' => 'LBL_LAST_7_DAYS'],
47
		'last15days' => ['label' => 'LBL_LAST_15_DAYS'],
48
		'last30days' => ['label' => 'LBL_LAST_30_DAYS'],
49
		'last60days' => ['label' => 'LBL_LAST_60_DAYS'],
50
		'last90days' => ['label' => 'LBL_LAST_90_DAYS'],
51
		'last120days' => ['label' => 'LBL_LAST_120_DAYS'],
52
		'next15days' => ['label' => 'LBL_NEXT_15_DAYS'],
53
		'next30days' => ['label' => 'LBL_NEXT_30_DAYS'],
54
		'next60days' => ['label' => 'LBL_NEXT_60_DAYS'],
55
		'next90days' => ['label' => 'LBL_NEXT_90_DAYS'],
56
		'next120days' => ['label' => 'LBL_NEXT_120_DAYS'],
57
		'moreThanDaysAgo' => ['label' => 'LBL_DATE_CONDITION_MORE_THAN_DAYS_AGO'],
58
	];
59
60
	/**
61
	 * @var string[] List of field comparison operators
62
	 */
63
	const FIELD_COMPARISON_OPERATORS = ['ef', 'nf', 'lf', 'gf', 'mf', 'hf'];
64
65
	/**
66
	 * @var string[] Supported advanced filter operations.
67
	 */
68
	const STANDARD_OPERATORS = [
69
		'e' => 'LBL_EQUALS',
70
		'n' => 'LBL_NOT_EQUAL_TO',
71
		's' => 'LBL_STARTS_WITH',
72
		'ew' => 'LBL_ENDS_WITH',
73
		'c' => 'LBL_CONTAINS',
74
		'ch' => 'LBL_CONTAINS_HIERARCHY',
75
		'k' => 'LBL_DOES_NOT_CONTAIN',
76
		'kh' => 'LBL_DOES_NOT_CONTAIN_HIERARCHY',
77
		'l' => 'LBL_LESS_THAN',
78
		'g' => 'LBL_GREATER_THAN',
79
		'm' => 'LBL_LESS_THAN_OR_EQUAL',
80
		'h' => 'LBL_GREATER_OR_EQUAL',
81
		'b' => 'LBL_BEFORE',
82
		'a' => 'LBL_AFTER',
83
		'bw' => 'LBL_BETWEEN',
84
		'y' => 'LBL_IS_EMPTY',
85
		'ny' => 'LBL_IS_NOT_EMPTY',
86
		'om' => 'LBL_CURRENTLY_LOGGED_USER',
87
		'nom' => 'LBL_USER_CURRENTLY_NOT_LOGGED',
88
		'ogr' => 'LBL_CURRENTLY_LOGGED_USER_GROUP',
89
		'ogu' => 'LBL_USERS_GROUP_LOGGED_IN_USER',
90
		'wr' => 'LBL_IS_WATCHING_RECORD',
91
		'nwr' => 'LBL_IS_NOT_WATCHING_RECORD',
92
		'hs' => 'LBL_HAS_CHANGED',
93
		'hst' => 'LBL_HAS_CHANGED_TO',
94
		'ro' => 'LBL_IS_RECORD_OPEN',
95
		'rc' => 'LBL_IS_RECORD_CLOSED',
96
		'nco' => 'LBL_NOT_CREATED_BY_OWNER',
97
		'ef' => 'LBL_EQUALS_FIELD',
98
		'nf' => 'LBL_NOT_EQUAL_TO_FIELD',
99
		'lf' => 'LBL_LESS_THAN_FIELD',
100
		'gf' => 'LBL_GREATER_THAN_FIELD',
101
		'mf' => 'LBL_LESS_THAN_OR_EQUAL_FIELD',
102
		'hf' => 'LBL_GREATER_OR_EQUAL_FIELD',
103
	];
104
105
	/**
106
	 * @var string[] Operators without values.
107
	 */
108
	const OPERATORS_WITHOUT_VALUES = [
109
		'y', 'ny', 'om', 'nom', 'ogr', 'wr', 'nwr', 'hs', 'ro', 'rc', 'nco', 'ogu',
110
		'smallerthannow',
111
		'greaterthannow',
112
		'prevfy',
113
		'thisfy',
114
		'nextfy',
115
		'prevfq',
116
		'thisfq',
117
		'yesterday',
118
		'today',
119
		'untiltoday',
120
		'tomorrow',
121
		'lastweek',
122
		'thisweek',
123
		'nextweek',
124
		'lastmonth',
125
		'thismonth',
126
		'nextmonth',
127
		'last7days',
128
		'last15days',
129
		'last30days',
130
		'last60days',
131
		'last90days',
132
		'last120days',
133
		'next15days',
134
		'next30days',
135
		'next60days',
136
		'next90days',
137
		'next120days',
138
		'previousworkingday',
139
		'nextworkingday',
140
	];
141
142
	/**
143
	 * Vtiger_Record_Model instance cache.
144
	 *
145
	 * @var Vtiger_Record_Model[]
146
	 */
147
	private static $recordCache = [];
148
149
	/**
150
	 * Gets operator labels.
151
	 *
152
	 * @param array $operators
153
	 *
154
	 * @return string[]
155
	 */
156
	public static function getOperatorLabels(array $operators): array
157
	{
158
		$labels = [];
159
		foreach ($operators as $operator) {
160
			$label = '';
161
			if (isset(self::STANDARD_OPERATORS[$operator])) {
162
				$label = self::STANDARD_OPERATORS[$operator];
163
			} elseif (isset(self::DATE_OPERATORS[$operator])) {
164
				$label = self::DATE_OPERATORS[$operator]['label'];
165
			}
166
			$labels[$operator] = $label;
167
		}
168
169
		return $labels;
170
	}
171
172
	/**
173
	 * Checks structure search_params.
174
	 *
175
	 * @param string $moduleName
176
	 * @param array  $searchParams
177
	 * @param bool   $convert
178
	 *
179
	 * @throws \App\Exceptions\IllegalValue
180
	 *
181
	 * @return array
182
	 */
183
	public static function validSearchParams(string $moduleName, array $searchParams, $convert = true): array
184
	{
185
		$searchParamsCount = \count($searchParams);
186
		if ($searchParamsCount > 2) {
187
			throw new Exceptions\IllegalValue("ERR_NUMBER_OF_ARGUMENTS_NOT_ALLOWED||{$searchParamsCount}|| > 2||" . Utils::varExport($searchParams, true), 406);
0 ignored issues
show
Unused Code introduced by
The call to App\Utils::varExport() has too many arguments starting with true. ( Ignorable by Annotation )

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

187
			throw new Exceptions\IllegalValue("ERR_NUMBER_OF_ARGUMENTS_NOT_ALLOWED||{$searchParamsCount}|| > 2||" . Utils::/** @scrutinizer ignore-call */ varExport($searchParams, true), 406);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
188
		}
189
		$fields = \Vtiger_Module_Model::getInstance($moduleName)->getFields();
190
		$result = [];
191
		foreach ($searchParams as $params) {
192
			$tempParam = [];
193
			foreach ($params as $param) {
194
				if (empty($param)) {
195
					continue;
196
				}
197
				$count = \count($param);
198
				if (3 !== $count && 4 !== $count) {
199
					throw new Exceptions\IllegalValue("ERR_NUMBER_OF_ARGUMENTS_NOT_ALLOWED||{$count}|| <> 3 or 4||" . Utils::varExport($param, true), 406);
200
				}
201
				[$relatedFieldName, $relatedModule, $referenceField] = array_pad(explode(':', $param[0]), 3, null);
202
				if ($relatedModule) {
203
					$relatedFields = \Vtiger_Module_Model::getInstance($relatedModule)->getFields();
204
					if (!isset($fields[$referenceField], $relatedFields[$relatedFieldName])) {
205
						throw new Exceptions\IllegalValue("ERR_FIELD_NOT_FOUND||{$param[0]}||" . Utils::varExport($param, true), 406);
206
					}
207
					$value = $relatedFields[$relatedFieldName]->getUITypeModel()->getDbConditionBuilderValue($param[2], $param[1]);
208
				} elseif (0 === strpos($param[0], 'relationColumn_') && preg_match('/(^relationColumn_)(\d+)$/', $param[0])) {
209
					$value = (int) $param[2];
210
				} else {
211
					if (!isset($fields[$param[0]])) {
212
						throw new Exceptions\IllegalValue("ERR_FIELD_NOT_FOUND||{$param[0]}||" . Utils::varExport($param, true), 406);
213
					}
214
					$value = $fields[$param[0]]->getUITypeModel()->getDbConditionBuilderValue($param[2], $param[1]);
215
				}
216
				if ($convert) {
217
					$param[2] = $value;
218
				}
219
				$tempParam[] = $param;
220
			}
221
			$result[] = $tempParam;
222
		}
223
		return $result;
224
	}
225
226
	/**
227
	 * Checks value search_value.
228
	 *
229
	 * @param string $value
230
	 * @param string $moduleName
231
	 * @param string $fieldName
232
	 * @param string $operator
233
	 *
234
	 * @return string
235
	 */
236
	public static function validSearchValue(string $value, string $moduleName, string $fieldName, string $operator): string
237
	{
238
		if ('' !== $value) {
239
			\Vtiger_Module_Model::getInstance($moduleName)->getField($fieldName)->getUITypeModel()->getDbConditionBuilderValue($value, $operator);
240
		}
241
		return $value;
242
	}
243
244
	/**
245
	 * Return condition from request.
246
	 *
247
	 * @param array $conditions
248
	 *
249
	 * @return array
250
	 */
251
	public static function getConditionsFromRequest(array $conditions): array
252
	{
253
		if (isset($conditions['rules'])) {
254
			foreach ($conditions['rules'] as &$condition) {
255
				if (isset($condition['condition'])) {
256
					$condition = static::getConditionsFromRequest($condition);
257
				} else {
258
					$operator = $condition['operator'];
259
					$value = $condition['value'] ?? '';
260
					if (!\in_array($operator, array_merge(self::OPERATORS_WITHOUT_VALUES, self::FIELD_COMPARISON_OPERATORS, array_keys(self::DATE_OPERATORS)))) {
261
						[$fieldName, $fieldModuleName,] = array_pad(explode(':', $condition['fieldname']), 3, false);
262
						$value = \Vtiger_Module_Model::getInstance($fieldModuleName)->getFieldByName($fieldName)
263
							->getUITypeModel()
264
							->getDbConditionBuilderValue($value, $operator);
265
					}
266
					$condition['value'] = $value;
267
				}
268
			}
269
		}
270
		return $conditions;
271
	}
272
273
	/**
274
	 * Check all conditions.
275
	 *
276
	 * @param array                $conditions
277
	 * @param \Vtiger_Record_Model $recordModel
278
	 *
279
	 * @return bool
280
	 */
281
	public static function checkConditions(array $conditions, \Vtiger_Record_Model $recordModel): bool
282
	{
283
		return self::parseConditions($conditions, $recordModel);
284
	}
285
286
	/**
287
	 * Parse conditions.
288
	 *
289
	 * @param array|null           $conditions
290
	 * @param \Vtiger_Record_Model $recordModel
291
	 *
292
	 * @return bool
293
	 */
294
	private static function parseConditions(?array $conditions, \Vtiger_Record_Model $recordModel): bool
295
	{
296
		if (empty($conditions) || empty($conditions['rules'])) {
297
			return true;
298
		}
299
		$result = false;
300
		$andCondition = 'AND' === $conditions['condition'];
301
		foreach ($conditions['rules'] as $rule) {
302
			if (isset($rule['condition'])) {
303
				$ruleResult = self::parseConditions($rule, $recordModel);
304
			} else {
305
				$ruleResult = self::checkCondition($rule, $recordModel);
306
			}
307
			if (!$andCondition && $ruleResult) {
308
				$result = true;
309
				break;
310
			}
311
			if ($andCondition && !$ruleResult) {
312
				$result = false;
313
				break;
314
			}
315
			$result = $ruleResult;
316
		}
317
		return $result;
318
	}
319
320
	/**
321
	 * Check one condition.
322
	 *
323
	 * @param array                $rule
324
	 * @param \Vtiger_Record_Model $recordModel
325
	 *
326
	 * @return bool
327
	 */
328
	public static function checkCondition(array $rule, \Vtiger_Record_Model $recordModel): bool
329
	{
330
		[$fieldName, $moduleName, $sourceFieldName] = array_pad(explode(':', $rule['fieldname']), 3, false);
331
		if ($sourceFieldName) {
332
			if ($recordModel->isEmpty($sourceFieldName)) {
333
				return false;
334
			}
335
			$sourceRecordModel = self::$recordCache[$recordModel->get($sourceFieldName)] ?? \Vtiger_Record_Model::getInstanceById($recordModel->get($sourceFieldName));
336
			$fieldModel = $sourceRecordModel->getField($fieldName);
337
		} elseif ($recordModel->getModuleName() === $moduleName) {
338
			$fieldModel = $recordModel->getField($fieldName);
339
		}
340
		if (empty($fieldModel)) {
341
			Log::error("Not found field model | Field name: $fieldName | Module: $moduleName", 'Condition');
342
			throw new \App\Exceptions\AppException('ERR_NOT_FOUND_FIELD_MODEL|' . $fieldName);
343
		}
344
		$className = '\App\Conditions\RecordFields\\' . ucfirst($fieldModel->getFieldDataType()) . 'Field';
345
		if (!class_exists($className)) {
346
			Log::error("Not found record field condition | Field name: $fieldName | Module: $moduleName | FieldDataType: " . ucfirst($fieldModel->getFieldDataType()), 'Condition');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fieldModel does not seem to be defined for all execution paths leading up to this point.
Loading history...
347
			throw new \App\Exceptions\AppException("ERR_NOT_FOUND_QUERY_FIELD_CONDITION|$fieldName|$className");
348
		}
349
		if (!empty($sourceFieldName)) {
350
			$recordField = new $className($sourceRecordModel, $fieldModel, $rule);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sourceRecordModel does not seem to be defined for all execution paths leading up to this point.
Loading history...
351
			$recordField->setSource($recordModel, $sourceFieldName);
352
		} else {
353
			$recordField = new $className($recordModel, $fieldModel, $rule);
354
		}
355
		return $recordField->check();
356
	}
357
358
	/**
359
	 * Get field names from conditions.
360
	 *
361
	 * @param array $conditions
362
	 *
363
	 * @return array ['baseModule' => [], 'referenceModule' => []]
364
	 */
365
	public static function getFieldsFromConditions(array $conditions): array
366
	{
367
		$fields = ['baseModule' => [], 'referenceModule' => []];
368
		if (isset($conditions['rules'])) {
369
			foreach ($conditions['rules'] as &$condition) {
370
				if (isset($condition['condition'])) {
371
					$fields = array_merge_recursive($fields, static::getFieldsFromConditions($condition));
372
				} else {
373
					[$fieldName, $moduleName, $sourceFieldName] = array_pad(explode(':', $condition['fieldname']), 3, false);
374
					if ($sourceFieldName) {
375
						$fields['referenceModule'][$moduleName][$sourceFieldName] = $fieldName;
376
					} else {
377
						$fields['baseModule'][] = $fieldName;
378
					}
379
				}
380
			}
381
		}
382
		return $fields;
383
	}
384
385
	/**
386
	 * Remove field from conditions.
387
	 *
388
	 * @param string $baseModuleName The base name of the module for which conditions have been set
389
	 * @param array  $conditions
390
	 * @param string $moduleName     The module name of the field to be deleted.
391
	 * @param string $fieldName      The name of the field to be deleted
392
	 *
393
	 * @return array
394
	 */
395
	public static function removeFieldFromCondition(string $baseModuleName, array $conditions, string $moduleName, string $fieldName): array
396
	{
397
		if (isset($conditions['rules'])) {
398
			foreach ($conditions['rules'] as $key => &$condition) {
399
				if (isset($condition['condition'])) {
400
					$condition = static::removeFieldFromCondition($baseModuleName, $condition, $moduleName, $fieldName);
401
				} else {
402
					[$cFieldName, $cModuleName, $sourceFieldName] = array_pad(explode(':', $condition['fieldname']), 3, false);
403
					if (($fieldName === $cFieldName && $moduleName === $cModuleName) || ($sourceFieldName && $sourceFieldName === $fieldName && $moduleName === $baseModuleName)) {
404
						$condition = [];
405
					}
406
				}
407
				if (empty($condition)) {
408
					unset($conditions['rules'][$key]);
409
				}
410
			}
411
			if (empty($conditions['rules'] = array_filter($conditions['rules']))) {
412
				$conditions = [];
413
			}
414
		}
415
		return $conditions;
416
	}
417
418
	/**
419
	 * Checks structure advancedConditions.
420
	 *
421
	 * @param array $advancedConditions
422
	 *
423
	 * @return array
424
	 */
425
	public static function validAdvancedConditions(array $advancedConditions): array
426
	{
427
		if (!empty($advancedConditions['relationConditions']) && 0 != $advancedConditions['relationId']) {
428
			$advancedConditions['relationConditions'] = self::getConditionsFromRequest($advancedConditions['relationConditions']);
429
		}
430
		if (!empty($advancedConditions['relationColumns'])) {
431
			array_map(function ($v) {
432
				if (!\App\Validator::integer($v)) {
433
					throw new \App\Exceptions\IllegalValue('ERR_NOT_ALLOWED_VALUE||' . $v, 406);
434
				}
435
			}, $advancedConditions['relationColumns']);
436
		}
437
		return $advancedConditions;
438
	}
439
}
440