Passed
Push — master ( c30585...90dc34 )
by Christopher
02:43
created

FilterBuilder::buildAndCondition()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 8
nop 2
dl 0
loc 12
rs 9.6111
c 0
b 0
f 0
1
<?php
2
/**
3
 * @link      https://github.com/chrmorandi/yii2-ldap for the source repository
4
 * @package   yii2-ldap
5
 * @author    Christopher Mota <[email protected]>
6
 * @license   MIT License - view the LICENSE file that was distributed with this source code.
7
 * @since     1.0.0
8
 */
9
10
namespace chrmorandi\ldap;
11
12
use Traversable;
13
use yii\base\InvalidParamException;
14
use yii\base\BaseObject;
0 ignored issues
show
Bug introduced by
The type yii\base\BaseObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use yii\helpers\ArrayHelper;
16
17
/**
18
 * FilterBuilder builds a Filter for search in LDAP.
19
 *
20
 * FilterBuilder is also used by [[Query]] to build Filters.
21
 *
22
 * @author Christopher Mota <[email protected]>
23
 * @since 1.0.0
24
 */
25
class FilterBuilder extends BaseObject
26
{
27
    /**
28
     * @var string the separator between different fragments of a SQL statement.
29
     * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement.
30
     */
31
    public $separator = ' ';
32
33
    /**
34
     * @var array map of query condition to builder methods.
35
     * These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
36
     */
37
    protected $conditionBuilders = [
38
        'NOT'         => 'buildAndCondition',
39
        'AND'         => 'buildAndCondition',
40
        'OR'          => 'buildAndCondition',
41
        'IN'          => 'buildInCondition',
42
        'NOT IN'      => 'buildInCondition',
43
        'LIKE'        => 'buildLikeCondition',
44
        'NOT LIKE'    => 'buildLikeCondition',
45
        'OR LIKE'     => 'buildLikeCondition',
46
        'OR NOT LIKE' => 'buildLikeCondition',
47
    ];
48
49
    /**
50
     * @var array map of operator for builder methods.
51
     */
52
    protected $operator = [
53
        'NOT'  => '!',
54
        'AND'  => '&',
55
        'OR'   => '|',
56
    ];
57
58
    /**
59
     * Parses the condition specification and generates the corresponding filters.
60
     * @param string|array $condition the condition specification. Please refer to [[Query::where()]]
61
     * on how to specify a condition.
62
     * @return string the generated
63
     */
64
    public function build($condition)
65
    {
66
        if (!is_array($condition)) {
67
            return (string) $condition;
68
        } elseif (empty($condition)) {
69
            return '';
70
        }
71
72
        if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
73
            $operator = strtoupper($condition[0]);
74
            if (isset($this->conditionBuilders[$operator])) {
75
                $method = $this->conditionBuilders[$operator];
76
            } else {
77
                $method = 'buildSimpleCondition';
78
            }
79
            array_shift($condition);
80
            return $this->$method($operator, $condition);
81
        } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
82
            return $this->buildHashCondition($condition);
83
        }
84
    }
85
86
    /**
87
     * Creates a condition based on column-value pairs.
88
     * @param array $condition the condition specification.
89
     * @return string the generated
90
     */
91
    public function buildHashCondition($condition)
92
    {
93
        $parts = [];
94
        foreach ($condition as $attribute => $value) {
95
            if (ArrayHelper::isTraversable($value)) {
96
                // IN condition
97
                $parts[] = $this->buildInCondition('IN', [$attribute, $value]);
98
            } elseif ($value === null) {
99
                $parts[] = "!$attribute=*";
100
            } elseif ($attribute === 'dn') {
101
                $parts[] = LdapHelper::getRdnFromDn($value);
102
            } else {
103
                $parts[] = "$attribute=$value";
104
            }
105
        }
106
        return count($parts) === 1 ? '(' . $parts[0] . ')' : '&(' . implode(') (', $parts) . ')';
107
    }
108
109
    /**
110
     * Connects two or more filters expressions with the `AND`(&) or `OR`(|) operator.
111
     * @param string $operator The operator to use for connecting the given operands
112
     * @param array $operands The filter expressions to connect.
113
     * @return string The generated filter
114
     */
115
    public function buildAndCondition($operator, $operands)
116
    {
117
        $parts = [];
118
        foreach ($operands as $operand) {
119
            if (is_array($operand)) {
120
                $parts[] = $this->build($operand);
121
            } elseif ($operand !== '') {
122
                $parts[] = $operand;
123
            }
124
        }
125
126
        return empty($parts) ? '' : '(' . $this->operator[$operator] . '(' . implode(') (', $parts) . '))';
127
    }
128
129
    /**
130
     * Creates an filter expressions with the `IN` operator.
131
     * @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
132
     * @param array $operands the first operand is the column name. If it is an array
133
     * a composite IN condition will be generated.
134
     * The second operand is an array of values that column value should be among.
135
     * If it is an empty array the generated expression will be a `false` value if
136
     * operator is `IN` and empty if operator is `NOT IN`.
137
     * @return string the generated SQL expression
138
     * @throws Exception if wrong number of operands have been given.
139
     */
140
    public function buildInCondition($operator, $operands)
141
    {
142
        if (!isset($operands[0], $operands[1])) {
143
            throw new InvalidParamException("Operator '$operator' requires two operands.");
144
        }
145
146
        list($attribute, $values) = $operands;
147
148
        if (is_string($attribute) || !is_array($values)) {
149
            throw new InvalidParamException('First operand must to be string and secund operand must to be array.');
150
        }
151
152
        $parts = [];
153
        foreach ($values as $value) {
154
            if (is_array($value)) {
155
                $value = isset($value[$attribute]) ? $value[$attribute] : null;
156
            }
157
            if ($value === null) {
158
                $parts[] = "!$attribute=*";
159
            } else {
160
                $parts[] = "$attribute=$value";
161
            }
162
        }
163
164
        if (empty($parts)) {
165
            return '';
166
        } elseif ($operator === 'NOT IN') {
167
            return '!(' . implode(') (', $parts) . ')';
168
        }
169
        return count($parts) === 1 ? '(' . $parts[0] . ')' : '|(' . implode(') (', $parts) . ')';
170
    }
171
172
    /**
173
     * Creates an SQL expressions with the `LIKE` operator.
174
     * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
175
     * @param array $operands an array of two or three operands
176
     *
177
     * - The first operand is the column name.
178
     * - The second operand is a single value or an array of values that column value
179
     *   should be compared with. If it is an empty array the generated expression will
180
     *   be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
181
     *   is `NOT LIKE` or `OR NOT LIKE`.
182
     * - An optional third operand can also be provided to specify how to escape special characters
183
     *   in the value(s). The operand should be an array of mappings from the special characters to their
184
     *   escaped counterparts. If this operand is not provided, a default escape mapping will be used.
185
     *   You may use `false` or an empty array to indicate the values are already escaped and no escape
186
     *   should be applied. Note that when using an escape mapping (or the third operand is not provided),
187
     *   the values will be automatically enclosed within a pair of percentage characters.
188
     * @return string the generated SQL expression
189
     * @throws InvalidParamException if wrong number of operands have been given.
190
     */
191
    public function buildLikeCondition($operator, $operands)
192
    {
193
        if (!isset($operands[0], $operands[1])) {
194
            throw new InvalidParamException("Operator '$operator' requires two operands.");
195
        }
196
197
        $escape = isset($operands[2]) ? $operands[2] : ['*' => '\*', '_' => '\_', '\\' => '\\\\'];
198
        unset($operands[2]);
199
200
        if (!preg_match('/^(OR|)(((NOT|))LIKE)/', $operator, $matches)) {
201
            throw new InvalidParamException("Invalid operator '$operator'.");
202
        }
203
        $andor    = (!empty($matches[1]) ? $matches[1] : 'AND');
204
        $not      = !empty($matches[3]);
0 ignored issues
show
Unused Code introduced by
The assignment to $not is dead and can be removed.
Loading history...
205
        $operator = $matches[2];
206
207
        list($attribute, $values) = $operands;
208
209
        if (!is_array($values)) {
210
            $values = [$values];
211
        }
212
213
        if (empty($values)) {
214
            return '';
215
        }
216
217
        $not = ($operator == 'NOT LIKE') ? '(' . $this->operator['NOT'] : false;
218
219
        $parts = [];
220
        foreach ($values as $value) {
221
            $value   = empty($escape) ? $value : strtr($value, $escape);
222
            $parts[] = $not . '(' . $attribute . '=*' . $value . '*)' . ($not ? ')' : '');
0 ignored issues
show
Bug introduced by
Are you sure $not of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

222
            $parts[] = /** @scrutinizer ignore-type */ $not . '(' . $attribute . '=*' . $value . '*)' . ($not ? ')' : '');
Loading history...
223
        }
224
225
        return '(' . $this->operator[trim($andor)] . implode($parts) . ')';
226
    }
227
228
    /**
229
     * Creates an LDAP filter expressions like `(attribute operator value)`.
230
     * @param string $operator the operator to use. A valid list could be used e.g. `=`, `>=`, `<=`, `~<`.
231
     * @param array $operands contains two column names.
232
     * @return string the generated LDAP filter expression
233
     * @throws InvalidParamException if wrong number of operands have been given.
234
     */
235
    public function buildSimpleCondition($operator, $operands)
236
    {
237
        if (count($operands) !== 2) {
238
            throw new InvalidParamException("Operator '$operator' requires two operands.");
239
        }
240
241
        list($attribute, $value) = $operands;
242
243
        if ($value === null) {
244
            return "(!$attribute = *)";
245
        } else {
246
            return "($attribute $operator $value)";
247
        }
248
    }
249
250
}
251