midgard_admin_asgard_navigation::_draw_element()   A
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 8
nop 4
dl 0
loc 28
ccs 0
cts 16
cp 0
crap 30
rs 9.4222
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package midgard.admin.asgard
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
 * Navigation class for Asgard
11
 *
12
 * @package midgard.admin.asgard
13
 */
14
class midgard_admin_asgard_navigation
15
{
16
    use midcom_baseclasses_components_base;
0 ignored issues
show
introduced by
The trait midcom_baseclasses_components_base requires some properties which are not provided by midgard_admin_asgard_navigation: $i18n, $head
Loading history...
17
18
    /**
19
     * @var midgard\portable\api\mgdobject
20
     */
21
    protected $_object;
22
23
    /**
24
     * Object path to the current object.
25
     */
26
    private array $_object_path = [];
27
28
    private array $_request_data = [];
29
    private array $expanded_root_types = [];
30
    protected array $shown_objects = [];
31
32 3
    public function __construct(?object $object, array &$request_data)
33
    {
34 3
        $this->_component = 'midgard.admin.asgard';
35
36 3
        $this->_object = $object;
37 3
        $this->_request_data =& $request_data;
38
39 3
        $root_types = midgard_admin_asgard_plugin::get_root_classes();
40
41 3
        if (array_key_exists('current_type', $request_data)) {
42
            $expanded_type = $request_data['current_type'];
43
            if (!array_key_exists($expanded_type, $root_types)) {
44
                $expanded_type = midcom_helper_reflector_tree::get($expanded_type)->get_parent_class();
45
            }
46
            $this->expanded_root_types[] = $expanded_type;
47 3
        } elseif (isset($this->_object)) {
48 3
            $this->_object_path = array_column(midcom_helper_reflector_tree::resolve_path_parts($object), 'object');
49
50
            // we go through the path bottom up and show the first root type we find
51 3
            foreach (array_reverse($this->_object_path) as $node) {
52 3
                foreach (array_keys($root_types) as $root_type) {
53 3
                    if (   $node instanceof $root_type
54 3
                        || midcom_helper_reflector::is_same_class($root_type, $node->__midcom_class_name__)) {
55 3
                        $this->expanded_root_types[] = $root_type;
56 3
                        break;
57
                    }
58
                }
59
            }
60
        }
61
    }
62
63
    protected function _is_collapsed(string $type, int $total) : bool
64
    {
65
        return (   $total > $this->_config->get('max_navigation_entries')
66
                && empty($_GET['show_all_' . $type]));
67
    }
68
69 3
    protected function _list_child_elements(object $object, int $level = 0)
70
    {
71 3
        if ($level > 25) {
72
            debug_add('Recursion level 25 exceeded, aborting', MIDCOM_LOG_ERROR);
73
            return;
74
        }
75 3
        $ref = midcom_helper_reflector_tree::get($object);
76
77 3
        $child_types = [];
78 3
        foreach ($ref->get_child_classes() as $class) {
79 3
            $qb = $ref->_child_objects_type_qb($class, $object, false);
80
81 3
            if (   !$qb
82 3
                || !($count = $qb->count_unchecked())) {
83 3
                continue;
84
            }
85 1
            midcom_helper_reflector_tree::add_schema_sorts_to_qb($qb, $class);
86 1
            if ($this->_is_collapsed($class, $count)) {
87
                $qb->set_limit($this->_config->get('max_navigation_entries'));
88
            }
89 1
            $child_types[$class] = ['total' => $count, 'qb' => $qb];
90
        }
91
92 3
        if (!empty($child_types)) {
93 1
            echo "<ul>\n";
94 1
            foreach ($child_types as $type => $data) {
95 1
                $children = $data['qb']->execute();
96 1
                $label_mapping = [];
97 1
                foreach ($children as $i => $child) {
98 1
                    if (isset($this->shown_objects[$child->guid])) {
99
                        continue;
100
                    }
101
102 1
                    $ref = midcom_helper_reflector_tree::get($child);
103 1
                    $label_mapping[$i] = htmlspecialchars($ref->get_object_label($child));
0 ignored issues
show
Bug introduced by
It seems like $ref->get_object_label($child) can also be of type null; however, parameter $string of htmlspecialchars() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

103
                    $label_mapping[$i] = htmlspecialchars(/** @scrutinizer ignore-type */ $ref->get_object_label($child));
Loading history...
104
                }
105
106 1
                asort($label_mapping);
107
108 1
                foreach ($label_mapping as $index => $label) {
109 1
                    $child = $children[$index];
110 1
                    $this->_draw_element($child, $label, $level);
111
                }
112 1
                if ($this->_is_collapsed($type, $data['total'])) {
113
                    $this->_draw_collapsed_element($level, $type, $data['total']);
114
                }
115
            }
116 1
            echo "</ul>\n";
117
        }
118
    }
119
120
    /**
121
     * Renders the given root objects to HTML and calls _list_child_elements()
122
     */
123
    private function _list_root_elements(midcom_helper_reflector_tree $ref)
124
    {
125
        $qb = $ref->_root_objects_qb();
126
127
        if (   !$qb
128
            || !($total = $qb->count_unchecked())) {
129
            return;
130
        }
131
        midcom_helper_reflector_tree::add_schema_sorts_to_qb($qb, $ref->mgdschema_class);
132
        if ($this->_is_collapsed($ref->mgdschema_class, $total)) {
133
            $qb->set_limit($this->_config->get('max_navigation_entries'));
134
        }
135
136
        echo "<ul class=\"midgard_admin_asgard_navigation\">\n";
137
138
        $root_objects = $qb->execute();
139
140
        $label_mapping = [];
141
        foreach ($root_objects as $i => $object) {
142
            $label_mapping[$i] = htmlspecialchars($ref->get_object_label($object));
0 ignored issues
show
Bug introduced by
It seems like $ref->get_object_label($object) can also be of type null; however, parameter $string of htmlspecialchars() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

142
            $label_mapping[$i] = htmlspecialchars(/** @scrutinizer ignore-type */ $ref->get_object_label($object));
Loading history...
143
        }
144
145
        asort($label_mapping);
146
        $autoexpand = (count($root_objects) == 1);
147
        foreach ($label_mapping as $index => $label) {
148
            $object = $root_objects[$index];
149
            $this->_draw_element($object, $label, 1, $autoexpand);
150
        }
151
        if ($this->_is_collapsed($ref->mgdschema_class, $total)) {
152
            $this->_draw_collapsed_element(0, $ref->mgdschema_class, $total);
153
        }
154
155
        echo "</ul>\n";
156
    }
157
158
    private function _draw_collapsed_element(int $level, string $type, int $total)
159
    {
160
        $ref = midcom_helper_reflector::get($type);
161
        if (!empty($this->_object_path[$level])) {
162
            if ($this->_object_path[$level]->__mgdschema_class_name__ == $type) {
163
                $object = $this->_object_path[$level];
164
            } elseif ($level == 0) {
165
                // this is the case where our object has parents, but we're in its type view directly
166
                foreach ($this->_object_path as $candidate) {
167
                    if ($candidate->__mgdschema_class_name__ == $type) {
168
                        $object = $candidate;
169
                        break;
170
                    }
171
                }
172
            }
173
            if (!empty($object)) {
174
                $label = htmlspecialchars($ref->get_object_label($object));
0 ignored issues
show
Bug introduced by
It seems like $ref->get_object_label($object) can also be of type null; however, parameter $string of htmlspecialchars() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

174
                $label = htmlspecialchars(/** @scrutinizer ignore-type */ $ref->get_object_label($object));
Loading history...
175
                $this->_draw_element($object, $label, $level);
176
            }
177
        }
178
        $icon = midcom_helper_reflector::get_object_icon(new $type);
179
        echo '<li><a class="expand-type-children" href="?show_all_' . $type . '=1">' . $icon . ' ' . sprintf($this->_l10n->get('show all %s %s entries'), $total, $ref->get_class_label()) . '</a></li>';
180
    }
181
182
    protected function _draw_element(object $object, string $label, int $level, bool $autoexpand = false)
183
    {
184
        $ref = midcom_helper_reflector_tree::get($object);
185
186
        $selected = $this->_is_selected($object);
187
        $css_class = $this->get_css_classes($object, $ref->mgdschema_class);
188
189
        $mode = $this->_request_data['default_mode'];
190
        if (str_contains($css_class, 'readonly')) {
191
            $mode = 'view';
192
        }
193
194
        $this->shown_objects[$object->guid] = true;
195
196
        echo "    <li class=\"{$css_class}\">";
197
198
        $icon = $ref->get_object_icon($object);
199
200
        if (trim($label) == '') {
201
            $label = $ref->get_class_label() . ' #' . $object->id;
202
        }
203
204
        echo "<a href=\"" . midcom_connection::get_url('self') . "__mfa/asgard/object/{$mode}/{$object->guid}/\" title=\"GUID: {$object->guid}, ID: {$object->id}\">{$icon}{$label}</a>\n";
205
        if (   $selected
206
            || $autoexpand) {
207
            $this->_list_child_elements($object, $level + 1);
208
        }
209
        echo "    </li>\n";
210
    }
211
212
    private function _draw_plugins()
213
    {
214
        $customdata = midcom::get()->componentloader->get_all_manifest_customdata('asgard_plugin');
215
        foreach ($customdata as $component => $plugin_config) {
216
            $this->_request_data['section_url'] = midcom_connection::get_url('self') . "__mfa/asgard_{$component}/";
217
            $this->_request_data['section_name'] = $this->_i18n->get_string($component, $component);
218
            $class = $plugin_config['class'];
219
220
            if (!midcom::get()->auth->can_user_do("{$component}:access", class: $class)) {
221
                // Disabled plugin
222
                continue;
223
            }
224
225
            if (   method_exists($class, 'navigation')
226
                && ($this->_request_data['plugin_name'] == "asgard_{$component}")) {
227
                $this->_request_data['expanded'] = true;
228
                midcom_show_style('midgard_admin_asgard_navigation_section_header');
229
                $class::navigation();
230
            } else {
231
                $this->_request_data['expanded'] = false;
232
                midcom_show_style('midgard_admin_asgard_navigation_section_header');
233
            }
234
235
            midcom_show_style('midgard_admin_asgard_navigation_section_footer');
236
        }
237
    }
238
239 1
    private function _is_selected(object $object) : bool
240
    {
241 1
        foreach ($this->_object_path as $path_object) {
242 1
            if ($object->guid == $path_object->guid) {
243
                return true;
244
            }
245
        }
246 1
        return false;
247
    }
248
249 1
    protected function get_css_classes(object $object, string $mgdschema_class) : string
250
    {
251 1
        $css_class = $object::class . " {$mgdschema_class}";
252
253
        // Populate common properties
254 1
        $css_class = midcom::get()->metadata->get_object_classes($object, $css_class);
255
256 1
        if ($this->_is_selected($object)) {
257
            $css_class .= ' selected';
258
        }
259 1
        if (   is_object($this->_object)
260 1
            && (   $object->guid == $this->_object->guid
261 1
                || (   $this->_object instanceof midcom_db_parameter
262 1
                    && $object->guid == $this->_object->parentguid))) {
263
            $css_class .= ' current';
264
        }
265 1
        if ( !$object->can_do('midgard:update')) {
266
            $css_class .= ' readonly';
267
        }
268 1
        return $css_class;
269
    }
270
271
    /**
272
     * Apply visibility restrictions from various sources
273
     *
274
     * @return array Alphabetically sorted list of class => title pairs
275
     */
276
    private function _process_root_types() : array
277
    {
278
        // Get the inclusion/exclusion model
279
        $exclude = midgard_admin_asgard_plugin::get_preference('midgard_types_model') == 'exclude';
280
281
        $label_mapping = midgard_admin_asgard_plugin::get_root_classes();
282
283
        // Get the types that might have special display conditions
284
        // @TODO: Should this just include to the configuration selection, although it would break the consistency
285
        // of other similar preference sets, which simply override the global settings?
286
        if (   ($selected = midgard_admin_asgard_plugin::get_preference('midgard_types'))
287
            && preg_match_all('/\|([a-z0-9\.\-_]+)/', $selected, $regs)) {
288
            $types = array_flip($regs[1]);
289
            if ($exclude) {
290
                $label_mapping = array_diff_key($label_mapping, $types);
291
            } else {
292
                $label_mapping = array_intersect_key($label_mapping, $types);
293
            }
294
        }
295
296
        // Get the possible regular expression
297
        if ($regexp = midgard_admin_asgard_plugin::get_preference('midgard_types_regexp')) {
298
            // "Convert" quickly to PERL regular expression
299
            if (!preg_match('/^[\/|]/', $regexp)) {
300
                $regexp = "/{$regexp}/";
301
            }
302
303
            // If the regular expression has been set, check which types should be shown
304
            $label_mapping = array_filter($label_mapping, function ($root_type) use ($regexp, $exclude) {
305
                return preg_match($regexp, $root_type) == $exclude;
306
            }, ARRAY_FILTER_USE_KEY);
307
        }
308
309
        return $label_mapping;
310
    }
311
312
    public function draw()
313
    {
314
        $this->_request_data['chapter_name'] = midcom::get()->config->get('midcom_site_title');
315
        midcom_show_style('midgard_admin_asgard_navigation_chapter');
316
317
        $this->_draw_plugins();
318
319
        if (!midcom::get()->auth->can_user_do('midgard.admin.asgard:manage_objects', class: 'midgard_admin_asgard_plugin')) {
320
            return;
321
        }
322
323
        $label_mapping = $this->_process_root_types();
324
325
        $expanded_types = array_intersect(array_keys($label_mapping), $this->expanded_root_types);
326
327
        /*
328
         * Use a dropdown for displaying the navigation if at least one type is expanded
329
         * and the user has the corresponding preference set. That way, you expanded types
330
         * can take up the maximum available space while all types are still accessible with one
331
         * click if nothing is expanded
332
         */
333
        $types_shown = false;
334
        if (    !empty($expanded_types)
335
             && midgard_admin_asgard_plugin::get_preference('navigation_type') === 'dropdown') {
336
            $this->_draw_select_navigation();
337
            $types_shown = true;
338
        }
339
340
        foreach ($expanded_types as $root_type) {
341
            $this->_request_data['section_url'] = midcom_connection::get_url('self') . "__mfa/asgard/{$root_type}/";
342
            $this->_request_data['section_name'] = $label_mapping[$root_type];
343
            $this->_request_data['expanded'] = true;
344
            midcom_show_style('midgard_admin_asgard_navigation_section_header');
345
            $ref = midcom_helper_reflector_tree::get($root_type);
346
            $this->_list_root_elements($ref);
347
348
            midcom_show_style('midgard_admin_asgard_navigation_section_footer');
349
        }
350
351
        if (!$types_shown) {
352
            $this->_request_data['section_name'] = $this->_l10n->get('midgard objects');
353
            $this->_request_data['section_url'] = null;
354
            $this->_request_data['expanded'] = true;
355
            midcom_show_style('midgard_admin_asgard_navigation_section_header');
356
            $collapsed_types = array_diff_key($label_mapping, array_flip($expanded_types));
357
358
            $this->_draw_type_list($collapsed_types);
359
360
            midcom_show_style('midgard_admin_asgard_navigation_section_footer');
361
        }
362
    }
363
364
    private function _draw_type_list(array $types)
365
    {
366
        echo "<ul class=\"midgard_admin_asgard_navigation\">\n";
367
368
        foreach ($types as $type => $label) {
369
            $url = midcom_connection::get_url('self') . "__mfa/asgard/{$type}/";
370
            echo "    <li class=\"mgdschema-type\">";
371
372
            if ($dbaclass = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($type)) {
373
                $object = new $dbaclass;
374
            } else {
375
                $object = new $type;
376
            }
377
            $icon = midcom_helper_reflector::get_object_icon($object);
378
379
            echo "<a href=\"" . $url . "\" title=\"{$label}\">{$icon}{$label}</a>\n";
380
381
            echo "    </li>\n";
382
        }
383
384
        echo "</ul>\n";
385
    }
386
387
    private function _draw_select_navigation()
388
    {
389
        if (!empty($this->_object_path)) {
390
            $this->_request_data['root_object'] = $this->_object_path[0];
391
            $this->_request_data['navigation_type'] = $this->_object_path[0]->__mgdschema_class_name__;
392
        } else {
393
            $this->_request_data['navigation_type'] = $this->expanded_root_types[0] ?? '';
394
        }
395
396
        $this->_request_data['label_mapping'] = midgard_admin_asgard_plugin::get_root_classes();
397
        $this->_request_data['expanded_root_types'] = $this->expanded_root_types;
398
399
        midcom_show_style('midgard_admin_asgard_navigation_sections');
400
    }
401
}
402