Passed
Push — master ( 344c14...9aa841 )
by Christopher
01:46
created

src/FilterBuilder.php (5 issues)

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 yii\base\InvalidParamException;
13
use yii\base\BaseObject;
14
15
/**
16
 * FilterBuilder builds a Filter for search in LDAP.
17
 *
18
 * FilterBuilder is also used by [[Query]] to build Filters.
19
 *
20
 * @author Christopher Mota <[email protected]>
21
 * @since 1.0.0
22
 */
23
class FilterBuilder extends BaseObject
24
{
25
    /**
26
     * @var string the separator between different fragments of a SQL statement.
27
     * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement.
28
     */
29
    public $separator = ' ';
30
31
    /**
32
     * @var array map of query condition to builder methods.
33
     * These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
34
     */
35
    protected $conditionBuilders = [
36
        'NOT'         => 'buildAndCondition',
37
        'AND'         => 'buildAndCondition',
38
        'OR'          => 'buildAndCondition',
39
        'IN'          => 'buildInCondition',
40
        'NOT IN'      => 'buildInCondition',
41
        'LIKE'        => 'buildLikeCondition',
42
        'NOT LIKE'    => 'buildLikeCondition',
43
        'OR LIKE'     => 'buildLikeCondition',
44
        'OR NOT LIKE' => 'buildLikeCondition',
45
    ];
46
47
    /**
48
     * @var array map of operator for builder methods.
49
     */
50
    protected $operator = [
51
        'NOT' => '!',
52
        'AND' => '&',
53
        'OR'  => '|',
54
    ];
55
56
    /**
57
     * Parses the condition specification and generates the corresponding filters.
58
     * @param string|array $condition the condition specification. Please refer to [[Query::where()]]
59
     * on how to specify a condition.
60
     * @return string the generated
61
     */
62
    public function build($condition)
63
    {
64
        if (!is_array($condition)) {
65
            return (string) $condition;
66
        } elseif (empty($condition)) {
67
            return '';
68
        }
69
70
        if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
71
            $operator = strtoupper($condition[0]);
72
            if (isset($this->conditionBuilders[$operator])) {
73
                $method = $this->conditionBuilders[$operator];
74
            } else {
75
                $method = 'buildSimpleCondition';
76
            }
77
            array_shift($condition);
78
            return $this->$method($operator, $condition);
79
        } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
80
            return $this->buildHashCondition($condition);
81
        }
82
    }
83
84
    /**
85
     * Creates a condition based on column-value pairs.
86
     * @param array $condition the condition specification.
87
     * @return string the generated
88
     */
89
    public function buildHashCondition($condition)
90
    {
91
        $parts = [];
92
        foreach ($condition as $attribute => $value) {
93
            if (is_array($value)) {
94
                // IN condition
95
                $parts[] = $this->buildInCondition('IN', [$attribute, $value]);
96
            } elseif ($value === null) {
97
                $parts[] = "(!($attribute=*))";
98
            } elseif ($attribute === 'dn') {
99
                $parts[] = '(' . LdapHelper::getRdnFromDn($value) . ')';
100
            } else {
101
                $parts[] = "($attribute=$value)";
102
            }
103
        }
104
        return count($parts) === 1 ? $parts[0] : '(&' . implode('', $parts) . ')';
105
    }
106
107
    /**
108
     * Connects two or more filters expressions with the `AND`(&) or `OR`(|) operator.
109
     * @param string $operator The operator to use for connecting the given operands
110
     * @param array $operands The filter expressions to connect.
111
     * @return string The generated filter
112
     */
113
    public function buildAndCondition($operator, $operands)
114
    {
115
        if ($operator === 'NOT' && count($operands) !== 1) {
116
            throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
0 ignored issues
show
Deprecated Code introduced by
The class yii\base\InvalidParamException has been deprecated: since 2.0.14. Use [[InvalidArgumentException]] instead. ( Ignorable by Annotation )

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

116
            throw /** @scrutinizer ignore-deprecated */ new InvalidParamException("Operator '$operator' requires exactly one operand.");
Loading history...
117
        }
118
119
        $parts = [];
120
        foreach ($operands as $operand) {
121
            if (is_array($operand)) {
122
                $parts[] = $this->build($operand);
123
            } elseif ($operand !== '') {
124
                $parts[] = $operand;
125
            }
126
        }
127
128
        return empty($parts) ? '' : '(' . $this->operator[$operator] . implode('', $parts) . ')';
129
    }
130
131
    /**
132
     * Creates an filter expressions with the `IN` operator.
133
     * @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
134
     * @param array $operands the first operand is the column name. If it is an array
135
     * a composite IN condition will be generated.
136
     * The second operand is an array of values that column value should be among.
137
     * If it is an empty array the generated expression will be a `false` value if
138
     * operator is `IN` and empty if operator is `NOT IN`.
139
     * @return string the generated SQL expression
140
     * @throws Exception if wrong number of operands have been given.
141
     */
142
    public function buildInCondition($operator, $operands)
143
    {
144
        if (!isset($operands[0], $operands[1])) {
145
            throw new InvalidParamException("Operator '$operator' requires two operands.");
0 ignored issues
show
Deprecated Code introduced by
The class yii\base\InvalidParamException has been deprecated: since 2.0.14. Use [[InvalidArgumentException]] instead. ( Ignorable by Annotation )

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

145
            throw /** @scrutinizer ignore-deprecated */ new InvalidParamException("Operator '$operator' requires two operands.");
Loading history...
146
        }
147
148
        list($attribute, $values) = $operands;
149
        if (!is_array($values)) {
150
            $values = [$values];
151
        }
152
153
        $parts = [];
154
        foreach ($values as $value) {
155
            if (is_array($value)) {
156
                $value = isset($value[$attribute]) ? $value[$attribute] : null;
157
            }
158
            if ($value === null) {
159
                $parts[] = "(!$attribute=*)";
160
            } else {
161
                $parts[] = "($attribute=$value)";
162
            }
163
        }
164
165
        if (empty($parts)) {
166
            return '';
167
        } elseif ($operator === 'NOT IN') {
168
            return '(!' . implode('', $parts) . ')';
169
        }
170
        return count($parts) === 1 ? $parts[0] : '(|' . implode('', $parts) . ')';
171
    }
172
173
    /**
174
     * Creates an LDAP filter expressions with the `LIKE` operator.
175
     * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
176
     * @param array $operands an array of two or three operands
177
     *
178
     * - The first operand is the column name.
179
     * - The second operand is a single value or an array of values that column value
180
     *   should be compared with. If it is an empty array the generated expression will
181
     *   be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
182
     *   is `NOT LIKE` or `OR NOT LIKE`.
183
     * - An optional third operand can also be provided to specify how to escape special characters
184
     *   in the value(s). The operand should be an array of mappings from the special characters to their
185
     *   escaped counterparts. If this operand is not provided, a default escape mapping will be used.
186
     *   You may use `false` or an empty array to indicate the values are already escaped and no escape
187
     *   should be applied. Note that when using an escape mapping (or the third operand is not provided),
188
     *   the values will be automatically enclosed within a pair of percentage characters.
189
     * @return string the generated LDAP filter expression.
190
     * @throws InvalidParamException if wrong number of operands have been given.
191
     */
192
    public function buildLikeCondition($operator, $operands)
193
    {
194
        if (!isset($operands[0], $operands[1])) {
195
            throw new InvalidParamException("Operator '$operator' requires two operands.");
0 ignored issues
show
Deprecated Code introduced by
The class yii\base\InvalidParamException has been deprecated: since 2.0.14. Use [[InvalidArgumentException]] instead. ( Ignorable by Annotation )

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

195
            throw /** @scrutinizer ignore-deprecated */ new InvalidParamException("Operator '$operator' requires two operands.");
Loading history...
196
        }
197
198
        unset($operands[2]);
199
200
        if (!preg_match('/^(OR|)(((NOT|))LIKE)/', $operator, $matches)) {
201
            throw new InvalidParamException("Invalid operator '$operator'.");
0 ignored issues
show
Deprecated Code introduced by
The class yii\base\InvalidParamException has been deprecated: since 2.0.14. Use [[InvalidArgumentException]] instead. ( Ignorable by Annotation )

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

201
            throw /** @scrutinizer ignore-deprecated */ new InvalidParamException("Invalid operator '$operator'.");
Loading history...
202
        }
203
        $andor    = (!empty($matches[1]) ? $matches[1] : 'AND');
204
        $not      = !empty($matches[3]);
205
206
        list($attribute, $values) = $operands;
207
        if (!is_array($values)) {
208
            $values = [$values];
209
        }
210
        if (empty($values)) {
211
            return '';
212
        }
213
214
        $parts = [];
215
        foreach ($values as $value) {
216
            $parts[] = ($not ? '(!' : '') . '(' . $attribute . '=*' . $value . '*)' . ($not ? ')' : '');
217
        }
218
219
        return '(' . $this->operator[$andor] . implode($parts) . ')';
220
    }
221
222
    /**
223
     * Creates an LDAP filter expressions like `(attribute operator value)`.
224
     * @param string $operator the operator to use. A valid list could be used e.g. `=`, `>=`, `<=`, `~<`.
225
     * @param array $operands contains two column names.
226
     * @return string the generated LDAP filter expression.
227
     * @throws InvalidParamException if wrong number of operands have been given.
228
     */
229
    public function buildSimpleCondition($operator, $operands)
230
    {
231
        if (count($operands) !== 2) {
232
            throw new InvalidParamException("Operator '$operator' requires two operands.");
0 ignored issues
show
Deprecated Code introduced by
The class yii\base\InvalidParamException has been deprecated: since 2.0.14. Use [[InvalidArgumentException]] instead. ( Ignorable by Annotation )

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

232
            throw /** @scrutinizer ignore-deprecated */ new InvalidParamException("Operator '$operator' requires two operands.");
Loading history...
233
        }
234
235
        list($attribute, $value) = $operands;
236
237
        if ($value === null) {
238
            return "(!($attribute = *))";
239
        } else {
240
            return "($attribute $operator $value)";
241
        }
242
    }
243
244
}
245