Completed
Push — master ( 08b78b...4c6769 )
by Andreas
14:04
created

midcom_helper_nav_backend::init_topics()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4.074

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 6
nop 2
dl 0
loc 17
ccs 10
cts 12
cp 0.8333
crap 4.074
rs 9.9
c 0
b 0
f 0
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
     * User id for ACL checks. This is set when instantiating to avoid unnecessary overhead
136
     *
137
     * @var string
138
     */
139
    private $_user_id = false;
140
141
    /**
142
     * Constructor
143
     *
144
     * It will initialize Root Topic, Current Topic and all cache arrays.
145
     * The constructor retrieves all initialization data from the component context.
146
     *
147
     * @param midcom_db_topic $root
148
     * @param midcom_db_topic[] $urltopics
149
     */
150 87
    public function __construct(midcom_db_topic $root, array $urltopics)
151
    {
152 87
        $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...
153
154 87
        if (!midcom::get()->auth->admin) {
155 86
            $this->_user_id = midcom::get()->auth->acl->get_user_id();
156
        }
157
158 87
        $this->_root = $root->id;
159 87
        $this->_current = $this->_root;
160
161 87
        if (empty($root->id)) {
162
            $this->_loadNodeData($root);
163
        } else {
164 87
            $this->init_topics($root, $urltopics);
165
        }
166 87
    }
167
168
    /**
169
     * Loads all nodes between root and current node.
170
     *
171
     * If the current node is behind an invisible or undescendable node, the last
172
     * known good node will be used instead for the current node.
173
     *
174
     * @param midcom_db_topic $root
175
     * @param midcom_db_topic[] $urltopics
176
     */
177 87
    private function init_topics(midcom_db_topic $root, array $urltopics)
178
    {
179 87
        $node_path_candidates = [$root];
180 87
        foreach ($urltopics as $topic) {
181 1
            $node_path_candidates[] = $topic;
182 1
            $this->_current = $topic->id;
183
        }
184
185 87
        $lastgood = null;
186 87
        foreach ($node_path_candidates as $topic) {
187 87
            if ($this->_loadNodeData($topic) !== MIDCOM_ERROK) {
188
                // Node is hidden behind an undescendable one
189
                $this->_current = $lastgood;
190
                return;
191
            }
192 87
            $this->_node_path[] = $topic->id;
193 87
            $lastgood = $topic->id;
194
        }
195 87
    }
196
197
    /**
198
     * This function is the controlling instance of the loading mechanism. It
199
     * is able to load the navigation data of any topic within MidCOM's topic
200
     * tree into memory. Any uplink nodes that are not loaded into memory will
201
     * be loaded until any other known topic is encountered. After the
202
     * necessary data has been loaded with calls to _loadNodeData.
203
     *
204
     * If all load calls were successful, MIDCOM_ERROK is returned. Any error
205
     * will be indicated with a corresponding return value.
206
     *
207
     * @param mixed $node_id  The node ID of the node to be loaded
208
     * @param int $parent_id  The node's parent ID, if known
209
     * @return int            MIDCOM_ERROK on success, MIDCOM_ERRFORBIDDEN when inaccessible
210
     */
211 157
    private function _loadNode($node_id, $parent_id = null)
212
    {
213
        // Check if we have a cached version of the node already
214 157
        if (isset(self::$_nodes[$node_id])) {
215 110
            return MIDCOM_ERROK;
216
        }
217
218 77
        $topic_id = (int) $node_id;
219
220 77
        if ($parent_id === null) {
221
            // Load parent nodes also to cache
222 77
            $parent_id = $this->_get_parent_id($topic_id);
223
        }
224
225 77
        while ((int) $parent_id > 0) {
226 21
            $stat = $this->_loadNodeData($parent_id);
227 21
            if ($stat == MIDCOM_ERRFORBIDDEN) {
228 1
                debug_add("The Node {$parent_id} is invisible, could not satisfy the dependency chain to Node #{$node_id}", MIDCOM_LOG_WARN);
229 1
                return $stat;
230
            }
231 20
            $parent_id = self::$_nodes[$parent_id]->nodeid;
232
        }
233 77
        return $this->_loadNodeData($topic_id);
234
    }
235
236
    /**
237
     * Load the navigational information associated with the topic $param, which
238
     * can be passed as an ID or as a MidgardTopic object.
239
     *
240
     * This method does query the topic for all information and completes it to
241
     * build up a full NAP data structure
242
     *
243
     * It determines the URL_NAME of the topic automatically using the name of the
244
     * topic in question.
245
     *
246
     * The currently active leaf is only queried if and only if the currently
247
     * processed topic is equal to the current context's content topic. This should
248
     * prevent dynamically loaded components from disrupting active leaf information,
249
     * as this can happen if dynamic_load is called before showing the navigation.
250
     *
251
     * @param mixed $topic Topic object or ID to be processed
252
     * @return integer MIDCOM_ERROK on success, MIDCOM_ERRFORBIDDEN when inaccessible
253
     */
254 141
    private function _loadNodeData($topic)
255
    {
256 141
        if (is_a($topic, midcom_db_topic::class)) {
257 87
            $id = $topic->id;
258
        } else {
259 77
            $id = $topic;
260
        }
261 141
        if (!array_key_exists($id, self::$_nodes)) {
262 91
            $node = new midcom_helper_nav_node($this, $topic);
263
264 91
            if (    !$node->is_object_visible()
265 87
                || !$node->is_readable_by($this->_user_id)) {
266 52
                return MIDCOM_ERRFORBIDDEN;
267
            }
268
269
            // The node is visible, add it to the list.
270 44
            self::$_nodes[$id] = $node;
271
        } else {
272 78
            $node = self::$_nodes[$id];
273
        }
274
        // Set the current leaf, this does *not* load the leaves from the DB, this is done during get_leaf.
275 100
        if ($node->id === $this->_current) {
276 100
            $currentleaf = midcom_baseclasses_components_configuration::get($node->component, 'active_leaf');
277 100
            if ($currentleaf !== false) {
278 8
                $this->_currentleaf = "{$node->id}-{$currentleaf}";
279
            }
280
        }
281
282 100
        return MIDCOM_ERROK;
283
    }
284
285
    /**
286
     * Loads the leaves for a given node from the cache or database.
287
     * It will relay the code to _get_leaves() and check the object visibility upon
288
     * return.
289
     *
290
     * @param midcom_helper_nav_node $node The NAP node data structure to load the nodes for.
291
     */
292 43
    private function _load_leaves(midcom_helper_nav_node $node)
293
    {
294 43
        if (!array_key_exists($node->id, $this->_loaded_leaves)) {
295 25
            $this->_loaded_leaves[$node->id] = [];
296
297 25
            $leaves = array_filter($this->_get_leaves($node), function($leaf) {
298 10
                return $leaf->is_object_visible();
299 25
            });
300 25
            foreach ($leaves as $id => $leaf) {
301 10
                $this->_leaves[$id] = $leaf;
302 10
                $this->_loaded_leaves[$node->id][$id] =& $this->_leaves[$id];
303
            }
304
        }
305 43
    }
306
307
    /**
308
     * Return the list of leaves for a given node. This helper will construct complete leaf
309
     * data structures for each leaf found. It will first check the cache for the leaf structures,
310
     * and query the database only if the corresponding objects have not been found there.
311
     *
312
     * No visibility checks are made at this point.
313
     *
314
     * @param midcom_helper_nav_node $node The node data structure for which to retrieve the leaves.
315
     * @return Array All leaves found for that node, in complete post processed leave data structures.
316
     */
317 25
    private function _get_leaves(midcom_helper_nav_node $node)
318
    {
319 25
        $fullprefix = midcom::get()->config->get('midcom_site_url');
320 25
        $absoluteprefix = midcom_connection::get_url('self');
321 25
        $result = [];
322
323 25
        foreach ($node->get_leaves() as $id => $leaf) {
324 10
            if (!$leaf->is_readable_by($this->_user_id)) {
325
                continue;
326
            }
327
            // Rewrite all host-dependent URLs based on the relative URL within our topic tree.
328 10
            $leaf->fullurl = $fullprefix . $leaf->relativeurl;
329 10
            $leaf->absoluteurl = $absoluteprefix . $leaf->relativeurl;
330
331 10
            if ($leaf->guid === null) {
332
                $leaf->permalink = $leaf->fullurl;
333
            } else {
334 10
                $leaf->permalink = midcom::get()->permalinks->create_permalink($leaf->guid);
335
            }
336
337 10
            $result[$id] = $leaf;
338
        }
339
340 25
        return $result;
341
    }
342
343
    /**
344
     * Lists all Sub-nodes of $parent_node. If there are no subnodes, or if there was an error
345
     * (for instance an unknown parent node ID) you will get an empty array
346
     *
347
     * @param mixed $parent_node    The ID of the node of which the subnodes are searched.
348
     * @param boolean $show_noentry Show all objects on-site which have the noentry flag set.
349
     * @return Array            An array of node IDs or false on failure.
350
     */
351 13
    public function list_nodes($parent_node, $show_noentry)
352
    {
353 13
        static $listed = [];
354
355 13
        if ($this->_loadNode($parent_node) !== MIDCOM_ERROK) {
356
            debug_add("Unable to load parent node $parent_node", MIDCOM_LOG_ERROR);
357
            return [];
358
        }
359
360 13
        $cache_identifier = $parent_node . (($show_noentry) ? 'noentry' : '');
361 13
        if (isset($listed[$cache_identifier])) {
362 4
            return $listed[$cache_identifier];
363
        }
364
365 9
        $subnodes = self::$_nodes[$parent_node]->get_subnodes();
366
367
        // No results, return an empty array
368 9
        if (empty($subnodes)) {
369 8
            $listed[$cache_identifier] = [];
370 8
            return $listed[$cache_identifier];
371
        }
372
373 1
        $result = [];
374
375 1
        foreach ($subnodes as $id) {
376 1
            if ($this->_loadNode($id, $parent_node) !== MIDCOM_ERROK) {
377
                continue;
378
            }
379
380 1
            if (   !$show_noentry
381 1
                && self::$_nodes[$id]->noentry) {
382
                // Hide "noentry" items
383
                continue;
384
            }
385
386 1
            $result[] = $id;
387
        }
388
389 1
        $listed[$cache_identifier] = $result;
390 1
        return $listed[$cache_identifier];
391
    }
392
393
    /**
394
     * Lists all leaves of $parent_node. If there are no leaves, or if there was an error
395
     * (for instance an unknown parent node ID) you will get an empty array,
396
     *
397
     * @param mixed $parent_node    The ID of the node of which the leaves are searched.
398
     * @param boolean $show_noentry Show all objects on-site which have the noentry flag set.
399
     * @return Array             A list of leaves found, or false on failure.
400
     */
401 10
    public function list_leaves($parent_node, $show_noentry)
402
    {
403 10
        static $listed = [];
404
405 10
        if ($this->_loadNode($parent_node) !== MIDCOM_ERROK) {
406
            return [];
407
        }
408 10
        $cache_key = $parent_node . '--' . $show_noentry;
409
410 10
        if (!isset($listed[$cache_key])) {
411 7
            $listed[$cache_key] = [];
412 7
            $this->_load_leaves(self::$_nodes[$parent_node]);
413
414 7
            foreach ($this->_loaded_leaves[self::$_nodes[$parent_node]->id] as $id => $leaf) {
415 1
                if ($show_noentry || !$leaf->noentry) {
416 1
                    $listed[$cache_key][] = $id;
417
                }
418
            }
419
        }
420
421 10
        return $listed[$cache_key];
422
    }
423
424
    /**
425
     * This is a helper function used by midcom_helper_nav::resolve_guid(). It
426
     * checks if the object denoted by the passed GUID is already loaded into
427
     * memory and returns it, if available. This should speed up GUID lookup heavy
428
     * code.
429
     *
430
     * @param string $guid The GUID to look up in the NAP cache.
431
     * @return Array A NAP structure if the GUID is known, null otherwise.
432
     */
433 26
    public function get_loaded_object_by_guid($guid)
434
    {
435 26
        $entry = $this->_nap_cache->get_guid($guid);
436 26
        if (empty($entry)) {
437 25
            return null;
438
        }
439 4
        if ($entry[MIDCOM_NAV_TYPE] == 'leaf') {
440
            return $this->get_leaf($entry[MIDCOM_NAV_ID]);
441
        }
442 4
        return $this->get_node($entry[MIDCOM_NAV_ID]);
443
    }
444
445
    /**
446
     * This will give you a key-value pair describing the node with the ID
447
     * $node_id. The defined keys are described above in Node data interchange
448
     * format. You will get false if the node ID is invalid.
449
     *
450
     * @param mixed $node_id    The node ID to be retrieved.
451
     * @return Array        The node data as outlined in the class introduction, false on failure
452
     */
453 153
    public function get_node($node_id)
454
    {
455 153
        $node = $node_id;
456 153
        if (!empty($node->guid)) {
457
            $node_id = $node->id;
458
        }
459 153
        if ($this->_loadNode($node_id) != MIDCOM_ERROK) {
460 52
            return false;
461
        }
462
463 106
        return self::$_nodes[$node_id]->get_data();
464
    }
465
466
    /**
467
     * This will give you a key-value pair describing the leaf with the ID
468
     * $node_id. The defined keys are described above in leaf data interchange
469
     * format. You will get false if the leaf ID is invalid.
470
     *
471
     * @param string $leaf_id    The leaf-id to be retrieved.
472
     * @return Array        The leaf-data as outlined in the class introduction, false on failure
473
     */
474 37
    public function get_leaf($leaf_id)
475
    {
476 37
        if (!$this->_check_leaf_id($leaf_id)) {
477 35
            debug_add("This leaf is unknown, aborting.", MIDCOM_LOG_INFO);
478 35
            return false;
479
        }
480
481 2
        return $this->_leaves[$leaf_id]->get_data();
482
    }
483
484
    /**
485
     * Retrieve the ID of the currently displayed node. Defined by the topic of
486
     * the component that declared able to handle the request.
487
     *
488
     * @return mixed    The ID of the node in question.
489
     */
490 15
    public function get_current_node()
491
    {
492 15
        return $this->_current;
493
    }
494
495
    /**
496
     * Retrieve the ID of the currently displayed leaf. This is a leaf that is
497
     * displayed by the handling topic. If no leaf is active, this function
498
     * returns false. (Remember to make a type sensitive check, e.g.
499
     * nav::get_current_leaf() !== false to distinguish "0" and "false".)
500
     *
501
     * @return string    The ID of the leaf in question or false on failure.
502
     */
503 3
    public function get_current_leaf()
504
    {
505 3
        return $this->_currentleaf;
506
    }
507
508
    /**
509
     * Retrieve the ID of the upper node of the currently displayed node.
510
     *
511
     * @return mixed    The ID of the node in question.
512
     */
513 2
    public function get_current_upper_node()
514
    {
515 2
        if (count($this->_node_path) > 1) {
516 1
            return $this->_node_path[count($this->_node_path) - 2];
517
        }
518 1
        return $this->_node_path[0];
519
    }
520
521
    /**
522
     * Retrieve the ID of the root node. Note that this ID is dependent from the
523
     * ID of the MidCOM Root topic and therefore will change as easily as the
524
     * root topic ID might. The MIDCOM_NAV_URL entry of the root node's data will
525
     * always be empty.
526
     *
527
     * @return int    The ID of the root node.
528
     */
529 117
    public function get_root_node()
530
    {
531 117
        return $this->_root;
532
    }
533
534
    /**
535
     * Retrieve the IDs of the nodes from the URL. First value at key 0 is
536
     * the root node ID, possible second value is the first subnode ID etc.
537
     * Contains only visible nodes (nodes which can be loaded).
538
     *
539
     * @return Array    The node path array.
540
     */
541 4
    public function get_node_path()
542
    {
543 4
        return $this->_node_path;
544
    }
545
546
    /**
547
     * Returns the ID of the node to which $leaf_id is associated to, false
548
     * on failure.
549
     *
550
     * @param string $leaf_id    The Leaf-ID to search an uplink for.
551
     * @return mixed             The ID of the Node for which we have a match, or false on failure.
552
     */
553 1
    function get_leaf_uplink($leaf_id)
554
    {
555 1
        if (!$this->_check_leaf_id($leaf_id)) {
556
            debug_add("This leaf is unknown, aborting.", MIDCOM_LOG_ERROR);
557
            return false;
558
        }
559
560 1
        return $this->_leaves[$leaf_id]->nodeid;
561
    }
562
563
    /**
564
     * Returns the ID of the node to which $node_id is associated to, false
565
     * on failure. The root node's uplink is -1.
566
     *
567
     * @param mixed $node_id    The node ID to search an uplink for.
568
     * @return mixed             The ID of the node for which we have a match, -1 for the root node, or false on failure.
569
     */
570 85
    public function get_node_uplink($node_id)
571
    {
572 85
        if ($this->_loadNode($node_id) !== MIDCOM_ERROK) {
573 36
            return false;
574
        }
575
576 49
        return self::$_nodes[$node_id]->nodeid;
577
    }
578
579
    /**
580
     * Verifies the existence of a given leaf. Call this before getting a leaf from the
581
     * $_leaves cache. It will load all necessary nodes/leaves as necessary.
582
     *
583
     * @param string $leaf_id A valid NAP leaf id ($nodeid-$leafid pattern).
584
     * @return boolean true if the leaf exists, false otherwise.
585
     */
586 37
    private function _check_leaf_id($leaf_id)
587
    {
588 37
        if (!$leaf_id) {
589
            debug_add("Tried to load a suspicious leaf id, probably a false from get_current_leaf.");
590
            return false;
591
        }
592
593 37
        if (array_key_exists($leaf_id, $this->_leaves)) {
594 2
            return true;
595
        }
596
597 36
        $node_id = explode('-', $leaf_id)[0];
598
599 36
        if ($this->_loadNode($node_id) !== MIDCOM_ERROK) {
600
            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.",
601
            MIDCOM_LOG_INFO);
602
            return false;
603
        }
604 36
        $this->_load_leaves(self::$_nodes[$node_id]);
605
606 36
        return array_key_exists($leaf_id, $this->_leaves);
607
    }
608
609
    /**
610
     * Determine a topic's parent id without loading the full object
611
     *
612
     * @param integer $topic_id The topic ID
613
     * @return integer The parent ID or false
614
     */
615 77
    private function _get_parent_id($topic_id)
616
    {
617 77
        $mc = midcom_db_topic::new_collector('id', $topic_id);
618 77
        $result = $mc->get_values('up');
619 77
        if (empty($result)) {
620 34
            return false;
621
        }
622 72
        return array_shift($result);
623
    }
624
}
625