Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core\navigation\output;
18
 
19
use renderable;
20
use renderer_base;
21
use templatable;
22
use custom_menu;
23
 
24
/**
25
 * Primary navigation renderable
26
 *
27
 * This file combines primary nav, custom menu, lang menu and
28
 * usermenu into a standardized format for the frontend
29
 *
30
 * @package     core
31
 * @category    navigation
32
 * @copyright   2021 onwards Peter Dias
33
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34
 */
35
class primary implements renderable, templatable {
36
    /** @var \moodle_page $page the moodle page that the navigation belongs to */
37
    private $page = null;
38
 
39
    /**
40
     * primary constructor.
41
     * @param \moodle_page $page
42
     */
43
    public function __construct($page) {
44
        $this->page = $page;
45
    }
46
 
47
    /**
48
     * Combine the various menus into a standardized output.
49
     *
50
     * @param renderer_base|null $output
51
     * @return array
52
     */
53
    public function export_for_template(?renderer_base $output = null): array {
54
        if (!$output) {
55
            $output = $this->page->get_renderer('core');
56
        }
57
 
58
        $menudata = (object) $this->merge_primary_and_custom($this->get_primary_nav(), $this->get_custom_menu($output));
59
        $moremenu = new \core\navigation\output\more_menu($menudata, 'navbar-nav', false);
60
        $mobileprimarynav = $this->merge_primary_and_custom($this->get_primary_nav(), $this->get_custom_menu($output), true);
61
 
62
        $languagemenu = new \core\output\language_menu($this->page);
63
 
64
        return [
65
            'mobileprimarynav' => $mobileprimarynav,
66
            'moremenu' => $moremenu->export_for_template($output),
67
            'lang' => !isloggedin() || isguestuser() ? $languagemenu->export_for_template($output) : [],
68
            'user' => $this->get_user_menu($output),
69
        ];
70
    }
71
 
72
    /**
73
     * Get the primary nav object and standardize the output
74
     *
75
     * @param \navigation_node|null $parent used for nested nodes, by default the primarynav node
76
     * @return array
77
     */
78
    protected function get_primary_nav($parent = null): array {
79
        if ($parent === null) {
80
            $parent = $this->page->primarynav;
81
        }
82
        $nodes = [];
83
        foreach ($parent->children as $node) {
84
            $children = $this->get_primary_nav($node);
85
            $activechildren = array_filter($children, function($child) {
86
                return !empty($child['isactive']);
87
            });
88
            if ($node->preceedwithhr && count($nodes) && empty($nodes[count($nodes) - 1]['divider'])) {
89
                $nodes[] = ['divider' => true];
90
            }
91
            $nodes[] = [
92
                'title' => $node->get_title(),
93
                'url' => $node->action(),
94
                'text' => $node->text,
95
                'icon' => $node->icon,
96
                'isactive' => $node->isactive || !empty($activechildren),
97
                'key' => $node->key,
98
                'children' => $children,
99
                'haschildren' => !empty($children) ? 1 : 0,
100
            ];
101
        }
102
 
103
        return $nodes;
104
    }
105
 
106
    /**
107
     * Custom menu items reside on the same level as the original nodes.
108
     * Fetch and convert the nodes to a standardised array.
109
     *
110
     * @param renderer_base $output
111
     * @return array
112
     */
113
    protected function get_custom_menu(renderer_base $output): array {
114
        global $CFG;
115
 
116
        // Early return if a custom menu does not exists.
117
        if (empty($CFG->custommenuitems)) {
118
            return [];
119
        }
120
 
121
        $custommenuitems = $CFG->custommenuitems;
122
        $currentlang = current_language();
123
        $custommenunodes = custom_menu::convert_text_to_menu_nodes($custommenuitems, $currentlang);
124
        $nodes = [];
125
        foreach ($custommenunodes as $node) {
126
            $nodes[] = $node->export_for_template($output);
127
        }
128
 
129
        return $nodes;
130
    }
131
 
132
    /**
133
     * When defining custom menu items, the active flag is not obvserved correctly. Therefore, the merge of the primary
134
     * and custom navigation must be handled a bit smarter. Change the "isactive" flag of the nodes (this may set by
135
     * default in the primary nav nodes but is entirely missing in the custom nav nodes).
136
     * Set the $expandedmenu argument to true when the menu for the mobile template is build.
137
     *
138
     * @param array $primary
139
     * @param array $custom
140
     * @param bool $expandedmenu
141
     * @return array
142
     */
143
    protected function merge_primary_and_custom(array $primary, array $custom, bool $expandedmenu = false): array {
144
        if (empty($custom)) {
145
            return $primary; // No custom nav, nothing to merge.
146
        }
147
        // Remember the amount of primary nodes and whether we changed the active flag in the custom menu nodes.
148
        $primarylen = count($primary);
149
        $changed = false;
150
        foreach (array_keys($custom) as $i) {
151
            if (!$changed) {
152
                if ($this->flag_active_nodes($custom[$i], $expandedmenu)) {
153
                    $changed = true;
154
                }
155
            }
156
            $primary[] = $custom[$i];
157
        }
158
        // In case some custom node is active, mark all primary nav elements as inactive.
159
        if ($changed) {
160
            for ($i = 0; $i < $primarylen; $i++) {
161
                $primary[$i]['isactive'] = false;
162
            }
163
        }
164
        return $primary;
165
    }
166
 
167
    /**
168
     * Recursive checks if any of the children is active. If that's the case this node (the parent) is active as
169
     * well. If the node has no children, check if the node itself is active. Use pass by reference for the node
170
     * object because we actively change/set the "isactive" flag inside the method and this needs to be kept at the
171
     * callers side.
172
     * Set $expandedmenu to true, if the mobile menu is done, in this case the active flag gets the node that is
173
     * actually active, while the parent hierarchy of the active node gets the flag isopen.
174
     *
175
     * @param object $node
176
     * @param bool $expandedmenu
177
     * @return bool
178
     */
179
    protected function flag_active_nodes(object $node, bool $expandedmenu = false): bool {
180
        global $FULLME;
181
        $active = false;
182
        foreach (array_keys($node->children ?? []) as $c) {
183
            if ($this->flag_active_nodes($node->children[$c], $expandedmenu)) {
184
                $active = true;
185
            }
186
        }
187
        // One of the children is active, so this node (the parent) is active as well.
188
        if ($active) {
189
            if ($expandedmenu) {
190
                $node->isopen = true;
191
            } else {
192
                $node->isactive = true;
193
            }
194
            return true;
195
        }
196
 
197
        // By default, the menu item node to check is not active.
198
        $node->isactive = false;
199
 
200
        // Check if the node url matches the called url. The node url may omit the trailing index.php, therefore check
201
        // this as well.
202
        if (empty($node->url)) {
203
            // Current menu node has no url set, so it can't be active.
204
            return false;
205
        }
206
        $nodeurl = parse_url($node->url);
207
        $current = parse_url($FULLME ?? '');
208
 
209
        $pathmatches = false;
210
 
211
        // Exact match of the path of node and current url.
212
        $nodepath = $nodeurl['path'] ?? '/';
213
        $currentpath = $current['path'] ?? '/';
214
        if ($nodepath === $currentpath) {
215
            $pathmatches = true;
216
        }
217
        // The current url may be trailed by a index.php, otherwise it's the same as the node path.
218
        if (!$pathmatches && $nodepath . 'index.php' === $currentpath) {
219
            $pathmatches = true;
220
        }
221
        // No path did match, so the node can't be active.
222
        if (!$pathmatches) {
223
            return false;
224
        }
225
        // We are here because the path matches, so now look at the query string.
226
        $nodequery = $nodeurl['query'] ?? '';
227
        $currentquery = $current['query'] ?? '';
228
        // If the node has no query string defined, then the patch match is sufficient.
229
        if (empty($nodeurl['query'])) {
230
            $node->isactive = true;
231
            return true;
232
        }
233
        // If the node contains a query string then also the current url must match this query.
234
        if ($nodequery === $currentquery) {
235
            $node->isactive = true;
236
        }
237
        return $node->isactive;
238
    }
239
 
240
    /**
241
     * Get/Generate the user menu.
242
     *
243
     * This is leveraging the data from user_get_user_navigation_info and the logic in $OUTPUT->user_menu()
244
     *
245
     * @param renderer_base $output
246
     * @return array
247
     */
248
    public function get_user_menu(renderer_base $output): array {
249
        global $CFG, $USER, $PAGE;
250
        require_once($CFG->dirroot . '/user/lib.php');
251
 
252
        $usermenudata = [];
253
        $submenusdata = [];
254
        $info = user_get_user_navigation_info($USER, $PAGE);
255
        if (isset($info->unauthenticateduser)) {
256
            $info->unauthenticateduser['content'] = get_string($info->unauthenticateduser['content']);
257
            $info->unauthenticateduser['url'] = get_login_url();
258
            return (array) $info;
259
        }
260
        // Gather all the avatar data to be displayed in the user menu.
261
        $usermenudata['avatardata'][] = [
262
            'content' => $info->metadata['useravatar'],
263
            'classes' => 'current'
264
        ];
265
        $usermenudata['userfullname'] = $info->metadata['realuserfullname'] ?? $info->metadata['userfullname'];
266
 
267
        // Logged in as someone else.
268
        if ($info->metadata['asotheruser']) {
269
            $usermenudata['avatardata'][] = [
270
                'content' => $info->metadata['realuseravatar'],
271
                'classes' => 'realuser'
272
            ];
273
            $usermenudata['metadata'][] = [
274
                'content' => get_string('loggedinas', 'moodle', $info->metadata['userfullname']),
275
                'classes' => 'viewingas'
276
            ];
277
        }
278
 
279
        // Gather all the meta data to be displayed in the user menu.
280
        $metadata = [
281
            'asotherrole' => [
282
                'value' => 'rolename',
283
                'class' => 'role role-##GENERATEDCLASS##',
284
            ],
285
            'userloginfail' => [
286
                'value' => 'userloginfail',
287
                'class' => 'loginfailures',
288
            ],
289
            'asmnetuser' => [
290
                'value' => 'mnetidprovidername',
291
                'class' => 'mnet mnet-##GENERATEDCLASS##',
292
            ],
293
        ];
294
        foreach ($metadata as $key => $value) {
295
            if (!empty($info->metadata[$key])) {
296
                $content = $info->metadata[$value['value']] ?? '';
297
                $generatedclass = strtolower(preg_replace('#[ ]+#', '-', trim($content)));
298
                $customclass = str_replace('##GENERATEDCLASS##', $generatedclass, ($value['class'] ?? ''));
299
                $usermenudata['metadata'][] = [
300
                    'content' => $content,
301
                    'classes' => $customclass
302
                ];
303
            }
304
        }
305
 
306
        $modifiedarray = array_map(function($value) {
307
            $value->divider = $value->itemtype == 'divider';
308
            $value->link = $value->itemtype == 'link';
309
            if (isset($value->pix) && !empty($value->pix)) {
310
                $value->pixicon = $value->pix;
311
                unset($value->pix);
312
            }
313
            return $value;
314
        }, $info->navitems);
315
 
316
        // Include the language menu as a submenu within the user menu.
317
        $languagemenu = new \core\output\language_menu($this->page);
318
        $langmenu = $languagemenu->export_for_template($output);
319
        if (!empty($langmenu)) {
320
            $languageitems = $langmenu['items'];
321
            // If there are available languages, generate the data for the the language selector submenu.
322
            if (!empty($languageitems)) {
323
                $langsubmenuid = uniqid();
324
                // Generate the data for the link to language selector submenu.
325
                $language = (object) [
326
                    'itemtype' => 'submenu-link',
327
                    'submenuid' => $langsubmenuid,
328
                    'title' => get_string('language'),
329
                    'divider' => false,
330
                    'submenulink' => true,
331
                ];
332
 
333
                // Place the link before the 'Log out' menu item which is either the last item in the menu or
334
                // second to last when 'Switch roles' is available.
335
                $menuposition = count($modifiedarray) - 1;
336
                if (has_capability('moodle/role:switchroles', $PAGE->context)) {
337
                    $menuposition = count($modifiedarray) - 2;
338
                }
339
                array_splice($modifiedarray, $menuposition, 0, [$language]);
340
 
341
                // Generate the data for the language selector submenu.
342
                $submenusdata[] = (object)[
343
                    'id' => $langsubmenuid,
344
                    'title' => get_string('languageselector'),
345
                    'items' => $languageitems,
346
                ];
347
            }
348
        }
349
 
350
        // Add divider before the last item.
351
        $modifiedarray[count($modifiedarray) - 2]->divider = true;
352
        $usermenudata['items'] = $modifiedarray;
353
        $usermenudata['submenus'] = array_values($submenusdata);
354
 
355
        return $usermenudata;
356
    }
357
}