Completed
Push — master ( beefde...920166 )
by Andreas
17:15
created

midcom_core_query::is_readable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 2
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 2
rs 10
1
<?php
2
/**
3
 * @package midcom
4
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
6
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
7
 */
8
9
use Doctrine\ORM\Query\Expr\Composite;
10
use Doctrine\ORM\QueryBuilder;
11
12
/**
13
 * Common base class for collector and querybuilder
14
 *
15
 * @package midcom
16
 */
17
abstract class midcom_core_query
18
{
19
    /**
20
     * Set this element to true to hide all items which are currently invisible according
21
     * to the approval/scheduling settings made using Metadata. This must be set before executing
22
     * the query.
23
     *
24
     * NOTE: Approval checks not implemented in collector yet
25
     *
26
     * Be aware, that this setting will currently not use the QB to filter the objects accordingly,
27
     * since there is no way yet to filter against parameters. This will mean some performance
28
     * impact.
29
     *
30
     * @var boolean
31
     */
32
    public $hide_invisible = true;
33
34
    /**
35
     * Flag that tracks whether deleted visibility check have already been added
36
     *
37
     * @var boolean
38
     */
39
    protected $_visibility_checks_added = false;
40
41
    /**
42
     * The number of records to return to the client at most.
43
     *
44
     * @var int
45
     */
46
    protected $_limit = 0;
47
48
    /**
49
     * The offset of the first record the client wants to have available.
50
     *
51
     * @var int
52
     */
53
    protected $_offset = 0;
54
55
    /**
56
     * The ordering instructions used for this query
57
     *
58
     * @var array
59
     */
60
    protected $_orders = [];
61
62
    /**
63
     * Type that the application expects to retrieve from this instance.
64
     *
65
     * @var string
66
     */
67
    protected $_real_class;
68
69
    /**
70
     * The number of records found by the last execute() run. This is -1 as long as no
71
     * query has been executed. This member is read-only.
72
     *
73
     * @var int
74
     */
75
    protected $count = -1;
76
77
    /**
78
     * The query backend, should be set in constructor. Currently collector or querybuilder
79
     *
80
     * @var \midgard\portable\query
81
     */
82
    protected $_query;
83
84
    /**
85
     * The number of objects for which access was denied.
86
     *
87
     * @var int
88
     */
89
    public $denied = 0;
90
91
    /**
92
     * User id for ACL checks. This is set when executing to avoid unnecessary overhead
93
     *
94
     * @var string
95
     */
96
    private $_user_id;
97
98
    /**
99
     * Class resolution into the MidCOM DBA system.
100
     * Currently, Midgard requires the actual MgdSchema base classes to be used
101
     * when dealing with the query, so we internally note the corresponding class
102
     * information to be able to do correct typecasting later.
103
     *
104
     * @param string $classname The classname which should be converted.
105
     * @return string MgdSchema class name
106
     */
107 461
    protected function _convert_class($classname) : string
108
    {
109 461
        if (!class_exists($classname)) {
110
            throw new midcom_error("Cannot create a midcom_core_query instance for the type {$classname}: Class does not exist.");
111
        }
112
113 461
        static $_class_mapping_cache = [];
114
115 461
        $this->_real_class = $classname;
116 461
        if (!array_key_exists($classname, $_class_mapping_cache)) {
117 30
            if (!is_subclass_of($classname, midcom_core_dbaobject::class)) {
118
                throw new midcom_error(
119
                    "Cannot create a midcom_core_query instance for the type {$classname}: Does not seem to be a DBA class name."
120
                );
121
            }
122
123
            // Figure out the actual MgdSchema class from the decorator
124 30
            $dummy = new $classname();
125 30
            $mgdschemaclass = $dummy->__mgdschema_class_name__;
126 30
            $_class_mapping_cache[$classname] = $mgdschemaclass;
127
        }
128 461
        return $_class_mapping_cache[$classname];
129
    }
130
131 200
    protected function is_readable(string $guid) : bool
132
    {
133 200
        return !$this->_user_id
134 200
            || midcom::get()->auth->acl->can_do_byguid('midgard:read', $guid, $this->_real_class, $this->_user_id);
135
    }
136
137 455
    protected function _add_visibility_checks()
138
    {
139 455
        if (!midcom::get()->auth->admin) {
140 454
            $this->_user_id = midcom::get()->auth->acl->get_user_id();
141
        }
142
143 455
        if (   $this->hide_invisible
144 455
            && !$this->_visibility_checks_added) {
145 455
            if (!midcom::get()->config->get('show_hidden_objects')) {
146
                $this->add_constraint('metadata.hidden', '=', false);
147
                $now = strftime('%Y-%m-%d %H:%M:%S');
148
                $this->add_constraint('metadata.schedulestart', '<=', $now);
149
                $this->begin_group('OR');
150
                    $this->add_constraint('metadata.scheduleend', '>=', $now);
151
                    $this->add_constraint('metadata.scheduleend', '<=', '0001-01-01 00:00:00');
152
                $this->end_group();
153
            }
154
155 455
            if (!midcom::get()->config->get('show_unapproved_objects')) {
156
                $this->add_constraint('metadata.isapproved', '=', true);
157
            }
158 455
            $this->_visibility_checks_added = true;
159
        }
160 455
    }
161
162
    /**
163
     * Resets some internal variables for re-execute
164
     */
165 454
    protected function _reset()
166
    {
167 454
        $this->count = -1;
168 454
        $this->denied = 0;
169 454
    }
170
171 23
    public function get_doctrine() : QueryBuilder
172
    {
173 23
        return $this->_query->get_doctrine();
174
    }
175
176 8
    public function get_current_group() : Composite
177
    {
178 8
        return $this->_query->get_current_group();
179
    }
180
181
    /**
182
     * Add a constraint to the query.
183
     *
184
     * @param string $field The name of the MgdSchema property to query against.
185
     * @param string $operator The operator to use for the constraint, currently supported are
186
     *     <, <=, =, <>, >=, >, LIKE. LIKE uses the percent sign ('%') as a
187
     *     wildcard character.
188
     * @param mixed $value The value to compare against. It should be of the same type as the
189
     *     queried property.
190
     * @return boolean Indicating success.
191
     */
192 452
    public function add_constraint(string $field, string $operator, $value) : bool
193
    {
194 452
        $this->_reset();
195
        // Add check against null values, Core MC is too stupid to get this right.
196 452
        if ($value === null) {
197
            debug_add("Query: Cannot add constraint on field '{$field}' with null value.", MIDCOM_LOG_WARN);
198
            return false;
199
        }
200
        // Deal with empty arrays, which would produce invalid queries
201
        // This is done here to avoid repetitive code in callers, and because
202
        // it's easy enough to generalize: IN empty set => always false, NOT IN empty set => always true
203 452
        if (   is_array($value)
204 452
            && empty($value)) {
205 2
            if ($operator == 'NOT IN') {
206 1
                return true;
207
            }
208 1
            if ($operator == 'IN') {
209 1
                return $this->add_constraint('id', '=', 0);
210
            }
211
        }
212 452
        if (!$this->_query->add_constraint($field, $operator, $value)) {
213
            debug_add("Failed to execute add_constraint.", MIDCOM_LOG_ERROR);
214
            debug_add("Class = '{$this->_real_class}, Field = '{$field}', Operator = '{$operator}'");
215
            debug_print_r('Value:', $value);
216
217
            return false;
218
        }
219
220 452
        return true;
221
    }
222
223
    /**
224
     * Add a constraint against another DB column to the query.
225
     *
226
     * @param string $field The name of the MgdSchema property to query against.
227
     * @param string $operator The operator to use for the constraint, currently supported are
228
     *     <, <=, =, <>, >=, >, LIKE. LIKE uses the percent sign ('%') as a
229
     *     wildcard character.
230
     * @param string $compare_field The field to compare against.
231
     * @return boolean Indicating success.
232
     */
233
    public function add_constraint_with_property(string $field, string $operator, string $compare_field) : bool
234
    {
235
        $this->_reset();
236
        if (!$this->_query->add_constraint_with_property($field, $operator, $compare_field)) {
237
            debug_add("Failed to execute add_constraint_with_property.", MIDCOM_LOG_ERROR);
238
            debug_add("Class = '{$this->_real_class}, Field = '{$field}', Operator = '{$operator}', compare_field: '{$compare_field}'");
239
240
            return false;
241
        }
242
243
        return true;
244
    }
245
246
    /**
247
     * Creates a new logical group within the query. They are set in parentheses in the final
248
     * SQL and will thus be evaluated with precedence over the normal out-of-group constraints.
249
     *
250
     * While the call lets you decide whether all constraints within the group are AND'ed or OR'ed,
251
     * only OR constraints make logically sense in this context, which is why this proxy function
252
     * sets 'OR' as the default operator.
253
     *
254
     * @param string $operator One of 'OR' or 'AND' denoting the logical operation with which all
255
     *     constraints in the group are concatenated.
256
     */
257 254
    public function begin_group(string $operator)
258
    {
259 254
        if (!$this->_query->begin_group($operator)) {
260
            debug_add("Failed to execute begin_group {$operator}", MIDCOM_LOG_ERROR);
261
        }
262 254
    }
263
264
    /**
265
     * Ends a group previously started with begin_group().
266
     */
267 254
    public function end_group()
268
    {
269 254
        if (!$this->_query->end_group()) {
270
            debug_add("Failed to execute end_group", MIDCOM_LOG_ERROR);
271
        }
272 254
    }
273
274
    /**
275
     * Limits the resultset to contain at most the specified number of records.
276
     * Set the limit to zero to retrieve all available records.
277
     *
278
     * @param int $limit The maximum number of records in the resultset.
279
     */
280 144
    public function set_limit(int $limit)
281
    {
282 144
        $this->_reset();
283 144
        $this->_limit = $limit;
284 144
    }
285
286
    /**
287
     * Sets the offset of the first record to retrieve. This is a zero based index,
288
     * so if you want to retrieve from the very first record, the correct offset would
289
     * be zero, not one.
290
     *
291
     * @param int $offset The record number to start with.
292
     */
293 14
    public function set_offset(int $offset)
294
    {
295 14
        $this->_reset();
296
297 14
        $this->_offset = $offset;
298 14
    }
299
300
    /**
301
     * Add an ordering constraint to the query builder.
302
     *
303
     * @param string $field The name of the MgdSchema property to query against.
304
     * @param string $direction One of 'ASC' or 'DESC' indicating ascending or descending
305
     *     ordering. The default is 'ASC'.
306
     * @return boolean Indicating success.
307
     */
308 319
    public function add_order(string $field, string $direction = 'ASC') : bool
309
    {
310 319
        if (!$this->_query->add_order($field, $direction)) {
311
            debug_add("Failed to execute add_order for column '{$field}', midgard error: " . midcom_connection::get_error_string(), MIDCOM_LOG_ERROR);
312
            return false;
313
        }
314 319
        $this->_orders[] = [
315 319
            'field' => $field,
316 319
            'direction' => $direction
317
        ];
318
319 319
        return true;
320
    }
321
322
    /**
323
     * Get the DBA class we're currently working on
324
     */
325
    public function get_classname() : string
326
    {
327
        return $this->_real_class;
328
    }
329
330
    abstract public function execute();
331
332
    abstract public function count();
333
334
    abstract public function count_unchecked();
335
}
336