Completed
Push — master ( 1b69f8...3702be )
by Andreas
14:12
created

midcom_helper_nav_backend::list_nodes()   B

Complexity

Conditions 9
Paths 13

Size

Total Lines 40
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 9.4867

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 9
eloc 21
c 3
b 0
f 0
nc 13
nop 2
dl 0
loc 40
ccs 18
cts 22
cp 0.8182
crap 9.4867
rs 8.0555
1
<?php
2
/**
3
 * @package midcom.helper
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
 * This class is the basic building stone of the Navigation Access Point
11
 * System of MidCOM.
12
 *
13
 * It is responsible for collecting the available
14
 * information and for building the navigational tree out of it. This
15
 * class is only the internal interface to the NAP System and is used by
16
 * midcom_helper_nav as a node cache. The framework should ensure that
17
 * only one class of this type is active at one time.
18
 *
19
 * It will give you a very abstract view of the content tree, modified
20
 * by the NAP classes of the components. You can retrieve a node/leaf tree
21
 * of the content, and for each element you can retrieve a URL name and a
22
 * long name for navigation display.
23
 *
24
 * Leaves and Nodes are both indexed by integer constants which are assigned
25
 * by the framework. The framework defines two starting points in this tree:
26
 * The root node and the "current" node. The current node defined through
27
 * the topic of the component that declared to be able to handle the request.
28
 *
29
 * The class will load the necessary information on demand to minimize
30
 * database traffic.
31
 *
32
 * The interface functions should enable you to build any navigation tree you
33
 * desire. The public nav class will give you some of those high-level
34
 * functions.
35
 *
36
 * <b>Node data interchange format</b>
37
 *
38
 * Node NAP data consists of a simple key => value array with the following
39
 * keys required by the component:
40
 *
41
 * - MIDCOM_NAV_NAME => The real (= displayable) name of the element
42
 *
43
 * Other keys delivered to NAP users include:
44
 *
45
 * - MIDCOM_NAV_URL  => The URL name of the element, which is automatically
46
 *   defined by NAP.
47
 *
48
 * <b>Leaf data interchange format</b>
49
 *
50
 * Basically for each leaf the usual meta information is returned:
51
 *
52
 * - MIDCOM_NAV_URL      => URL of the leaf element
53
 * - MIDCOM_NAV_NAME     => Name of the leaf element
54
 * - MIDCOM_NAV_GUID     => Optional argument denoting the GUID of the referred element
55
 * - MIDCOM_NAV_SORTABLE => Optional argument denoting whether the element is sortable
56
 *
57
 * @package midcom.helper
58
 */
59
class midcom_helper_nav_backend
60
{
61
    /**
62
     * The ID of the MidCOM Root Content Topic
63
     *
64
     * @var int
65
     */
66
    private $_root;
67
68
    /**
69
     * The ID of the currently active Navigation Node, determined by the active
70
     * MidCOM Topic or one of its uplinks, if the subtree in question is invisible.
71
     *
72
     * @var int
73
     */
74
    private $_current;
75
76
    /**
77
     * The GUID of the currently active leaf.
78
     *
79
     * @var string
80
     */
81
    private $_currentleaf = false;
82
83
    /**
84
     * Leaf cache. It is an array which contains elements indexed by
85
     * their leaf ID. The data is again stored in an associative array:
86
     *
87
     * - MIDCOM_NAV_NODEID => ID of the parent node (int)
88
     * - MIDCOM_NAV_URL => URL name of the leaf (string)
89
     * - MIDCOM_NAV_NAME => Textual name of the leaf (string)
90
     *
91
     * @todo Update the data structure documentation
92
     * @var midcom_helper_nav_leaf[]
93
     */
94
    private $_leaves = [];
95
96
    /**
97
     * Node cache. It is an array which contains elements indexed by
98
     * their node ID. The data is again stored in an associative array:
99
     *
100
     * - MIDCOM_NAV_NODEID => ID of the parent node (-1 for the root node) (int)
101
     * - MIDCOM_NAV_URL => URL name of the leaf (string)
102
     * - MIDCOM_NAV_NAME => Textual name of the leaf (string)
103
     *
104
     * @todo Update the data structure documentation
105
     * @var midcom_helper_nav_node[]
106
     */
107
    private static $_nodes = [];
108
109
    /**
110
     * List of all topics for which the leaves have been loaded.
111
     * If the id of the node is in this array, the leaves are available, otherwise,
112
     * the leaves have to be loaded.
113
     *
114
     * @var midcom_helper_nav_leaf[]
115
     */
116
    private $_loaded_leaves = [];
117
118
    /**
119
     * The NAP cache store
120
     *
121
     * @var midcom_services_cache_module_nap
122
     */
123
    private $_nap_cache;
124
125
    /**
126
     * This array holds the node path from the URL. First value at key 0 is
127
     * the root node ID, possible second value is the first subnode ID etc.
128
     * Contains only visible nodes (nodes which can be loaded).
129
     *
130
     * @var Array
131
     */
132
    private $_node_path = [];
133
134
    /**
135
     * Constructor
136
     *
137
     * It will initialize Root Topic, Current Topic and all cache arrays.
138
     * The constructor retrieves all initialization data from the component context.
139
     *
140
     * @param midcom_db_topic $root
141
     * @param midcom_db_topic[] $urltopics
142
     */
143 293
    public function __construct(midcom_db_topic $root, array $urltopics)
144
    {
145 293
        $this->_nap_cache = midcom::get()->cache->nap;
0 ignored issues
show
Bug introduced by
The property nap does not seem to exist on midcom_services_cache.
Loading history...
146
147 293
        $this->_root = $root->id;
148 293
        $this->_current = $this->_root;
149
150 293
        $this->init_topics($root, $urltopics);
151 293
    }
152
153
    /**
154
     * Loads all nodes between root and current node.
155
     *
156
     * If the current node is behind an invisible or undescendable node, the last
157
     * known good node will be used instead for the current node.
158
     *
159
     * @param midcom_db_topic $root
160
     * @param midcom_db_topic[] $urltopics
161
     */
162 293
    private function init_topics(midcom_db_topic $root, array $urltopics)
163
    {
164 293
        $node_path_candidates = [$root];
165 293
        foreach ($urltopics as $topic) {
166 3
            $node_path_candidates[] = $topic;
167 3
            $this->_current = $topic->id;
168
        }
169
170 293
        $lastgood = null;
171 293
        foreach ($node_path_candidates as $topic) {
172 293
            if (!$this->load_node($topic)) {
173
                // Node is hidden behind an undescendable one
174
                $this->_current = $lastgood;
175
                return;
176
            }
177 293
            $this->_node_path[] = $topic->id;
178 293
            $lastgood = $topic->id;
179
        }
180 293
    }
181
182
    /**
183
     * Load the navigational information associated with the topic $param, which
184
     * can be passed as an ID or as a MidgardTopic object.
185
     *
186
     * This function is the controlling instance of the loading mechanism. It
187
     * is able to load the navigation data of any topic within MidCOM's topic
188
     * tree into memory. Any uplink nodes that are not loaded into memory will
189
     * be loaded until any other known topic is encountered.
190
     *
191
     * This method does query the topic for all information and completes it to
192
     * build up a full NAP data structure
193
     *
194
     * It determines the URL_NAME of the topic automatically using the name of the
195
     * topic in question.
196
     *
197
     * The currently active leaf is only queried if and only if the currently
198
     * processed topic is equal to the current context's content topic. This should
199
     * prevent dynamically loaded components from disrupting active leaf information,
200
     * as this can happen if dynamic_load is called before showing the navigation.
201
     *
202
     * @param mixed $topic Topic object or ID to be processed
203
     * @return bool Indicating success
204
     */
205 344
    private function load_node($topic) : bool
206
    {
207 344
        if (is_a($topic, midcom_db_topic::class)) {
208 293
            $id = $topic->id;
209
        } else {
210 332
            $id = $topic;
211
        }
212 344
        if (!array_key_exists($id, self::$_nodes)) {
213 122
            $node = new midcom_helper_nav_node($topic);
214 122
            if (!$node->is_visible()) {
215
                return false;
216
            }
217
218 118
            if ($node->id == $this->_root) {
219 60
                $node->nodeid = -1;
220 60
                $node->relativeurl = '';
221 60
                $node->url = '';
222
            } else {
223 68
                if (!$node->nodeid || !$this->load_node($node->nodeid)) {
224 50
                    return false;
225
                }
226 19
                $node->relativeurl = self::$_nodes[$node->nodeid]->relativeurl . $node->url;
227
            }
228
            // Rewrite all host dependent URLs based on the relative URL within our topic tree.
229 78
            $node->fullurl = midcom::get()->config->get('midcom_site_url') . $node->relativeurl;
230 78
            $node->absoluteurl = midcom_connection::get_url('self') . $node->relativeurl;
231 78
            $node->permalink = midcom::get()->permalinks->create_permalink($node->guid);
232
233
            // The node is visible, add it to the list.
234 78
            self::$_nodes[$id] = $node;
235
        } else {
236 307
            $node = self::$_nodes[$id];
237
        }
238
        // Set the current leaf, this does *not* load the leaves from the DB, this is done during get_leaf.
239 307
        if ($node->id === $this->_current) {
240 300
            $currentleaf = midcom_baseclasses_components_configuration::get($node->component, 'active_leaf');
241 300
            if ($currentleaf !== false) {
242 31
                $this->_currentleaf = "{$node->id}-{$currentleaf}";
243
            }
244
        }
245
246 307
        return true;
247
    }
248
249
    /**
250
     * Return the list of leaves for a given node. This helper will construct complete leaf
251
     * data structures for each leaf found. It will first check the cache for the leaf structures,
252
     * and query the database only if the corresponding objects have not been found there.
253
     *
254
     * @param midcom_helper_nav_node $node The NAP node data structure to load the nodes for.
255
     */
256 54
    private function load_leaves(midcom_helper_nav_node $node)
257
    {
258 54
        if (array_key_exists($node->id, $this->_loaded_leaves)) {
259 25
            return;
260
        }
261 45
        $this->_loaded_leaves[$node->id] = [];
262
263 45
        $fullprefix = midcom::get()->config->get('midcom_site_url');
264 45
        $absoluteprefix = midcom_connection::get_url('self');
265
266 45
        foreach ($node->get_leaves() as $id => $leaf) {
267 32
            if (!$leaf->is_visible()) {
268
                continue;
269
            }
270
271
            // Rewrite all host-dependent URLs based on the relative URL within our topic tree.
272 32
            $leaf->fullurl = $fullprefix . $leaf->relativeurl;
273 32
            $leaf->absoluteurl = $absoluteprefix . $leaf->relativeurl;
274
275 32
            if ($leaf->guid === null) {
276 10
                $leaf->permalink = $leaf->fullurl;
277
            } else {
278 22
                $leaf->permalink = midcom::get()->permalinks->create_permalink($leaf->guid);
279
            }
280
281 32
            $this->_leaves[$id] = $leaf;
282 32
            $this->_loaded_leaves[$node->id][$id] =& $this->_leaves[$id];
283
        }
284 45
    }
285
286
    /**
287
     * Verifies the existence of a given leaf. Call this before getting a leaf from the
288
     * $_leaves cache. It will load all necessary nodes/leaves as necessary.
289
     *
290
     * @param string $leaf_id A valid NAP leaf id ($nodeid-$leafid pattern).
291
     */
292 45
    private function load_leaf($leaf_id) : bool
293
    {
294 45
        if (!$leaf_id) {
295
            debug_add("Tried to load a suspicious leaf id, probably a false from get_current_leaf.");
296
            return false;
297
        }
298
299 45
        if (array_key_exists($leaf_id, $this->_leaves)) {
300 2
            return true;
301
        }
302
303 45
        $node_id = explode('-', $leaf_id)[0];
304
305 45
        if (!$this->load_node($node_id)) {
306
            debug_add("Tried to verify the leaf id {$leaf_id}, which should belong to node {$node_id}, but this node cannot be loaded, see debug level log for details.",
307
            MIDCOM_LOG_INFO);
308
            return false;
309
        }
310 45
        $this->load_leaves(self::$_nodes[$node_id]);
311
312 45
        return array_key_exists($leaf_id, $this->_leaves);
313
    }
314
315
    /**
316
     * Lists all Sub-nodes of $parent_node. If there are no subnodes, or if there was an error
317
     * (for instance an unknown parent node ID) you will get an empty array
318
     *
319
     * @param mixed $parent_node    The ID of the node of which the subnodes are searched.
320
     * @param boolean $show_noentry Show all objects on-site which have the noentry flag set.
321
     */
322 22
    public function list_nodes($parent_node, $show_noentry) : array
323
    {
324 22
        static $listed = [];
325
326 22
        if (!$this->load_node($parent_node)) {
327
            debug_add("Unable to load parent node $parent_node", MIDCOM_LOG_ERROR);
328
            return [];
329
        }
330
331 22
        $cache_identifier = $parent_node . (($show_noentry) ? 'noentry' : '');
332 22
        if (isset($listed[$cache_identifier])) {
333 13
            return $listed[$cache_identifier];
334
        }
335
336 11
        $subnodes = self::$_nodes[$parent_node]->get_subnodes();
337
338
        // No results, return an empty array
339 11
        if (empty($subnodes)) {
340 10
            $listed[$cache_identifier] = [];
341 10
            return $listed[$cache_identifier];
342
        }
343
344 1
        $result = [];
345
346 1
        foreach ($subnodes as $id) {
347 1
            if (!$this->load_node($id)) {
348
                continue;
349
            }
350
351 1
            if (   !$show_noentry
352 1
                && self::$_nodes[$id]->noentry) {
353
                // Hide "noentry" items
354
                continue;
355
            }
356
357 1
            $result[] = $id;
358
        }
359
360 1
        $listed[$cache_identifier] = $result;
361 1
        return $listed[$cache_identifier];
362
    }
363
364
    /**
365
     * Lists all leaves of $parent_node. If there are no leaves, or if there was an error
366
     * (for instance an unknown parent node ID) you will get an empty array,
367
     *
368
     * @param mixed $parent_node    The ID of the node of which the leaves are searched.
369
     * @param boolean $show_noentry Show all objects on-site which have the noentry flag set.
370
     */
371 20
    public function list_leaves($parent_node, $show_noentry) : array
372
    {
373 20
        static $listed = [];
374
375 20
        if (!$this->load_node($parent_node)) {
376
            return [];
377
        }
378 20
        $cache_key = $parent_node . '--' . $show_noentry;
379
380 20
        if (!isset($listed[$cache_key])) {
381 10
            $listed[$cache_key] = [];
382 10
            $this->load_leaves(self::$_nodes[$parent_node]);
383
384 10
            foreach ($this->_loaded_leaves[self::$_nodes[$parent_node]->id] as $id => $leaf) {
385 1
                if ($show_noentry || !$leaf->noentry) {
386 1
                    $listed[$cache_key][] = $id;
387
                }
388
            }
389
        }
390
391 20
        return $listed[$cache_key];
392
    }
393
394
    /**
395
     * This is a helper function used by midcom_helper_nav::resolve_guid(). It
396
     * checks if the object denoted by the passed GUID is already loaded into
397
     * memory and returns it, if available. This should speed up GUID lookup heavy
398
     * code.
399
     *
400
     * @param string $guid The GUID to look up in the NAP cache.
401
     * @return Array A NAP structure if the GUID is known, null otherwise.
402
     */
403 28
    public function get_loaded_object_by_guid($guid)
404
    {
405 28
        $entry = $this->_nap_cache->get_guid($guid);
406 28
        if (empty($entry)) {
407 20
            return null;
408
        }
409 11
        if ($entry[MIDCOM_NAV_TYPE] == 'leaf') {
410
            return $this->get_leaf($entry[MIDCOM_NAV_ID]);
411
        }
412 11
        return $this->get_node($entry[MIDCOM_NAV_ID]);
413
    }
414
415
    /**
416
     * This will give you a key-value pair describing the node with the ID
417
     * $node_id. The defined keys are described above in Node data interchange
418
     * format. You will get false if the node ID is invalid.
419
     *
420
     * @param mixed $node_id    The node ID to be retrieved.
421
     * @return Array        The node data as outlined in the class introduction, false on failure
422
     */
423 315
    public function get_node($node_id)
424
    {
425 315
        $node = $node_id;
426 315
        if (!empty($node->guid)) {
427
            $node_id = $node->id;
428
        }
429 315
        if (!$this->load_node($node_id)) {
430 45
            return false;
431
        }
432
433 282
        return self::$_nodes[$node_id]->get_data();
434
    }
435
436
    /**
437
     * This will give you a key-value pair describing the leaf with the ID
438
     * $node_id. The defined keys are described above in leaf data interchange
439
     * format. You will get false if the leaf ID is invalid.
440
     *
441
     * @param string $leaf_id    The leaf-id to be retrieved.
442
     * @return Array        The leaf-data as outlined in the class introduction, false on failure
443
     */
444 45
    public function get_leaf($leaf_id)
445
    {
446 45
        if (!$this->load_leaf($leaf_id)) {
447 33
            debug_add("This leaf is unknown, aborting.", MIDCOM_LOG_INFO);
448 33
            return false;
449
        }
450
451 29
        return $this->_leaves[$leaf_id]->get_data();
452
    }
453
454
    /**
455
     * Retrieve the ID of the currently displayed node. Defined by the topic of
456
     * the component that declared able to handle the request.
457
     *
458
     * @return mixed    The ID of the node in question.
459
     */
460 245
    public function get_current_node()
461
    {
462 245
        return $this->_current;
463
    }
464
465
    /**
466
     * Retrieve the ID of the currently displayed leaf. This is a leaf that is
467
     * displayed by the handling topic. If no leaf is active, this function
468
     * returns false. (Remember to make a type sensitive check, e.g.
469
     * nav::get_current_leaf() !== false to distinguish "0" and "false".)
470
     *
471
     * @return string    The ID of the leaf in question or false on failure.
472
     */
473 268
    public function get_current_leaf()
474
    {
475 268
        return $this->_currentleaf;
476
    }
477
478
    /**
479
     * Retrieve the ID of the upper node of the currently displayed node.
480
     *
481
     * @return mixed    The ID of the node in question.
482
     */
483 2
    public function get_current_upper_node()
484
    {
485 2
        if (count($this->_node_path) > 1) {
486 1
            return $this->_node_path[count($this->_node_path) - 2];
487
        }
488 1
        return $this->_node_path[0];
489
    }
490
491
    /**
492
     * Retrieve the ID of the root node. Note that this ID is dependent from the
493
     * ID of the MidCOM Root topic and therefore will change as easily as the
494
     * root topic ID might. The MIDCOM_NAV_URL entry of the root node's data will
495
     * always be empty.
496
     */
497 110
    public function get_root_node() : int
498
    {
499 110
        return $this->_root;
500
    }
501
502
    /**
503
     * Retrieve the IDs of the nodes from the URL. First value at key 0 is
504
     * the root node ID, possible second value is the first subnode ID etc.
505
     * Contains only visible nodes (nodes which can be loaded).
506
     */
507 14
    public function get_node_path() : array
508
    {
509 14
        return $this->_node_path;
510
    }
511
512
    /**
513
     * Returns the ID of the node to which $leaf_id is associated to, false
514
     * on failure.
515
     *
516
     * @param string $leaf_id    The Leaf-ID to search an uplink for.
517
     * @return mixed             The ID of the Node for which we have a match, or false on failure.
518
     */
519 1
    function get_leaf_uplink($leaf_id)
520
    {
521 1
        if (!$this->load_leaf($leaf_id)) {
522
            debug_add("This leaf is unknown, aborting.", MIDCOM_LOG_ERROR);
523
            return false;
524
        }
525
526 1
        return $this->_leaves[$leaf_id]->nodeid;
527
    }
528
529
    /**
530
     * Returns the ID of the node to which $node_id is associated to, false
531
     * on failure. The root node's uplink is -1.
532
     *
533
     * @param mixed $node_id    The node ID to search an uplink for.
534
     * @return mixed             The ID of the node for which we have a match, -1 for the root node, or false on failure.
535
     */
536 88
    public function get_node_uplink($node_id)
537
    {
538 88
        if (!$this->load_node($node_id)) {
539 33
            return false;
540
        }
541
542 55
        return self::$_nodes[$node_id]->nodeid;
543
    }
544
}
545