<?php

/**
 * @package     Joomla.Administrator
 * @subpackage  com_menus
 *
 * @copyright   (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\Component\Menus\Administrator\Helper;

use Joomla\CMS\Application\ApplicationHelper;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Event\Menu\PreprocessMenuItemsEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Menu\AdministratorMenuItem;
use Joomla\CMS\Table\Table;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\ParameterType;
use Joomla\Filesystem\File;
use Joomla\Registry\Registry;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Menus component helper.
 *
 * @since  1.6
 */
class MenusHelper extends ContentHelper
{
    /**
     * Defines the valid request variables for the reverse lookup.
     *
     * @var     array
     */
    protected static $_filter = ['option', 'view', 'layout'];

    /**
     * List of preset include paths
     *
     * @var  array
     *
     * @since   4.0.0
     */
    protected static $presets = null;

    /**
     * Gets a standard form of a link for lookups.
     *
     * @param   mixed  $request  A link string or array of request variables.
     *
     * @return  mixed  A link in standard option-view-layout form, or false if the supplied response is invalid.
     *
     * @since   1.6
     */
    public static function getLinkKey($request)
    {
        if (empty($request)) {
            return false;
        }

        // Check if the link is in the form of index.php?...
        if (\is_string($request)) {
            $args = [];

            if (strpos($request, 'index.php') === 0) {
                parse_str(parse_url(htmlspecialchars_decode($request), PHP_URL_QUERY), $args);
            } else {
                parse_str($request, $args);
            }

            $request = $args;
        }

        // Only take the option, view and layout parts.
        foreach ($request as $name => $value) {
            if ((!\in_array($name, self::$_filter)) && (!($name == 'task' && !\array_key_exists('view', $request)))) {
                // Remove the variables we want to ignore.
                unset($request[$name]);
            }
        }

        ksort($request);

        return 'index.php?' . http_build_query($request, '', '&');
    }

    /**
     * Get the menu list for create a menu module
     *
     * @param   int  $clientId  Optional client id - viz 0 = site, 1 = administrator, can be NULL for all
     *
     * @return  array  The menu array list
     *
     * @since    1.6
     */
    public static function getMenuTypes($clientId = 0)
    {
        $db    = Factory::getDbo();
        $query = $db->getQuery(true)
            ->select($db->quoteName('a.menutype'))
            ->from($db->quoteName('#__menu_types', 'a'));

        if (isset($clientId)) {
            $clientId = (int) $clientId;
            $query->where($db->quoteName('a.client_id') . ' = :clientId')
                ->bind(':clientId', $clientId, ParameterType::INTEGER);
        }

        $db->setQuery($query);

        return $db->loadColumn();
    }

    /**
     * Get a list of menu links for one or all menus.
     *
     * @param   string   $menuType   An option menu to filter the list on, otherwise all menu with given client id links
     *                               are returned as a grouped array.
     * @param   integer  $parentId   An optional parent ID to pivot results around.
     * @param   integer  $mode       An optional mode. If parent ID is set and mode=2, the parent and children are excluded from the list.
     * @param   array    $published  An optional array of states
     * @param   array    $languages  Optional array of specify which languages we want to filter
     * @param   int      $clientId   Optional client id - viz 0 = site, 1 = administrator, can be NULL for all (used only if menutype not given)
     *
     * @return  array|boolean
     *
     * @since   1.6
     */
    public static function getMenuLinks($menuType = null, $parentId = 0, $mode = 0, $published = [], $languages = [], $clientId = 0)
    {
        $hasClientId = $clientId !== null;
        $clientId    = (int) $clientId;

        $db    = Factory::getDbo();
        $query = $db->getQuery(true)
            ->select(
                [
                    'DISTINCT ' . $db->quoteName('a.id', 'value'),
                    $db->quoteName('a.title', 'text'),
                    $db->quoteName('a.alias'),
                    $db->quoteName('a.level'),
                    $db->quoteName('a.menutype'),
                    $db->quoteName('a.client_id'),
                    $db->quoteName('a.type'),
                    $db->quoteName('a.published'),
                    $db->quoteName('a.template_style_id'),
                    $db->quoteName('a.checked_out'),
                    $db->quoteName('a.language'),
                    $db->quoteName('a.lft'),
                    $db->quoteName('e.name', 'componentname'),
                    $db->quoteName('e.element'),
                ]
            )
            ->from($db->quoteName('#__menu', 'a'))
            ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id'));

        if (Multilanguage::isEnabled()) {
            $query->select(
                [
                    $db->quoteName('l.title', 'language_title'),
                    $db->quoteName('l.image', 'language_image'),
                    $db->quoteName('l.sef', 'language_sef'),
                ]
            )
                ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language'));
        }

        // Filter by the type if given, this is more specific than client id
        if ($menuType) {
            $query->where('(' . $db->quoteName('a.menutype') . ' = :menuType OR ' . $db->quoteName('a.parent_id') . ' = 0)')
                ->bind(':menuType', $menuType);
        } elseif ($hasClientId) {
            $query->where($db->quoteName('a.client_id') . ' = :clientId')
                ->bind(':clientId', $clientId, ParameterType::INTEGER);
        }

        // Prevent the parent and children from showing if requested.
        if ($parentId && $mode == 2) {
            $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :parentId')
                ->where(
                    '(' . $db->quoteName('a.lft') . ' <= ' . $db->quoteName('p.lft')
                    . ' OR ' . $db->quoteName('a.rgt') . ' >= ' . $db->quoteName('p.rgt') . ')'
                )
                ->bind(':parentId', $parentId, ParameterType::INTEGER);
        }

        if (!empty($languages)) {
            $query->whereIn($db->quoteName('a.language'), (array) $languages, ParameterType::STRING);
        }

        if (!empty($published)) {
            $query->whereIn($db->quoteName('a.published'), (array) $published);
        }

        $query->where($db->quoteName('a.published') . ' != -2');
        $query->order($db->quoteName('a.lft') . ' ASC');

        try {
            // Get the options.
            $db->setQuery($query);
            $links = $db->loadObjectList();
        } catch (\RuntimeException $e) {
            Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');

            return false;
        }

        if (empty($menuType)) {
            // If the menutype is empty, group the items by menutype.
            $query = $db->getQuery(true)
                ->select('*')
                ->from($db->quoteName('#__menu_types'))
                ->where($db->quoteName('menutype') . ' <> ' . $db->quote(''))
                ->order(
                    [
                        $db->quoteName('title'),
                        $db->quoteName('menutype'),
                    ]
                );

            if ($hasClientId) {
                $query->where($db->quoteName('client_id') . ' = :clientId')
                    ->bind(':clientId', $clientId, ParameterType::INTEGER);
            }

            try {
                $db->setQuery($query);
                $menuTypes = $db->loadObjectList();
            } catch (\RuntimeException $e) {
                Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');

                return false;
            }

            // Create a reverse lookup and aggregate the links.
            $rlu = [];

            foreach ($menuTypes as &$type) {
                $rlu[$type->menutype] = & $type;
                $type->links          = [];
            }

            // Loop through the list of menu links.
            foreach ($links as &$link) {
                if (isset($rlu[$link->menutype])) {
                    $rlu[$link->menutype]->links[] = & $link;

                    // Cleanup garbage.
                    unset($link->menutype);
                }
            }

            return $menuTypes;
        }

        return $links;
    }

    /**
     * Get the associations
     *
     * @param   integer  $pk  Menu item id
     *
     * @return  array
     *
     * @since   3.0
     */
    public static function getAssociations($pk)
    {
        $langAssociations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', $pk, 'id', '', '');
        $associations     = [];

        foreach ($langAssociations as $langAssociation) {
            $associations[$langAssociation->language] = $langAssociation->id;
        }

        return $associations;
    }

    /**
     * Load the menu items from database for the given menutype
     *
     * @param   string   $menutype     The selected menu type
     * @param   boolean  $enabledOnly  Whether to load only enabled/published menu items.
     * @param   int[]    $exclude      The menu items to exclude from the list
     *
     * @return  AdministratorMenuItem  A root node with the menu items as children
     *
     * @since   4.0.0
     */
    public static function getMenuItems($menutype, $enabledOnly = false, $exclude = [])
    {
        $root  = new AdministratorMenuItem();
        $db    = Factory::getContainer()->get(DatabaseInterface::class);
        $query = $db->getQuery(true);

        // Prepare the query.
        $query->select($db->quoteName('m') . '.*')
            ->from($db->quoteName('#__menu', 'm'))
            ->where(
                [
                    $db->quoteName('m.menutype') . ' = :menutype',
                    $db->quoteName('m.client_id') . ' = 1',
                    $db->quoteName('m.id') . ' > 1',
                ]
            )
            ->bind(':menutype', $menutype);

        if ($enabledOnly) {
            $query->where($db->quoteName('m.published') . ' = 1');
        }

        // Filter on the enabled states.
        $query->select($db->quoteName('e.element'))
            ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id'))
            ->extendWhere(
                'AND',
                [
                    $db->quoteName('e.enabled') . ' = 1',
                    $db->quoteName('e.enabled') . ' IS NULL',
                ],
                'OR'
            );

        if (\count($exclude)) {
            $exId = array_map('intval', array_filter($exclude, 'is_numeric'));
            $exEl = array_filter($exclude, 'is_string');

            if ($exId) {
                $query->whereNotIn($db->quoteName('m.id'), $exId)
                    ->whereNotIn($db->quoteName('m.parent_id'), $exId);
            }

            if ($exEl) {
                $query->whereNotIn($db->quoteName('e.element'), $exEl, ParameterType::STRING);
            }
        }

        // Order by lft.
        $query->order($db->quoteName('m.lft'));

        try {
            $menuItems = [];
            $iterator  = $db->setQuery($query)->getIterator();

            foreach ($iterator as $item) {
                $menuItems[$item->id] = new AdministratorMenuItem((array) $item);
            }

            unset($iterator);

            foreach ($menuItems as $menuitem) {
                // Resolve the alias item to get the original item
                if ($menuitem->type == 'alias') {
                    static::resolveAlias($menuitem);
                }

                if ($menuitem->link = \in_array($menuitem->type, ['separator', 'heading', 'container']) ? '#' : trim($menuitem->link)) {
                    $menuitem->submenu = [];
                    $menuitem->class   = $menuitem->img ?? '';
                    $menuitem->scope   = $menuitem->scope ?? null;
                    $menuitem->target  = $menuitem->browserNav ? '_blank' : '';
                }

                $menuitem->ajaxbadge  = $menuitem->getParams()->get('ajax-badge');
                $menuitem->dashboard  = $menuitem->getParams()->get('dashboard');

                if ($menuitem->parent_id > 1) {
                    if (isset($menuItems[$menuitem->parent_id])) {
                        $menuItems[$menuitem->parent_id]->addChild($menuitem);
                    }
                } else {
                    $root->addChild($menuitem);
                }
            }
        } catch (\RuntimeException $e) {
            Factory::getApplication()->enqueueMessage(Text::_('JERROR_AN_ERROR_HAS_OCCURRED'), 'error');
        }

        return $root;
    }

    /**
     * Method to install a preset menu into database and link them to the given menutype
     *
     * @param   string  $preset    The preset name
     * @param   string  $menutype  The target menutype
     *
     * @return  void
     *
     * @throws  \Exception
     *
     * @since   4.0.0
     */
    public static function installPreset($preset, $menutype)
    {
        $root = static::loadPreset($preset, false);

        if (\count($root->getChildren()) == 0) {
            throw new \Exception(Text::_('COM_MENUS_PRESET_LOAD_FAILED'));
        }

        static::installPresetItems($root, $menutype);
    }

    /**
     * Method to install a preset menu item into database and link it to the given menutype
     *
     * @param   AdministratorMenuItem  $node      The parent node of the items to process
     * @param   string                 $menutype  The target menutype
     *
     * @return  void
     *
     * @throws  \Exception
     *
     * @since   4.0.0
     */
    protected static function installPresetItems($node, $menutype)
    {
        $db    = Factory::getDbo();
        $query = $db->getQuery(true);
        $items = $node->getChildren();

        static $components = [];

        if (!$components) {
            $query->select(
                [
                    $db->quoteName('extension_id'),
                    $db->quoteName('element'),
                ]
            )
                ->from($db->quoteName('#__extensions'))
                ->where($db->quoteName('type') . ' = ' . $db->quote('component'));
            $components = $db->setQuery($query)->loadObjectList();
            $components = array_column((array) $components, 'element', 'extension_id');
        }

        $dispatcher = Factory::getApplication()->getDispatcher();
        $items      = $dispatcher->dispatch('onPreprocessMenuItems', new PreprocessMenuItemsEvent('onPreprocessMenuItems', [
            'context' => 'com_menus.administrator.import',
            'subject' => &$items, // @todo: Remove reference in Joomla 6, see PreprocessMenuItemsEvent::__constructor()
            'params'  => null,
            'enabled' => true,
        ]))->getArgument('subject', $items);

        foreach ($items as $item) {
            /** @var \Joomla\CMS\Table\Menu $table */
            $table = Table::getInstance('Menu');

            $item->alias = $menutype . '-' . $item->title;

            // Temporarily set unicodeslugs if a menu item has an unicode alias
            $unicode     = Factory::getApplication()->set('unicodeslugs', 1);
            $item->alias = ApplicationHelper::stringURLSafe($item->alias);
            Factory::getApplication()->set('unicodeslugs', $unicode);

            if ($item->type == 'separator') {
                // Do not reuse a separator
                $item->title = $item->title ?: '-';
                $item->alias = microtime(true);
            } elseif ($item->type == 'heading' || $item->type == 'container') {
                // Try to match an existing record to have minimum collision for a heading
                $keys  = [
                    'menutype'  => $menutype,
                    'type'      => $item->type,
                    'title'     => $item->title,
                    'parent_id' => (int) $item->getParent()->id,
                    'client_id' => 1,
                ];
                $table->load($keys);
            } elseif ($item->type == 'url' || $item->type == 'component') {
                if (substr($item->link, 0, 8) === 'special:') {
                    $special = substr($item->link, 8);

                    if ($special === 'language-forum') {
                        $item->link = 'index.php?option=com_admin&amp;view=help&amp;layout=langforum';
                    } elseif ($special === 'custom-forum') {
                        $item->link = '';
                    }
                }

                // Try to match an existing record to have minimum collision for a link
                $keys  = [
                    'menutype'  => $menutype,
                    'type'      => $item->type,
                    'link'      => $item->link,
                    'parent_id' => (int) $item->getParent()->id,
                    'client_id' => 1,
                ];
                $table->load($keys);
            }

            // Translate "hideitems" param value from "element" into "menu-item-id"
            if ($item->type == 'container' && \count($hideitems = (array) $item->getParams()->get('hideitems'))) {
                foreach ($hideitems as &$hel) {
                    if (!is_numeric($hel)) {
                        $hel = array_search($hel, $components);
                    }
                }

                $query = $db->getQuery(true)
                    ->select($db->quoteName('id'))
                    ->from($db->quoteName('#__menu'))
                    ->whereIn($db->quoteName('component_id'), $hideitems);
                $hideitems = $db->setQuery($query)->loadColumn();

                $item->getParams()->set('hideitems', $hideitems);
            }

            $record = [
                'menutype'     => $menutype,
                'title'        => $item->title,
                'alias'        => $item->alias,
                'type'         => $item->type,
                'link'         => $item->link,
                'browserNav'   => $item->browserNav,
                'img'          => $item->class,
                'access'       => $item->access,
                'component_id' => array_search($item->element, $components) ?: 0,
                'parent_id'    => (int) $item->getParent()->id,
                'client_id'    => 1,
                'published'    => 1,
                'language'     => '*',
                'home'         => 0,
                'params'       => (string) $item->getParams(),
            ];

            if (!$table->bind($record)) {
                throw new \Exception($table->getError());
            }

            $table->setLocation($item->getParent()->id, 'last-child');

            if (!$table->check()) {
                throw new \Exception($table->getError());
            }

            if (!$table->store()) {
                throw new \Exception($table->getError());
            }

            $item->id = $table->get('id');

            if ($item->hasChildren()) {
                static::installPresetItems($item, $menutype);
            }
        }
    }

    /**
     * Add a custom preset externally via plugin or any other means.
     * WARNING: Presets with same name will replace previously added preset *except* Joomla's default preset (joomla)
     *
     * @param   string  $name     The unique identifier for the preset.
     * @param   string  $title    The display label for the preset.
     * @param   string  $path     The path to the preset file.
     * @param   bool    $replace  Whether to replace the preset with the same name if any (except 'joomla').
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public static function addPreset($name, $title, $path, $replace = true)
    {
        if (static::$presets === null) {
            static::getPresets();
        }

        if ($name == 'joomla') {
            $replace = false;
        }

        if (($replace || !\array_key_exists($name, static::$presets)) && is_file($path)) {
            $preset = new \stdClass();

            $preset->name  = $name;
            $preset->title = $title;
            $preset->path  = $path;

            static::$presets[$name] = $preset;
        }
    }

    /**
     * Get a list of available presets.
     *
     * @return  \stdClass[]
     *
     * @since   4.0.0
     */
    public static function getPresets()
    {
        if (static::$presets === null) {
            // Important: 'null' will cause infinite recursion.
            static::$presets = [];

            $components = ComponentHelper::getComponents();
            $lang       = Factory::getApplication()->getLanguage();

            foreach ($components as $component) {
                if (!$component->enabled) {
                    continue;
                }

                $folder = JPATH_ADMINISTRATOR . '/components/' . $component->option . '/presets/';

                if (!Folder::exists($folder)) {
                    continue;
                }

                $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR)
                || $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component->option);

                $presets = Folder::files($folder, '.xml');

                foreach ($presets as $preset) {
                    $name  = File::stripExt($preset);
                    $title = strtoupper($component->option . '_MENUS_PRESET_' . $name);
                    static::addPreset($name, $title, $folder . $preset);
                }
            }

            // Load from template folder automatically
            $app = Factory::getApplication();
            $tpl = JPATH_THEMES . '/' . $app->getTemplate() . '/html/com_menus/presets';

            if (is_dir($tpl)) {
                $files = Folder::files($tpl, '\.xml$');

                foreach ($files as $file) {
                    $name  = substr($file, 0, -4);
                    $title = str_replace('-', ' ', $name);

                    static::addPreset(strtolower($name), ucwords($title), $tpl . '/' . $file);
                }
            }
        }

        return static::$presets;
    }

    /**
     * Load the menu items from a preset file into a hierarchical list of objects
     *
     * @param   string                 $name      The preset name
     * @param   bool                   $fallback  Fallback to default (joomla) preset if the specified one could not be loaded?
     * @param   AdministratorMenuItem  $parent    Root node of the menu
     *
     * @return  AdministratorMenuItem
     *
     * @since   4.0.0
     */
    public static function loadPreset($name, $fallback = true, $parent = null)
    {
        $presets = static::getPresets();

        if (!$parent) {
            $parent = new AdministratorMenuItem();
        }

        if (isset($presets[$name]) && ($xml = simplexml_load_file($presets[$name]->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) {
            static::loadXml($xml, $parent);
        } elseif ($fallback && isset($presets['default'])) {
            if (($xml = simplexml_load_file($presets['default']->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) {
                static::loadXml($xml, $parent);
            }
        }

        return $parent;
    }

    /**
     * Method to resolve the menu item alias type menu item
     *
     * @param   AdministratorMenuItem  &$item  The alias object
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public static function resolveAlias(&$item)
    {
        $obj = $item;

        while ($obj->type == 'alias') {
            $aliasTo = (int) $obj->getParams()->get('aliasoptions');

            $db    = Factory::getDbo();
            $query = $db->getQuery(true);
            $query->select(
                [
                    $db->quoteName('a.id'),
                    $db->quoteName('a.link'),
                    $db->quoteName('a.type'),
                    $db->quoteName('e.element'),
                ]
            )
                ->from($db->quoteName('#__menu', 'a'))
                ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id'))
                ->where($db->quoteName('a.id') . ' = :aliasTo')
                ->bind(':aliasTo', $aliasTo, ParameterType::INTEGER);

            try {
                $obj = new AdministratorMenuItem($db->setQuery($query)->loadAssoc());

                if (!$obj) {
                    $item->link = '';

                    return;
                }
            } catch (\Exception $e) {
                $item->link = '';

                return;
            }
        }

        $item->id      = $obj->id;
        $item->link    = $obj->link;
        $item->type    = $obj->type;
        $item->element = $obj->element;
    }

    /**
     * Parse the flat list of menu items and prepare the hierarchy of them using parent-child relationship.
     *
     * @param   AdministratorMenuItem  $item  Menu item to preprocess
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public static function preprocess($item)
    {
        // Resolve the alias item to get the original item
        if ($item->type == 'alias') {
            static::resolveAlias($item);
        }

        if ($item->link = \in_array($item->type, ['separator', 'heading', 'container']) ? '#' : trim($item->link)) {
            $item->class  = $item->img ?? '';
            $item->scope  = $item->scope ?? null;
            $item->target = $item->browserNav ? '_blank' : '';
        }
    }

    /**
     * Load a menu tree from an XML file
     *
     * @param   \SimpleXMLElement[]    $elements  The xml menuitem nodes
     * @param   AdministratorMenuItem  $parent    The menu hierarchy list to be populated
     * @param   string[]               $replace   The substring replacements for iterator type items
     *
     * @return  void
     *
     * @since  4.0.0
     */
    protected static function loadXml($elements, $parent, $replace = [])
    {
        foreach ($elements as $element) {
            if ($element->getName() != 'menuitem') {
                continue;
            }

            $select = (string) $element['sql_select'];
            $from   = (string) $element['sql_from'];

            /**
             * Following is a repeatable group based on simple database query. This requires sql_* attributes (sql_select and sql_from are required)
             * The values can be used like - "{sql:columnName}" in any attribute of repeated elements.
             * The repeated elements are place inside this xml node but they will be populated in the same level in the rendered menu
             */
            if ($select && $from) {
                $hidden = $element['hidden'] == 'true';
                $where  = (string) $element['sql_where'];
                $order  = (string) $element['sql_order'];
                $group  = (string) $element['sql_group'];
                $lJoin  = (string) $element['sql_leftjoin'];
                $iJoin  = (string) $element['sql_innerjoin'];

                $db    = Factory::getDbo();
                $query = $db->getQuery(true);
                $query->select($select)->from($from);

                if ($where) {
                    $query->where($where);
                }

                if ($order) {
                    $query->order($order);
                }

                if ($group) {
                    $query->group($group);
                }

                if ($lJoin) {
                    $query->join('LEFT', $lJoin);
                }

                if ($iJoin) {
                    $query->join('INNER', $iJoin);
                }

                $results = $db->setQuery($query)->loadObjectList();

                // Skip the entire group if no items to iterate over.
                if ($results) {
                    // Show the repeatable group heading node only if not set as hidden.
                    if (!$hidden) {
                        $child = static::parseXmlNode($element, $replace);
                        $parent->addChild($child);
                    }

                    // Iterate over the matching records, items goes in the same level (not $item->submenu) as this node.
                    if ('self' == (string) $element['sql_target']) {
                        foreach ($results as $result) {
                            static::loadXml($element->menuitem, $child, $result);
                        }
                    } else {
                        foreach ($results as $result) {
                            static::loadXml($element->menuitem, $parent, $result);
                        }
                    }
                }
            } else {
                $item = static::parseXmlNode($element, $replace);

                // Process the child nodes
                static::loadXml($element->menuitem, $item, $replace);

                $parent->addChild($item);
            }
        }
    }

    /**
     * Create a menu item node from an xml element
     *
     * @param   \SimpleXMLElement  $node     A menuitem element from preset xml
     * @param   string[]           $replace  The values to substitute in the title, link and element texts
     *
     * @return  \stdClass
     *
     * @since   4.0.0
     */
    protected static function parseXmlNode($node, $replace = [])
    {
        $item = new AdministratorMenuItem();

        $item->id         = null;
        $item->type       = (string) $node['type'];
        $item->title      = (string) $node['title'];
        $item->alias      = (string) $node['alias'];
        $item->link       = (string) $node['link'];
        $item->target     = (string) $node['target'];
        $item->element    = (string) $node['element'];
        $item->class      = (string) $node['class'];
        $item->icon       = (string) $node['icon'];
        $item->access     = (int) $node['access'];
        $item->scope      = (string) $node['scope'] ?: 'default';
        $item->ajaxbadge  = (string) $node['ajax-badge'];
        $item->dashboard  = (string) $node['dashboard'];

        $params = new Registry(trim($node->params));
        $params->set('menu-permission', (string) $node['permission']);

        if ($item->type == 'separator' && trim($item->title, '- ')) {
            $params->set('text_separator', 1);
        }

        if ($item->type == 'heading' || $item->type == 'container') {
            $item->link = '#';
        }

        if ((string) $node['quicktask']) {
            $params->set('menu-quicktask', (string) $node['quicktask']);
            $params->set('menu-quicktask-title', (string) $node['quicktask-title']);
            $params->set('menu-quicktask-icon', (string) $node['quicktask-icon']);
            $params->set('menu-quicktask-permission', (string) $node['quicktask-permission']);
        }

        // Translate attributes for iterator values
        foreach ($replace as $var => $val) {
            $item->title   = str_replace("{sql:$var}", $val, $item->title);
            $item->element = str_replace("{sql:$var}", $val, $item->element);
            $item->link    = str_replace("{sql:$var}", $val, $item->link);
            $item->class   = str_replace("{sql:$var}", $val, $item->class);
            $item->icon    = str_replace("{sql:$var}", $val, $item->icon);
            $params->set('menu-quicktask', str_replace("{sql:$var}", $val, $params->get('menu-quicktask', '')));
        }

        $item->setParams($params);

        return $item;
    }
}
