Proyectos de Subversion Moodle

Rev

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