Completed
Push — master ( ef9dbd...271d52 )
by Andreas
14:53
created

midcom_helper_reflector_tree::_check_permissions()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4.3731

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 2
nop 1
dl 0
loc 10
ccs 5
cts 7
cp 0.7143
crap 4.3731
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package midcom.helper.reflector
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
/**
10
 * The Grand Unified Reflector, Tree information
11
 *
12
 * @package midcom.helper.reflector
13
 */
14
class midcom_helper_reflector_tree extends midcom_helper_reflector
15
{
16
    /**
17
     * Creates a QB instance for get_root_objects
18
     */
19 5
    public function _root_objects_qb($deleted)
20
    {
21 5
        $schema_type = $this->mgdschema_class;
22 5
        $root_classes = self::get_root_classes();
23 5
        if (!in_array($schema_type, $root_classes)) {
24
            debug_add("Type {$schema_type} is not a \"root\" type", MIDCOM_LOG_ERROR);
25
            return false;
26
        }
27
28 5
        $qb = $this->_get_type_qb($schema_type, $deleted);
29 5
        if (!$qb) {
30
            debug_add("Could not get QB for type '{$schema_type}'", MIDCOM_LOG_ERROR);
31
            return false;
32
        }
33
34
        // Figure out constraint to use to get root level objects
35 5
        $upfield = midgard_object_class::get_property_up($schema_type);
36 5
        if (!empty($upfield)) {
37 5
            $uptype = $this->_mgd_reflector->get_midgard_type($upfield);
38
            switch ($uptype) {
39 5
                case MGD_TYPE_STRING:
40 5
                case MGD_TYPE_GUID:
41
                    $qb->add_constraint($upfield, '=', '');
42
                    break;
43 5
                case MGD_TYPE_INT:
44 5
                case MGD_TYPE_UINT:
45 5
                    $qb->add_constraint($upfield, '=', 0);
46 5
                    break;
47
                default:
48
                    debug_add("Do not know how to handle upfield '{$upfield}' has type {$uptype}", MIDCOM_LOG_ERROR);
49
                    return false;
50
            }
51
        }
52 5
        return $qb;
53
    }
54
55
    /**
56
     * Get "root" objects for the class this reflector was instantiated for
57
     *
58
     * NOTE: deleted objects can only be listed as admin, also: they do not come
59
     * MidCOM DBA wrapped (since you cannot normally instantiate such object)
60
     *
61
     * @param boolean $deleted whether to get (only) deleted or not-deleted objects
62
     * @return array of objects or false on failure
63
     */
64
    public function get_root_objects($deleted = false)
65
    {
66
        if (!self::_check_permissions($deleted)) {
67
            return false;
68
        }
69
70
        $qb = $this->_root_objects_qb($deleted);
71
        if (!$qb) {
72
            debug_add('Could not get QB instance', MIDCOM_LOG_ERROR);
73
            return false;
74
        }
75
        self::add_schema_sorts_to_qb($qb, $this->mgdschema_class);
76
77
        return $qb->execute();
78
    }
79
80
    /**
81
     * Get rendered path for object
82
     *
83
     * @param midgard\portable\api\mgdobject $object The object to get path for
84
     * @param string $separator the string used to separate path components
85
     * @return string resolved path
86
     */
87 1
    public static function resolve_path($object, $separator = ' &gt; ') : string
88
    {
89 1
        $parts = self::resolve_path_parts($object);
90 1
        return implode($separator, array_column($parts, 'label'));
91
    }
92
93
    /**
94
     * Get path components for object
95
     *
96
     * @param midgard\portable\api\mgdobject $object The object to get path for
97
     * @return array path components
98
     */
99 3
    public static function resolve_path_parts($object) : array
100
    {
101 3
        static $cache = [];
102 3
        if (isset($cache[$object->guid])) {
103 2
            return $cache[$object->guid];
104
        }
105
106 2
        $ret = [];
107 2
        $ret[] = [
108 2
            'object' => $object,
109 2
            'label' => parent::get($object)->get_object_label($object),
110
        ];
111
112 2
        $parent = self::get_parent($object);
113 2
        while (is_object($parent)) {
114 1
            $ret[] = [
115 1
                'object' => $parent,
116 1
                'label' => parent::get($parent)->get_object_label($parent),
117
            ];
118 1
            $parent = self::get_parent($parent);
119
        }
120
121 2
        $cache[$object->guid] = array_reverse($ret);
122 2
        return $cache[$object->guid];
123
    }
124
125
    /**
126
     * Get the parent object of given object
127
     *
128
     * Tries to utilize MidCOM DBA features first but can fallback on pure MgdSchema
129
     * as necessary
130
     *
131
     * NOTE: since this might fall back to pure MgdSchema never trust that MidCOM DBA features
132
     * are available, check for is_callable/method_exists first !
133
     *
134
     * @param midgard\portable\api\mgdobject $object the object to get parent for
135
     */
136 91
    public static function get_parent($object)
137
    {
138 91
        if (method_exists($object, 'get_parent')) {
139
            /**
140
             * The object might have valid reasons for returning empty value here, but we can't know if it's
141
             * because it's valid or because the get_parent* methods have not been overridden in the actually
142
             * used class
143
             */
144 91
            return $object->get_parent();
145
        }
146
147
        return false;
148
    }
149
150 20
    private static function _check_permissions($deleted) : bool
151
    {
152
        // PONDER: Check for some generic user privilege instead  ??
153 20
        if (   $deleted
154 20
            && !midcom_connection::is_admin()
155 20
            && !midcom::get()->auth->is_component_sudo()) {
156
            debug_add('Non-admins are not allowed to list deleted objects', MIDCOM_LOG_ERROR);
157
            return false;
158
        }
159 20
        return true;
160
    }
161
162
    /**
163
     * Get children of given object
164
     *
165
     * @param midgard\portable\api\mgdobject $object object to get children for
166
     * @param boolean $deleted whether to get (only) deleted or not-deleted objects
167
     * @return array multidimensional array (keyed by classname) of objects or false on failure
168
     */
169 20
    public static function get_child_objects($object, $deleted = false)
170
    {
171 20
        if (!self::_check_permissions($deleted)) {
172
            return false;
173
        }
174 20
        $resolver = new self($object);
175 20
        $child_classes = $resolver->get_child_classes();
176 20
        if (!$child_classes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $child_classes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
177
            return false;
178
        }
179
180 20
        $child_objects = [];
181 20
        foreach ($child_classes as $schema_type) {
182 20
            $type_children = $resolver->_get_child_objects_type($schema_type, $object, $deleted);
183
            // PONDER: check for boolean false as result ??
184 20
            if (empty($type_children)) {
185 20
                continue;
186
            }
187 1
            $child_objects[$schema_type] = $type_children;
188
        }
189 20
        return $child_objects;
190
    }
191
192 97
    private function _get_type_qb($schema_type, $deleted)
193
    {
194 97
        if (empty($schema_type)) {
195
            debug_add('Passed schema_type argument is empty, this is fatal', MIDCOM_LOG_ERROR);
196
            return false;
197
        }
198 97
        if ($deleted) {
199
            $qb = new midgard_query_builder($schema_type);
200
            $qb->include_deleted();
201
            $qb->add_constraint('metadata.deleted', '<>', 0);
202
            return $qb;
203
        }
204
        // Figure correct MidCOM DBA class to use and get midcom QB
205 97
        $midcom_dba_classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($schema_type);
206 97
        if (empty($midcom_dba_classname)) {
207
            debug_add("MidCOM DBA does not know how to handle {$schema_type}", MIDCOM_LOG_ERROR);
208
            return false;
209
        }
210
211 97
        return call_user_func([$midcom_dba_classname, 'new_query_builder']);
212
    }
213
214
    /**
215
     * Figure out constraint(s) to use to get child objects
216
     */
217 96
    private function _get_link_fields($schema_type, $for_object) : array
218
    {
219 96
        static $cache = [];
220 96
        $cache_key = $schema_type . '-' . get_class($for_object);
221 96
        if (empty($cache[$cache_key])) {
222 7
            $ref = new midgard_reflection_property($schema_type);
223
224
            $linkfields = [
225 7
                'up' => midgard_object_class::get_property_up($schema_type),
226 7
                'parent' => midgard_object_class::get_property_parent($schema_type)
227
            ];
228 7
            $linkfields = array_filter($linkfields);
229 7
            $data = [];
230 7
            foreach ($linkfields as $link_type => $field) {
231
                $info = [
232 7
                    'name' => $field,
233 7
                    'type' => $ref->get_midgard_type($field),
234 7
                    'target' => $ref->get_link_target($field)
235
                ];
236 7
                $linked_class = $ref->get_link_name($field);
237 7
                if (   empty($linked_class)
238 7
                    && $info['type'] === MGD_TYPE_GUID) {
239
                    // Guid link without class specification, valid for all classes
240 7
                    if (empty($info['target'])) {
241 7
                        $info['target'] = 'guid';
242
                    }
243 5
                } elseif (!self::is_same_class($linked_class, get_class($for_object))) {
244
                    // This link points elsewhere
245 2
                    continue;
246
                }
247 7
                $data[$link_type] = $info;
248
            }
249 7
            $cache[$cache_key] = $data;
250
        }
251 96
        return $cache[$cache_key];
252
    }
253
254
    /**
255
     * Creates a QB instance for _get_child_objects_type
256
     */
257 96
    public function _child_objects_type_qb($schema_type, $for_object, $deleted)
258
    {
259 96
        if (!is_object($for_object)) {
260
            debug_add('Passed for_object argument is not object, this is fatal', MIDCOM_LOG_ERROR);
261
            return false;
262
        }
263 96
        $qb = $this->_get_type_qb($schema_type, $deleted);
264 96
        if (!$qb) {
265
            debug_add("Could not get QB for type '{$schema_type}'", MIDCOM_LOG_ERROR);
266
            return false;
267
        }
268
269 96
        $linkfields = $this->_get_link_fields($schema_type, $for_object);
270
271 96
        if (empty($linkfields)) {
272
            debug_add("Class '{$schema_type}' has no valid link properties pointing to class '" . get_class($for_object) . "', this should not happen here", MIDCOM_LOG_ERROR);
273
            return false;
274
        }
275
276 96
        $multiple_links = false;
277 96
        if (count($linkfields) > 1) {
278
            $multiple_links = true;
279
            $qb->begin_group('OR');
280
        }
281
282 96
        foreach ($linkfields as $link_type => $field_data) {
283 96
            $field_target = $field_data['target'];
284 96
            $field_type = $field_data['type'];
285 96
            $field = $field_data['name'];
286
287 96
            if (   !$field_target
288 96
                || !isset($for_object->$field_target)) {
289
                // Why return false ???
290
                return false;
291
            }
292
            switch ($field_type) {
293 96
                case MGD_TYPE_STRING:
294 96
                case MGD_TYPE_GUID:
295 96
                    $qb->add_constraint($field, '=', (string) $for_object->$field_target);
296 96
                    break;
297 93
                case MGD_TYPE_INT:
298 93
                case MGD_TYPE_UINT:
299 93
                    if ($link_type == 'up') {
300 93
                        $qb->add_constraint($field, '=', (int) $for_object->$field_target);
301 87
                    } elseif ($link_type == 'parent') {
302 87
                        $up_property = midgard_object_class::get_property_up($schema_type);
303 87
                        if (!empty($up_property)) {
304
                            //we only return direct children (otherwise they would turn up twice in recursive queries)
305 87
                            $qb->begin_group('AND');
306 87
                            $qb->add_constraint($field, '=', (int) $for_object->$field_target);
307 87
                            $qb->add_constraint($up_property, '=', 0);
308 87
                            $qb->end_group();
309
                        } else {
310 87
                            $qb->add_constraint($field, '=', (int) $for_object->$field_target);
311
                        }
312
                    } else {
313
                        $qb->begin_group('AND');
314
                        $qb->add_constraint($field, '=', (int) $for_object->$field_target);
315
                        // make sure we don't accidentally find other objects with the same id
316
                        $qb->add_constraint($field . '.guid', '=', (string) $for_object->guid);
317
                        $qb->end_group();
318
                    }
319 93
                    break;
320
                default:
321
                    debug_add("Do not know how to handle linked field '{$field}', has type {$field_type}", MIDCOM_LOG_INFO);
322
323
                    // Why return false ???
324 96
                    return false;
325
            }
326
        }
327
328 96
        if ($multiple_links) {
329
            $qb->end_group();
330
        }
331
332 96
        return $qb;
333
    }
334
335
    /**
336
     * Used by get_child_objects
337
     *
338
     * @return array of objects
339
     */
340 20
    public function _get_child_objects_type($schema_type, $for_object, $deleted)
341
    {
342 20
        $qb = $this->_child_objects_type_qb($schema_type, $for_object, $deleted);
343 20
        if (!$qb) {
344
            debug_add('Could not get QB instance', MIDCOM_LOG_ERROR);
345
            return false;
346
        }
347
348
        // Sort by title and name if available
349 20
        self::add_schema_sorts_to_qb($qb, $schema_type);
350
351 20
        return $qb->execute();
352
    }
353
354
    /**
355
     * Get the parent class of the class this reflector was instantiated for
356
     *
357
     * @return string class name (or false if the type has no parent)
358
     */
359 2
    public function get_parent_class()
360
    {
361 2
        $parent_property = midgard_object_class::get_property_parent($this->mgdschema_class);
362 2
        if (!$parent_property) {
363 2
            return false;
364
        }
365
        $ref = new midgard_reflection_property($this->mgdschema_class);
366
        return $ref->get_link_name($parent_property);
367
    }
368
369
    /**
370
     * Get the child classes of the class this reflector was instantiated for
371
     *
372
     * @return array of class names
373
     */
374 97
    public function get_child_classes() : array
375
    {
376 97
        static $child_classes_all = [];
377 97
        if (!isset($child_classes_all[$this->mgdschema_class])) {
378 5
            $child_classes_all[$this->mgdschema_class] = $this->_resolve_child_classes();
379
        }
380 97
        return $child_classes_all[$this->mgdschema_class];
381
    }
382
383
    /**
384
     * Resolve the child classes of the class this reflector was instantiated for, used by get_child_classes()
385
     *
386
     * @return array of class names
387
     */
388 5
    private function _resolve_child_classes() : array
389
    {
390 5
        $child_class_exceptions_neverchild = $this->_config->get('child_class_exceptions_neverchild');
391
392
        // Safety against misconfiguration
393 5
        if (!is_array($child_class_exceptions_neverchild)) {
394
            debug_add("config->get('child_class_exceptions_neverchild') did not return array, invalid configuration ??", MIDCOM_LOG_ERROR);
395
            $child_class_exceptions_neverchild = [];
396
        }
397 5
        $child_classes = [];
398 5
        $types = array_diff(midcom_connection::get_schema_types(), $child_class_exceptions_neverchild);
399 5
        foreach ($types as $schema_type) {
400 5
            $parent_property = midgard_object_class::get_property_parent($schema_type);
401 5
            $up_property = midgard_object_class::get_property_up($schema_type);
402
403 5
            if (   !$this->_resolve_child_classes_links_back($parent_property, $schema_type, $this->mgdschema_class)
404 5
                && !$this->_resolve_child_classes_links_back($up_property, $schema_type, $this->mgdschema_class)) {
405 5
                continue;
406
            }
407
408 5
            $child_classes[] = $schema_type;
409
        }
410
411
        //make sure children of the same type come out on top
412 5
        if ($key = array_search($this->mgdschema_class, $child_classes)) {
413 2
            unset($child_classes[$key]);
414 2
            array_unshift($child_classes, $this->mgdschema_class);
415
        }
416 5
        return $child_classes;
417
    }
418
419 5
    private function _resolve_child_classes_links_back($property, $prospect_type, $schema_type) : bool
420
    {
421 5
        if (empty($property)) {
422 5
            return false;
423
        }
424
425 5
        $ref = new midgard_reflection_property($prospect_type);
426 5
        $link_class = $ref->get_link_name($property);
427 5
        if (   empty($link_class)
428 5
            && $ref->get_midgard_type($property) === MGD_TYPE_GUID) {
429 5
            return true;
430
        }
431 5
        return self::is_same_class($link_class, $schema_type);
432
    }
433
434
    /**
435
     * Get an array of "root level" classes, can (and should) be called statically
436
     *
437
     * @return array of classnames (or false on critical failure)
438
     */
439 12
    public static function get_root_classes() : array
440
    {
441 12
        static $root_classes = false;
442 12
        if (empty($root_classes)) {
443 1
            $root_classes = self::_resolve_root_classes();
444
        }
445 12
        return $root_classes;
446
    }
447
448
    /**
449
     * Resolves the "root level" classes, used by get_root_classes()
450
     *
451
     * @return array of classnames (or false on critical failure)
452
     */
453 1
    private static function _resolve_root_classes() : array
454
    {
455 1
        $root_exceptions_notroot = midcom_baseclasses_components_configuration::get('midcom.helper.reflector', 'config')->get('root_class_exceptions_notroot');
456
        // Safety against misconfiguration
457 1
        if (!is_array($root_exceptions_notroot)) {
458
            debug_add("config->get('root_class_exceptions_notroot') did not return array, invalid configuration ??", MIDCOM_LOG_ERROR);
459
            $root_exceptions_notroot = [];
460
        }
461 1
        $root_classes = [];
462 1
        $types = array_diff(midcom_connection::get_schema_types(), $root_exceptions_notroot);
463 1
        foreach ($types as $schema_type) {
464
            // Class extensions mapping
465 1
            $schema_type = self::class_rewrite($schema_type);
466
467
            // Make sure we only add classes once
468 1
            if (in_array($schema_type, $root_classes)) {
469
                // Already listed
470
                continue;
471
            }
472
473 1
            if (midgard_object_class::get_property_parent($schema_type)) {
474
                // type has parent set, thus cannot be root type
475 1
                continue;
476
            }
477
478 1
            if (!midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($schema_type)) {
479
                // Not a MidCOM DBA object, skip
480
                continue;
481
            }
482
483 1
            $root_classes[] = $schema_type;
484
        }
485
486 1
        $root_exceptions_forceroot = midcom_baseclasses_components_configuration::get('midcom.helper.reflector', 'config')->get('root_class_exceptions_forceroot');
487
        // Safety against misconfiguration
488 1
        if (!is_array($root_exceptions_forceroot)) {
489
            debug_add("config->get('root_class_exceptions_forceroot') did not return array, invalid configuration ??", MIDCOM_LOG_ERROR);
490
            $root_exceptions_forceroot = [];
491
        }
492 1
        $root_exceptions_forceroot = array_diff($root_exceptions_forceroot, $root_classes);
493 1
        foreach ($root_exceptions_forceroot as $schema_type) {
494
            if (!class_exists($schema_type)) {
495
                // Not a valid class
496
                debug_add("Type {$schema_type} has been listed to always be root class, but the class does not exist", MIDCOM_LOG_WARN);
497
                continue;
498
            }
499
            $root_classes[] = $schema_type;
500
        }
501
502 1
        usort($root_classes, 'strnatcmp');
503 1
        return $root_classes;
504
    }
505
506
    /**
507
     * Add default ("title" and "name") sorts to a QB instance
508
     *
509
     * @param midgard_query_builder $qb QB instance
510
     * @param string $schema_type valid mgdschema class name
511
     */
512 20
    public static function add_schema_sorts_to_qb($qb, $schema_type)
513
    {
514
        // Sort by "title" and "name" if available
515 20
        $ref = self::get($schema_type);
516 20
        $dummy = new $schema_type();
517 20
        if ($title_property = $ref->get_title_property($dummy)) {
518 20
            $qb->add_order($title_property);
519
        }
520 20
        if ($name_property = $ref->get_name_property($dummy)) {
521 15
            $qb->add_order($name_property);
522
        }
523 20
    }
524
525
    /**
526
     * List object children
527
     *
528
     * @param midcom_core_dbaobject $parent
529
     * @return array
530
     */
531
    public static function get_tree(midcom_core_dbaobject $parent) : array
532
    {
533
        static $shown_guids = [];
534
        $tree = [];
535
        try {
536
            $children = self::get_child_objects($parent);
0 ignored issues
show
Bug introduced by
$parent of type midcom_core_dbaobject is incompatible with the type midgard\portable\api\mgdobject expected by parameter $object of midcom_helper_reflector_tree::get_child_objects(). ( Ignorable by Annotation )

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

536
            $children = self::get_child_objects(/** @scrutinizer ignore-type */ $parent);
Loading history...
537
        } catch (midcom_error $e) {
538
            return $tree;
539
        }
540
541
        foreach ($children as $class => $objects) {
542
            $reflector = parent::get($class);
543
544
            foreach ($objects as $object) {
545
                if (array_key_exists($object->guid, $shown_guids)) {
546
                    //we might see objects twice if they have both up and parent
547
                    continue;
548
                }
549
                $shown_guids[$object->guid] = true;
550
551
                $leaf = [
552
                    'title' => $reflector->get_object_label($object),
553
                    'icon' => $reflector->get_object_icon($object),
554
                    'class' => $class
555
                ];
556
                $grandchildren = self::get_tree($object);
557
                if (!empty($grandchildren)) {
558
                    $leaf['children'] = $grandchildren;
559
                }
560
                $tree[] = $leaf;
561
            }
562
        }
563
        return $tree;
564
    }
565
}
566