<?php

/**
 * This class provides basic functionality for standard admin sections which are powered by the model.
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Controller
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin\Controller;

use Nails\Admin\Factory\Nav;
use Nails\Admin\Helper;
use Nails\Common\Exception\FactoryException;
use Nails\Common\Exception\NailsException;
use Nails\Common\Exception\ValidationException;
use Nails\Common\Resource;
use Nails\Common\Service\Locale;
use Nails\Common\Service\Uri;
use Nails\Common\Traits\Model\Localised;
use Nails\Common\Traits\Model\Nestable;
use Nails\Common\Traits\Model\Sortable;
use Nails\Factory;

abstract class DefaultController extends Base
{
    /**
     * The model to use for this admin view (and which module provides it)
     */
    const CONFIG_MODEL_NAME     = '';
    const CONFIG_MODEL_PROVIDER = '';

    /**
     * The permission string to use when checking permissions; if not provided then no permissions required
     */
    const CONFIG_PERMISSION = '';

    /**
     * The singular and plural name of the item being managed; defaults to the model name
     */
    const CONFIG_TITLE_SINGLE = '';
    const CONFIG_TITLE_PLURAL = '';

    /**
     * Where to display this controller in the admin sidebar; defaults to CONFIG_TITLE_PLURAL
     */
    const CONFIG_SIDEBAR_GROUP = '';

    /**
     * What icon to display in the nav
     */
    const CONFIG_SIDEBAR_ICON = '';

    /**
     * the base URL for this controller
     */
    const CONFIG_BASE_URL = '';

    /**
     * The sorting options to give the user on the index view
     */
    const CONFIG_SORT_OPTIONS = [
        'Label'    => 'label',
        'Created'  => 'created',
        'Modified' => 'modified',
    ];

    /**
     * The default sorting order
     */
    const CONFIG_SORT_DIRECTION = 'asc';

    /**
     * The fields to show on the index view;
     *
     * This array forms the basis for the $this->$aConfig['INDEX_FIELDS'] config.
     * You are free to manipulate this after the constructor has been called.
     *
     * If you wish to create any dynamic fields then the element value can be a
     * closure/callable which is passed $oItem as the one and only argument.
     */
    const CONFIG_INDEX_FIELDS = [
        'Label'       => 'label',
        'Created'     => 'created',
        'Modified'    => 'modified',
        'Modified By' => 'modified_by',
    ];

    /**
     * Any additional header buttons to add to the index page.
     */
    const CONFIG_INDEX_HEADER_BUTTONS = [];

    /**
     * Any additional buttons to add to each row on the page.
     * See static::$aConfigIndexRowButtons for details
     */
    const CONFIG_INDEX_ROW_BUTTONS = [];

    /**
     * Specify whether the controller supports item creation
     */
    const CONFIG_CAN_CREATE = true;

    /**
     * Specify whether the controller supports item editing
     */
    const CONFIG_CAN_EDIT = true;

    /**
     * Specify whether the controller supports linking to the item
     */
    const CONFIG_CAN_VIEW = true;

    /**
     * Specify whether the controller supports item deletion
     */
    const CONFIG_CAN_DELETE = true;

    /**
     * Specify whether the controller supports item restoration
     */
    const CONFIG_CAN_RESTORE = true;

    /**
     * Additional data to pass into the getAll call on the index view
     */
    const CONFIG_INDEX_DATA = [];

    /**
     * The fields on the index view which should be rendered as user cells
     */
    const CONFIG_INDEX_USER_FIELDS = [
        'created_by',
        'modified_by',
        'user_id',
    ];

    /**
     * The fields on the index view which should be rendered as boolean cells
     */
    const CONFIG_INDEX_BOOL_FIELDS = [
        'is_active',
        'is_published',
        'is_deleted',
    ];

    /**
     * The fields on the index view which should be run through number_format
     */
    const CONFIG_INDEX_NUMERIC_FIELDS = [
        'id',
    ];

    /**
     * The fields on the index view which should be centered
     */
    const CONFIG_INDEX_CENTERED_FIELDS = [
        'id',
    ];

    /**
     * Fields which should be marked as readonly when creating an item
     */
    const CONFIG_CREATE_READONLY_FIELDS = [];

    /**
     * The fields to ignore on the create view
     */
    const CONFIG_CREATE_IGNORE_FIELDS = [
        'id',
        'slug',
        'token',
        'is_deleted',
        'created',
        'created_by',
        'modified',
        'modified_by',
    ];

    /**
     * The fields to ignore on the edit view
     */
    const CONFIG_EDIT_IGNORE_FIELDS = self::CONFIG_CREATE_IGNORE_FIELDS;

    /**
     * Fields which should be marked as readonly when editing an item
     */
    const CONFIG_EDIT_READONLY_FIELDS = [];

    /**
     * Additional data to pass into the getAll call on the edit view
     */
    const CONFIG_EDIT_DATA = [];

    /**
     * Any additional header buttons to add to the edit page.
     */
    const CONFIG_EDIT_HEADER_BUTTONS = [];

    /**
     * Specify a specific order for fieldsets
     */
    const CONFIG_EDIT_FIELDSET_ORDER = [];

    /**
     * Additional data to pass into the getAll call on the sort view
     */
    const CONFIG_SORT_DATA = [];

    /**
     * Which column to use for the label when sorting
     */
    const CONFIG_SORT_LABEL = 'label';

    /**
     * When creating, this string is passed to supporting functions
     */
    const EDIT_MODE_CREATE = 'CREATE';

    /**
     * When editing, this string is passed to supporting functions
     */
    const EDIT_MODE_EDIT = 'EDIT';

    /**
     * Enable or disable the "Notes" feature
     */
    const EDIT_ENABLE_NOTES = true;

    /**
     * Message displayed to user when an item is successfully created
     */
    const CREATE_SUCCESS_MESSAGE = 'Item created successfully. %s';

    /**
     * Message displayed to user when an item fails to be created
     */
    const CREATE_ERROR_MESSAGE = 'Failed to create item.';

    /**
     * Message displayed to user when an item is successfully updated
     */
    const EDIT_SUCCESS_MESSAGE = 'Item updated successfully. %s';

    /**
     * Message displayed to user when an item fails to be created
     */
    const EDIT_ERROR_MESSAGE = 'Failed to update item.';

    /**
     * Message displayed to user when an item is successfully deleted
     */
    const DELETE_SUCCESS_MESSAGE = 'Item deleted successfully.';

    /**
     * Message displayed to user when an item fails to be deleted
     */
    const DELETE_ERROR_MESSAGE = 'Failed to delete item.';

    /**
     * Message displayed to user when an item is successfully restored
     */
    const RESTORE_SUCCESS_MESSAGE = 'Item restored successfully.';

    /**
     * Message displayed to user when an item fails to be restored
     */
    const RESTORE_ERROR_MESSAGE = 'Failed to restore item.';

    /**
     * Message displayed to user when an items are ordered successfully
     */
    const ORDER_SUCCESS_MESSAGE = 'Items ordered successfully.';

    /**
     * Message displayed to user when an item fails to be ordered
     */
    const ORDER_ERROR_MESSAGE = 'Failed to order items.';

    // --------------------------------------------------------------------------

    /**
     * Any additional buttons to add for each row item (will sit between "View" if available and "Edit").
     * Takes the following format:
     *
     *   [
     *       // The button's URL, row items can be substituted in using double curly syntax.
     *       // Will be prefixed with $CONFIG['BASE_URL'] (which is the URL to the controller)
     *       'url'        => 'edit/{{id}}',
     *
     *       // The button's value/label
     *       'label'      => 'Edit',
     *
     *       // Additional classes to add to the button
     *       'class'      => 'btn-primary',
     *
     *       // Additional attributes to add to the button
     *       'attr'       => '',
     *
     *       // If required, a permission string to check in order to render the button;
     *       // will be appended to `admin:$CONFIG['PERMISSION']:`
     *       'permission' => 'edit',
     *
     *       // An expression to determine if the button can be rendered
     *       'enabled'   => function($oItem) { return true; },
     *   ],
     */
    protected static $aConfigIndexRowButtons = [];

    // --------------------------------------------------------------------------

    /**
     * Contains the configs for this controller
     *
     * @var array
     */
    protected $aConfig;

    // --------------------------------------------------------------------------

    /**
     * DefaultController constructor.
     *
     * @throws NailsException
     */
    public function __construct()
    {
        parent::__construct();
        $this->getConfig();
    }

    // --------------------------------------------------------------------------

    /**
     * Announces this controller's navGroups
     *
     * @return array|Nav
     * @throws NailsException
     */
    public static function announce()
    {
        $oNavGroup = Factory::factory('Nav', 'nails/module-admin');
        $oNavGroup
            ->setLabel(static::getSidebarGroup())
            ->setIcon(static::CONFIG_SIDEBAR_ICON);

        if (static::userCan('browse')) {
            $oNavGroup
                ->addAction('Manage ' . static::getTitlePlural(), 'index');
        }

        return $oNavGroup;
    }

    // --------------------------------------------------------------------------

    /**
     * Returns an array of extra permissions for this controller
     *
     * @return array
     */
    public static function permissions(): array
    {
        $aPermissions = parent::permissions();

        if (!empty(static::CONFIG_PERMISSION)) {
            $aPermissions['browse']  = 'Can browse items';
            $aPermissions['create']  = 'Can create items';
            $aPermissions['edit']    = 'Can edit items';
            $aPermissions['delete']  = 'Can delete items';
            $aPermissions['restore'] = 'Can restore items';
        }

        return $aPermissions;
    }

    // --------------------------------------------------------------------------

    /**
     * Browse all items
     *
     * @return void
     */
    public function index(): void
    {
        if (!static::userCan('browse')) {
            unauthorised();
        }

        $oInput  = Factory::service('Input');
        $oModel  = $this->getModel();
        $aConfig = $this->getConfig();

        $sAlias      = $oModel->getTableAlias();
        $aSortConfig = $aConfig['SORT_OPTIONS'];

        if (classUses($oModel, Nestable::class)) {
            $aSortConfig = array_merge(['Hierarchy' => 'order'], $aSortConfig);
        }

        //  Get the first key (i.e the default sort)
        $sFirstKey = reset($aSortConfig);

        //  Prepare the sort options so they have the appropriate table alias
        $aSortCol = [];
        foreach ($aSortConfig as $sLabel => $sColumn) {
            if (strpos($sColumn, '.') === false) {
                $aSortCol[$sAlias . '.' . $sColumn] = $sLabel;
            } else {
                $aSortCol[$sColumn] = $sLabel;
            }
        }

        //  Other parameters
        $iPage      = $oInput->get('page') ? $oInput->get('page') : 0;
        $iPerPage   = $oInput->get('perPage') ? $oInput->get('perPage') : 50;
        $sSortOn    = $oInput->get('sortOn') ? $oInput->get('sortOn') : $sAlias . '.' . $sFirstKey;
        $sSortOrder = $oInput->get('sortOrder') ? $oInput->get('sortOrder') : $aConfig['SORT_DIRECTION'];
        $sKeywords  = $oInput->get('keywords');
        $aCbFilters = $this->indexCheckboxFilters();
        $aDdFilters = $this->indexDropdownFilters();

        $aData = [
                'cbFilters' => $aCbFilters,
                'ddFilters' => $aDdFilters,
                'keywords'  => $sKeywords,
                'sort'      => [
                    [$sSortOn, $sSortOrder],
                ],
            ] + $aConfig['INDEX_DATA'];

        // --------------------------------------------------------------------------

        if (classUses($oModel, Localised::class)) {
            $aData['NO_LOCALISE_FILTER'] = true;
        }

        // --------------------------------------------------------------------------

        $iTotalRows               = $oModel->countAll($aData);
        $this->data['items']      = $oModel->getAll($iPage, $iPerPage, $aData);
        $this->data['pagination'] = Helper::paginationObject($iPage, $iPerPage, $iTotalRows);
        $this->data['search']     = Helper::searchObject(
            true,
            $aSortCol,
            $sSortOn,
            $sSortOrder,
            $iPerPage,
            $sKeywords,
            $aCbFilters,
            $aDdFilters
        );

        // --------------------------------------------------------------------------

        static::addHeaderButtons($aConfig['INDEX_HEADER_BUTTONS']);

        // --------------------------------------------------------------------------

        $this->data['page']->title = $aConfig['TITLE_PLURAL'] . ' › Manage';
        Helper::loadView('index');
    }

    // --------------------------------------------------------------------------

    /**
     * Create a new item
     *
     * @return void
     */
    public function create(): void
    {
        $aConfig = $this->getConfig();
        if (!$aConfig['CAN_CREATE']) {
            show404();
        } elseif (!static::userCan('create')) {
            unauthorised();
        }

        $oDb    = Factory::service('Database');
        $oInput = Factory::service('Input');
        $oModel = $this->getModel();

        // --------------------------------------------------------------------------

        //  View Data & Assets
        $this->loadEditViewData();

        // --------------------------------------------------------------------------

        if ($oInput->post()) {
            try {

                $this->runFormValidation(static::EDIT_MODE_CREATE);
                $oDb->trans_begin();
                $this->beforeCreateAndEdit(static::EDIT_MODE_CREATE);
                $this->beforeCreate();

                if (classUses($oModel, Localised::class)) {

                    /** @var Locale $oLocale */
                    $oLocale = Factory::service('Locale');
                    /** @var \Nails\Common\Factory\Locale $oItemLocale */
                    $oItemLocale = Factory::factory('Locale');
                    $oLocale->setFromString($oItemLocale, $oInput->post('locale'));

                    $oItem = $oModel->create($this->getPostObject(), true, $oItemLocale);

                } else {
                    $oItem = $oModel->create($this->getPostObject(), true);
                }

                if (!$oItem) {
                    throw new NailsException(static::CREATE_ERROR_MESSAGE . ' ' . $oModel->lastError());
                }

                $this->afterCreateAndEdit(static::EDIT_MODE_CREATE, $oItem);
                $this->afterCreate($oItem);
                $oDb->trans_commit();

                if (property_exists($oItem, 'url')) {
                    $sLink = anchor(
                        $oItem->url,
                        'View  ',
                        'class="btn btn-success btn-xs pull-right" target="_blank"'
                    );
                } else {
                    $sLink = '';
                }

                $oSession = Factory::service('Session', 'nails/module-auth');
                $oSession->setFlashData('success', sprintf(static::CREATE_SUCCESS_MESSAGE, $sLink));

                if ($aConfig['CAN_EDIT'] && static::userCan('edit')) {
                    if (classUses($oModel, Localised::class)) {
                        redirect($aConfig['BASE_URL'] . '/edit/' . $oItem->id . '/' . $oItem->locale);
                    } else {
                        redirect($aConfig['BASE_URL'] . '/edit/' . $oItem->id);
                    }
                } else {
                    $this->returnToIndex();
                }

            } catch (\Exception $e) {
                $oDb->trans_rollback();
                $this->data['error'] = $e->getMessage();
            }
        }

        // --------------------------------------------------------------------------

        $oUri           = Factory::service('Uri');
        $iExistingId    = (int) $oUri->segment(5) ?: null;
        $sDesiredLocale = $oUri->segment(6) ?: null;
        if (classUses($oModel, Localised::class) && !$iExistingId) {

            /** @var Locale $oLocale */
            $oLocale = Factory::service('Locale');
            foreach ($aConfig['FIELDS'] as $oField) {
                if ($oField->key === 'locale') {
                    $oField->info .= ' New items must be created in ' . $oLocale->getDefautLocale()->getDisplayLanguage();
                    break;
                }
            }

        } elseif (classUses($oModel, Localised::class)) {

            //  Validate the ID, and only allow creation of new locales
            $aExisting = $oModel->getAll([
                'NO_LOCALISE_FILTER' => true,
                'where'              => [
                    ['id', $iExistingId],
                ],
            ]);

            if (empty($aExisting)) {
                show404();
            }

            //  Test if new locales can be created, filter out existing locales from the options list
            $aExistingLocales = arrayExtractProperty($aExisting, 'locale');
            foreach ($aConfig['FIELDS'] as $oField) {
                if ($oField->key === 'locale') {

                    $aDiff = array_diff(
                        array_keys($oField->options),
                        array_values($aExistingLocales)
                    );

                    if (empty($aDiff)) {
                        $oSession = Factory::service('Session', 'nails/module-auth');
                        $oSession->setFlashData('error', 'No more variations of this item can be created.');
                        $this->returnToIndex();
                    }

                    $oField->options = array_intersect_key(
                        $oField->options,
                        array_flip($aDiff)
                    );

                    $oField->default = $sDesiredLocale;

                    unset($this->aConfig['CREATE_READONLY_FIELDS'][array_search('locale', $this->aConfig['CREATE_READONLY_FIELDS'])]);
                    break;
                }
            }
        }

        // --------------------------------------------------------------------------

        $this->data['page']->title = $aConfig['TITLE_SINGLE'] . ' › Create';
        Helper::loadView('edit');
    }

    // --------------------------------------------------------------------------

    /**
     * Edit an existing item
     *
     * @return void
     */
    public function edit(): void
    {
        $aConfig = $this->getConfig();
        if (!$aConfig['CAN_EDIT']) {
            show404();
        } elseif (!static::userCan('edit')) {
            unauthorised();
        }

        $oDb    = Factory::service('Database');
        $oInput = Factory::service('Input');
        $oModel = $this->getModel();
        $oItem  = $this->getItem($aConfig['EDIT_DATA']);

        // --------------------------------------------------------------------------

        //  View Data & Assets
        $this->loadEditViewData($oItem);

        // --------------------------------------------------------------------------

        if ($oInput->post()) {
            try {

                $this->runFormValidation(static::EDIT_MODE_EDIT);
                $oDb->trans_begin();
                $this->beforeCreateAndEdit(static::EDIT_MODE_EDIT, $oItem);
                $this->beforeEdit($oItem);

                if (classUses($oModel, Localised::class)) {
                    $bResult = $oModel->update($oItem->id, $this->getPostObject(), $oItem->locale);
                } else {
                    $bResult = $oModel->update($oItem->id, $this->getPostObject());
                }

                if (!$bResult) {
                    throw new NailsException(static::EDIT_ERROR_MESSAGE . ' ' . $oModel->lastError());
                }

                if (classUses($oModel, Localised::class)) {
                    $oNewItem = $oModel->getById($oItem->id, [
                        'USE_LOCALE' => $oInput->post('locale'),
                    ]);
                } else {
                    $oNewItem = $oModel->getById($oItem->id);
                }
                $this->afterCreateAndEdit(static::EDIT_MODE_EDIT, $oNewItem, $oItem);
                $this->afterEdit($oNewItem, $oItem);
                $oDb->trans_commit();

                if (property_exists($oNewItem, 'url')) {
                    $sLink = anchor(
                        $oNewItem->url,
                        'View  ',
                        'class="btn btn-success btn-xs pull-right" target="_blank"'
                    );
                } else {
                    $sLink = '';
                }

                $oSession = Factory::service('Session', 'nails/module-auth');
                $oSession->setFlashData('success', sprintf(static::EDIT_SUCCESS_MESSAGE, $sLink));

                if (classUses($oModel, Localised::class)) {
                    redirect($aConfig['BASE_URL'] . '/edit/' . $oItem->id . '/' . $oItem->locale);
                } else {
                    redirect($aConfig['BASE_URL'] . '/edit/' . $oItem->id);
                }

            } catch (\Exception $e) {
                $oDb->trans_rollback();
                $this->data['error'] = $e->getMessage();
            }
        }

        // --------------------------------------------------------------------------

        static::addHeaderButtons($aConfig['EDIT_HEADER_BUTTONS']);

        // --------------------------------------------------------------------------

        $this->data['page']->title = $aConfig['TITLE_SINGLE'] . ' › Edit';
        Helper::loadView('edit');
    }

    // --------------------------------------------------------------------------

    /**
     * Delete an item
     *
     * @return void
     */
    public function delete(): void
    {
        $aConfig = $this->getConfig();
        if (!$aConfig['CAN_DELETE']) {
            show404();
        } elseif (!static::userCan('delete')) {
            unauthorised();
        }

        $oDb    = Factory::service('Database');
        $oModel = $this->getModel();
        $oItem  = $this->getItem();

        if (empty($oItem)) {
            show404();
        }

        try {

            $oDb->trans_begin();
            $this->beforeDelete($oItem);

            if (classUses($oModel, Localised::class)) {
                $oModel->delete($oItem->id, $oItem->locale);
            } elseif (!$oModel->delete($oItem->id)) {
                throw new NailsException(static::DELETE_ERROR_MESSAGE . ' ' . $oModel->lastError());
            }

            $this->afterDelete($oItem);
            $oDb->trans_commit();

            if ($aConfig['CAN_RESTORE'] && static::userCan('restore')) {
                if (classUses($oModel, Localised::class)) {
                    $sRestoreLink = anchor(
                        $aConfig['BASE_URL'] . '/restore/' . $oItem->id . '/' . $oItem->locale,
                        'Restore?'
                    );
                } else {
                    $sRestoreLink = anchor(
                        $aConfig['BASE_URL'] . '/restore/' . $oItem->id,
                        'Restore?'
                    );
                }
            } else {
                $sRestoreLink = '';
            }

            $oSession = Factory::service('Session', 'nails/module-auth');
            $oSession->setFlashData('success', static::DELETE_SUCCESS_MESSAGE . ' ' . $sRestoreLink);
            $this->returnToIndex();

        } catch (\Exception $e) {
            $oDb->trans_rollback();
            $oSession = Factory::service('Session', 'nails/module-auth');
            $oSession->setFlashData('error', static::DELETE_ERROR_MESSAGE . ' ' . $e->getMessage());
            $this->returnToIndex();
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Restore an item
     *
     * @return void
     */
    public function restore(): void
    {
        $aConfig = $this->getConfig();
        if (!$aConfig['CAN_RESTORE']) {
            show404();
        } elseif (!static::userCan('restore')) {
            unauthorised();
        }

        $oUri     = Factory::service('Uri');
        $oDb      = Factory::service('Database');
        $oSession = Factory::service('Session', 'nails/module-auth');
        $oModel   = $this->getModel();
        $oItem    = $this->getItem([], null, true);

        try {

            $oDb->trans_begin();
            if (classUses($oModel, Localised::class)) {
                $bResult = $oModel->restore($oItem->id, $oItem->locale);
            } else {
                $bResult = $oModel->restore($oItem->id);
            }

            if (!$bResult) {
                throw new NailsException(static::RESTORE_ERROR_MESSAGE . ' ' . $oModel->lastError());
            }

            $oDb->trans_commit();
            $oSession->setFlashData('success', static::RESTORE_SUCCESS_MESSAGE);
            $this->returnToIndex();

        } catch (\Exception $e) {
            $oDb->trans_rollback();
            $oSession->setFlashData('error', static::RESTORE_ERROR_MESSAGE . ' ' . $e->getMessage());
            $this->returnToIndex();
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Sort items into order
     *
     * @return void
     */
    public function sort(): void
    {
        $aConfig = $this->getConfig();
        if (!$aConfig['CAN_EDIT']) {
            show404();
        } elseif (!static::userCan('edit')) {
            unauthorised();
        }

        $oModel = $this->getModel();
        $oInput = Factory::service('Input');
        $oDb    = Factory::service('Database');

        if ($oInput->post()) {
            try {

                $oDb->trans_begin();
                $aItems = array_values((array) $oInput->post('order'));
                foreach ($aItems as $iOrder => $iId) {

                    if (classUses($oModel, Localised::class)) {

                        $aItems = $oModel->getAll([
                            'NO_LOCALISE_FILTER' => true,
                            'where'              => [
                                ['id', $iId],
                            ],
                        ]);

                        foreach ($aItems as $oItem) {
                            if (!$oModel->update($iId, ['order' => $iOrder], $oItem->locale)) {
                                throw new NailsException(
                                    static::ORDER_ERROR_MESSAGE . ' ' . $oModel->lastError()
                                );
                            }
                        }

                    } elseif (!$oModel->update($iId, ['order' => $iOrder])) {
                        throw new NailsException(
                            static::ORDER_ERROR_MESSAGE . ' ' . $oModel->lastError()
                        );
                    }
                }

                $oDb->trans_commit();

                $oSession = Factory::service('Session', 'nails/module-auth');
                $oSession->setFlashData('success', static::ORDER_SUCCESS_MESSAGE);

                redirect($aConfig['BASE_URL'] . '/sort');

            } catch (\Exception $e) {
                $oDb->trans_rollback();
                $this->data['error'] = $e->getMessage();
            }
        }

        $aItems                    = $oModel->getAll($aConfig['SORT_DATA']);
        $this->data['items']       = $aItems;
        $this->data['page']->title = $aConfig['TITLE_PLURAL'] . ' › Sort';
        Helper::loadView('order');
    }

    // --------------------------------------------------------------------------

    /**
     * Returns the controllers config
     *
     * @return array
     * @throws NailsException
     * @throws FactoryException
     */
    protected function &getConfig(): array
    {
        if (!empty($this->aConfig)) {
            return $this->aConfig;
        }

        //  Ensure required constants are set
        $aRequiredConstants = [
            'CONFIG_MODEL_NAME',
            'CONFIG_MODEL_PROVIDER',
        ];
        foreach ($aRequiredConstants as $sConstant) {
            if (empty(constant('static::' . $sConstant))) {
                throw new NailsException(
                    'The constant "static::' . $sConstant . '" must be set in ' . get_called_class()
                );
            }
        }

        //  Build the config array
        $oModel  = static::getModel();
        $aConfig = [
            'MODEL_NAME'             => static::CONFIG_MODEL_NAME,
            'MODEL_PROVIDER'         => static::CONFIG_MODEL_PROVIDER,
            'MODEL_INSTANCE'         => $oModel,
            'CAN_CREATE'             => static::CONFIG_CAN_CREATE,
            'CAN_EDIT'               => static::CONFIG_CAN_EDIT,
            'CAN_VIEW'               => static::CONFIG_CAN_VIEW,
            'CAN_DELETE'             => static::CONFIG_CAN_DELETE,
            'CAN_RESTORE'            => static::CONFIG_CAN_RESTORE && !$oModel->isDestructiveDelete(),
            'PERMISSION'             => static::CONFIG_PERMISSION,
            'TITLE_SINGLE'           => static::getTitleSingle(),
            'TITLE_PLURAL'           => static::getTitlePlural(),
            'SIDEBAR_GROUP'          => static::getSidebarGroup(),
            'SIDEBAR_ICON'           => static::CONFIG_SIDEBAR_ICON,
            'BASE_URL'               => static::getBaseUrl(),
            'SORT_OPTIONS'           => static::CONFIG_SORT_OPTIONS,
            'SORT_DIRECTION'         => static::CONFIG_SORT_DIRECTION,
            'INDEX_FIELDS'           => static::CONFIG_INDEX_FIELDS,
            'INDEX_HEADER_BUTTONS'   => static::CONFIG_INDEX_HEADER_BUTTONS,
            'INDEX_ROW_BUTTONS'      => array_merge(static::$aConfigIndexRowButtons, static::CONFIG_INDEX_ROW_BUTTONS),
            'INDEX_DATA'             => static::CONFIG_INDEX_DATA,
            'INDEX_BOOL_FIELDS'      => static::CONFIG_INDEX_BOOL_FIELDS,
            'INDEX_USER_FIELDS'      => static::CONFIG_INDEX_USER_FIELDS,
            'INDEX_NUMERIC_FIELDS'   => static::CONFIG_INDEX_NUMERIC_FIELDS,
            'INDEX_CENTERED_FIELDS'  => static::CONFIG_INDEX_CENTERED_FIELDS,
            'CREATE_READONLY_FIELDS' => static::CONFIG_CREATE_READONLY_FIELDS,
            'CREATE_IGNORE_FIELDS'   => static::CONFIG_CREATE_IGNORE_FIELDS,
            'EDIT_HEADER_BUTTONS'    => static::CONFIG_EDIT_HEADER_BUTTONS,
            'EDIT_READONLY_FIELDS'   => static::CONFIG_EDIT_READONLY_FIELDS,
            'EDIT_IGNORE_FIELDS'     => static::CONFIG_EDIT_IGNORE_FIELDS,
            'EDIT_DATA'              => static::CONFIG_EDIT_DATA,
            'SORT_DATA'              => static::CONFIG_SORT_DATA,
            'SORT_LABEL'             => static::CONFIG_SORT_LABEL,
            'FIELDSET_ORDER'         => static::CONFIG_EDIT_FIELDSET_ORDER,
            'ENABLE_NOTES'           => static::EDIT_ENABLE_NOTES,
            'FIELDS'                 => $oModel->describeFields(),
        ];

        if (classUses($oModel, Sortable::class)) {
            $aConfig['SORT_OPTIONS']           = array_merge(['Defined Order' => 'order'], $aConfig['SORT_OPTIONS']);
            $aConfig['CREATE_IGNORE_FIELDS'][] = $oModel->getSortableColumn();
            $aConfig['EDIT_IGNORE_FIELDS'][]   = $oModel->getSortableColumn();
        }

        if (classUses($oModel, Nestable::class)) {
            $aConfig['CREATE_IGNORE_FIELDS'][] = $oModel->getBreadcrumbsColumn();
            $aConfig['EDIT_IGNORE_FIELDS'][]   = $oModel->getBreadcrumbsColumn();
        }

        if (classUses($oModel, Localised::class)) {
            $aConfig['CREATE_READONLY_FIELDS'][] = 'locale';
            $aConfig['EDIT_READONLY_FIELDS'][]   = 'locale';
            $aConfig['INDEX_FIELDS']             = array_merge(
                [
                    'Locale' => function ($oRow) {
                        $sFlag = $oRow->locale->getFlagEmoji();
                        return $sFlag ? 'locale->getDisplayLanguage() . '">' . $sFlag . '' : $oRow->locale;
                    },
                ],
                $aConfig['INDEX_FIELDS']
            );
        }

        // --------------------------------------------------------------------------

        if ($aConfig['CAN_CREATE'] && static::userCan('create') && classUses($oModel, Localised::class)) {
            $oItem = $this->getItem([], null, false, false);
            if (!empty($oItem)) {
                $aVersions = [];
                foreach ($oItem->missing_locales as $oLocale) {
                    $aVersions['Create ' . $oLocale->getDisplayLanguage()] = $aConfig['BASE_URL'] . '/create/' . $oItem->id . '/' . $oLocale;
                }
                $aConfig['EDIT_HEADER_BUTTONS'][] = [
                    $aVersions,
                    'Create Version',
                    'btn btn-warning',
                ];
            }
        }

        if ($aConfig['CAN_CREATE'] && static::userCan('create')) {
            $aConfig['INDEX_HEADER_BUTTONS'][] = [
                $aConfig['BASE_URL'] . '/create',
                'Create',
            ];
            $aConfig['EDIT_HEADER_BUTTONS'][]  = [
                $aConfig['BASE_URL'] . '/create',
                'Create',
            ];
        }

        if ($aConfig['CAN_EDIT'] && static::userCan('edit') && classUses($oModel, Sortable::class)) {
            $aConfig['INDEX_HEADER_BUTTONS'][] = [
                $aConfig['BASE_URL'] . '/sort',
                'Set Order',
            ];
        }

        // --------------------------------------------------------------------------

        $bIsLocalised = classUses(static::getModel(), Localised::class);

        /** @var Locale $oLocale */
        $oLocale = Factory::service('Locale');

        $aConfig['INDEX_ROW_BUTTONS'] = array_merge(
            $aConfig['INDEX_ROW_BUTTONS'],
            array_filter([
                [
                    'url'     => '{{url}}',
                    'label'   => lang('action_view'),
                    'class'   => 'btn-default',
                    'attr'    => 'target="_blank"',
                    'enabled' => function ($oItem) {
                        return static::isViewButtonEnabled($oItem);
                    },
                ],

                //  Non-localised buttons
                !$bIsLocalised ? [
                    'url'     => 'edit/{{id}}',
                    'label'   => lang('action_edit'),
                    'class'   => 'btn-primary',
                    'enabled' => function ($oItem) {
                        return static::isEditButtonEnabled($oItem);
                    },
                ] : null,
                !$bIsLocalised ? [
                    'url'     => 'delete/{{id}}',
                    'label'   => lang('action_delete'),
                    'class'   => 'btn-danger confirm',
                    'enabled' => function ($oItem) {
                        return static::isDeleteButtonEnabled($oItem);
                    },
                ] : null,

                //  Localised buttons
                $bIsLocalised ? [
                    'url'     => 'edit/{{id}}/{{locale}}',
                    'label'   => lang('action_edit'),
                    'class'   => 'btn-primary',
                    'enabled' => function ($oItem) {
                        return static::isEditButtonEnabled($oItem);
                    },
                ] : null,
                $bIsLocalised ? [
                    'url'   => function ($oItem) {
                        $aOut = [];
                        foreach ($oItem->missing_locales as $oLocale) {
                            $aOut['Add ' . $oLocale->getDisplayLanguage()] = 'create/{{id}}/' . $oLocale;
                        }
                        return $aOut;
                    },
                    'label' => 'Create Version',
                    'class' => function ($oItem) {
                        if (!static::localisedItemCanBeCreated($oItem)) {
                            return 'btn-warning btn-disabled';
                        }
                        return 'btn-warning';
                    },
                    'attr'  => function ($oItem) {
                        if (!static::localisedItemCanBeCreated($oItem)) {
                            //  @todo (Pablo - 2019-04-17) - Explicitly state why
                            return 'rel="tipsy" title="A new version cannot be created; you may not have permission, ' .
                                'or all supported locales exist already"';
                        }
                        return '';
                    },
                ] : null,
                $bIsLocalised ? [
                    'url'     => 'delete/{{id}}/{{locale}}',
                    'label'   => lang('action_delete'),
                    'class'   => function ($oItem) {
                        if (!static::localisedItemCanBeDeleted($oItem)) {
                            return 'btn-danger btn-disabled';
                        }
                        return 'btn-danger confirm';
                    },
                    'attr'    => function ($oItem) {
                        if (!static::localisedItemCanBeDeleted($oItem)) {
                            //  @todo (Pablo - 2019-04-17) - Explicitly state why
                            return 'rel="tipsy" title="This item cannot be deleted; you may not have permission, ' .
                                'or other locales may exist which need to be deleted first"';
                        }
                        return '';
                    },
                    'enabled' => function ($oItem) use ($oLocale) {
                        return static::isDeleteButtonEnabled($oItem);
                    },
                ] : null,
            ])
        );

        // --------------------------------------------------------------------------

        $this->aConfig        =& $aConfig;
        $this->data['CONFIG'] =& $this->aConfig;
        return $this->aConfig;
    }

    // --------------------------------------------------------------------------

    /**
     * Determines whether a user has permission to create a localised item
     *
     * @param Resource $oItem The item to test
     *
     * @return bool
     */
    protected static function localisedItemCanBeCreated(Resource $oItem)
    {
        //  New versions can only be created if the user has permissions and there is a remaining supported locale
        if (static::CONFIG_CAN_CREATE && static::userCan('create')) {
            return !empty($oItem->missing_locales);
        }
        return false;
    }

    // --------------------------------------------------------------------------

    /**
     * Determines whether a localised item can be delted
     *
     * @param Resource $oItem The item to test
     *
     * @return bool
     * @throws FactoryException
     */
    protected static function localisedItemCanBeDeleted(Resource $oItem)
    {
        /** @var Locale $oLocale */
        $oLocale        = Factory::service('Locale');
        $sDefaultLocale = (string) $oLocale->getDefautLocale();
        $sItemLocale    = (string) $oItem->locale;
        if ($sDefaultLocale !== $sItemLocale) {
            return true;
        } elseif ($sDefaultLocale === $sItemLocale && count($oItem->available_locales) === 1) {
            return true;
        } else {
            return false;
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Returns the string to use as the "single" title
     *
     * @return string
     */
    protected static function getTitleSingle(): string
    {
        if (!empty(static::CONFIG_TITLE_SINGLE)) {
            return static::CONFIG_TITLE_SINGLE;
        }

        $sTitle = preg_replace('/([a-z])([A-Z])/', '$1 $2', static::CONFIG_MODEL_NAME);
        $sTitle = strtolower($sTitle);
        $sTitle = ucwords($sTitle);

        return $sTitle;
    }

    // --------------------------------------------------------------------------

    /**
     * Returns the title to use as the "plural" title
     *
     * @return string
     * @throws FactoryException
     */
    protected static function getTitlePlural(): string
    {
        Factory::helper('inflector');
        return static::CONFIG_TITLE_PLURAL ?: pluralise(2, static::getTitleSingle());
    }

    // --------------------------------------------------------------------------

    /**
     * Returns the value to use for the sidebar group
     *
     * @return string
     * @throws FactoryException
     */
    protected static function getSidebarGroup(): string
    {
        return static::CONFIG_SIDEBAR_GROUP ?: static::getTitlePlural();
    }

    // --------------------------------------------------------------------------

    /**
     * Returns the base URL
     *
     * @return string
     */
    protected static function getBaseUrl(): string
    {
        if (!empty(static::CONFIG_BASE_URL)) {
            return static::CONFIG_BASE_URL;
        }

        $aBits   = explode('\\', get_called_class());
        $sModule = strtolower($aBits[count($aBits) - 2]);
        $sClass  = lcfirst($aBits[count($aBits) - 1]);
        return 'admin/' . $sModule . '/' . $sClass;
    }

    // --------------------------------------------------------------------------

    /**
     * Determines whether a user has permission to perform an action
     *
     * @param string $sPermission The permission to check
     *
     * @return bool
     */
    protected static function userCan(string $sPermission): bool
    {
        return empty(static::CONFIG_PERMISSION) || userHasPermission('admin:' . static::CONFIG_PERMISSION . ':' . $sPermission);
    }

    // --------------------------------------------------------------------------

    /**
     * Returns the model instance
     *
     * @return \Nails\Common\Model\Base
     * @throws FactoryException
     */
    protected static function getModel(): \Nails\Common\Model\Base
    {
        return Factory::model(
            static::CONFIG_MODEL_NAME,
            static::CONFIG_MODEL_PROVIDER
        );
    }

    // --------------------------------------------------------------------------

    /**
     * Any checkbox style filters to include on the index page
     *
     * @return array
     */
    protected function indexCheckboxFilters(): array
    {
        return [];
    }

    // --------------------------------------------------------------------------

    /**
     * Any dropdown style filters to include on the index page
     *
     * @return array
     */
    protected function indexDropdownFilters(): array
    {
        $aFilters = [];
        if (classUses(static::getModel(), Localised::class)) {

            /** @var Locale $oLocale */
            $oLocale = Factory::service('Locale');

            $aOptions   = [];
            $aOptions[] = Factory::factory('IndexFilterOption', 'nails/module-admin')
                ->setLabel('All Locales');

            foreach ($oLocale->getSupportedLocales() as $oSupportedLocale) {
                $aOptions[] = Factory::factory('IndexFilterOption', 'nails/module-admin')
                    ->setLabel($oSupportedLocale->getDisplayLanguage())
                    ->setValue($oSupportedLocale->getLanguage() . '_' . $oSupportedLocale->getRegion());
            }

            $aFilters[] = Factory::factory('IndexFilter', 'nails/module-admin')
                ->setLabel('Locale')
                ->setColumn('CONCAT(`language`, \'_\', `region`)')
                ->addOptions($aOptions);
        }

        return $aFilters;
    }

    // --------------------------------------------------------------------------

    /**
     * Executed before an item is edited
     *
     * @param string   $sMode Whether the action was CREATE or EDIT
     * @param Resource $oItem The old item, before it was edited
     *
     * @return void
     */
    protected function beforeCreateAndEdit($sMode, Resource $oItem = null): void
    {
    }

    // --------------------------------------------------------------------------

    /**
     * Executed before an item is edited
     *
     * @param Resource $oItem The old item, before it was edited
     *
     * @return void
     */
    protected function beforeEdit(Resource $oItem = null): void
    {
    }

    // --------------------------------------------------------------------------

    /**
     * Executed before an item is created
     *
     * @return void
     */
    protected function beforeCreate(): void
    {
    }

    // --------------------------------------------------------------------------

    /**
     * Executed before an item is deleted
     *
     * @param Resource $oItem The item being deleted
     *
     * @return void
     */
    protected function beforeDelete(Resource $oItem): void
    {
    }

    // --------------------------------------------------------------------------

    /**
     * Executed after an item is edited
     *
     * @param string   $sMode    Whether the action was CREATE or EDIT
     * @param Resource $oNewItem The new item, after it was edited
     * @param Resource $oOldItem The old item, before it was edited
     *
     * @return void
     */
    protected function afterCreateAndEdit($sMode, Resource $oNewItem, Resource $oOldItem = null): void
    {
    }

    // --------------------------------------------------------------------------

    /**
     * Executed after an item is edited
     *
     * @param Resource $oNewItem The new item, after it was edited
     * @param Resource $oOldItem The old item, before it was edited
     *
     * @return void
     */
    protected function afterEdit(Resource $oNewItem, Resource $oOldItem = null): void
    {
    }

    // --------------------------------------------------------------------------

    /**
     * Executed after an item is created
     *
     * @param Resource $oNewItem The new item
     *
     * @return void
     */
    protected function afterCreate(Resource $oNewItem): void
    {
    }

    // --------------------------------------------------------------------------

    /**
     * Executed after an item is deleted
     *
     * @param Resource $oItem The deleted item
     *
     * @return void
     */
    protected function afterDelete(Resource $oItem): void
    {
    }

    // --------------------------------------------------------------------------

    /**
     * Form validation for edit/create
     *
     * @param string $sMode      The mode in which the validation is being run
     * @param array  $aOverrides Any overrides for the fields; best to do this in the model's describeFields() method
     *
     * @return void
     * @throws ValidationException
     */
    protected function runFormValidation(string $sMode, array $aOverrides = []): void
    {
        $aConfig              = $this->getConfig();
        $oModel               = static::getModel();
        $oFormValidation      = Factory::service('FormValidation');
        $aRulesFormValidation = [];
        $aImplementedRules    = [];

        foreach ($aConfig['FIELDS'] as &$oField) {

            if ($sMode === static::EDIT_MODE_CREATE && classUses($oModel, Localised::class)) {
                if ($oField->key == 'locale') {

                    /** @var Locale $oLocale */
                    $oLocale = Factory::service('Locale');
                    /** @var Uri $oUri */
                    $oUri = Factory::service('Uri');
                    if (empty($oUri->segment(5))) {
                        $oField->validation[] = 'is[' . $oLocale->getDefautLocale() . ']';
                    }
                }
            }

            if (array_key_exists($oField->key, $aOverrides)) {
                $sRules            = implode('|', $aOverrides[$oField->key]);
                $aImplementedRules = array_merge($aImplementedRules, $aOverrides[$oField->key]);
            } else {
                $sRules            = implode('|', $oField->validation);
                $aImplementedRules = array_merge($aImplementedRules, $oField->validation);
            }

            $aRulesFormValidation[] = [
                'field' => $oField->key,
                'label' => $oField->label,
                'rules' => $sRules,
            ];
        }

        $oFormValidation->set_rules($aRulesFormValidation);

        //  Load up friendly versions of the form validation rules if they exist
        $aImplementedRules = array_map(
            function ($sRule) {
                return preg_replace('/\[.*\]/', '', $sRule);
            },
            $aImplementedRules
        );
        $aImplementedRules = arrayUniqueMulti($aImplementedRules);

        foreach ($aImplementedRules as $sRule) {
            $sMessage = lang('fv_' . $sRule);
            if ($sMessage) {
                $oFormValidation->set_message($sRule, $sMessage);
            }
        }

        if (!$oFormValidation->run()) {
            throw new ValidationException(lang('fv_there_were_errors'));
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Load data for the edit/create view
     *
     * @param Resource $oItem The main item object
     *
     * @return void
     */
    protected function loadEditViewData(Resource $oItem = null): void
    {
        //  Extract the fields into fieldsets
        $aConfig    = $this->getConfig();
        $aFieldSets = array_combine(
            $aConfig['FIELDSET_ORDER'],
            array_pad(
                [],
                count($aConfig['FIELDSET_ORDER']),
                []
            )
        );

        foreach ($aConfig['FIELDS'] as $oField) {

            if (empty($oItem)) {
                if (in_array($oField->key, $aConfig['CREATE_IGNORE_FIELDS'])) {
                    continue;
                }
            } else {
                if (in_array($oField->key, $aConfig['EDIT_IGNORE_FIELDS'])) {
                    continue;
                }
            }

            $sFieldSet = getFromArray('fieldset', (array) $oField, 'Details');

            if (!array_key_exists($sFieldSet, $aFieldSets)) {
                $aFieldSets[$sFieldSet] = [];
            }

            $aFieldSets[$sFieldSet][] = $oField;
        }

        $this->data['aFieldSets'] = array_filter($aFieldSets);
        $this->data['item']       = $oItem;
    }

    // --------------------------------------------------------------------------

    /**
     * Extract data from post variable
     *
     * @return array
     */
    protected function getPostObject(): array
    {
        $aConfig = $this->getConfig();
        $oModel  = static::getModel();
        $oInput  = Factory::service('Input');
        $oUri    = Factory::service('Uri');
        $aOut    = [];

        foreach ($aConfig['FIELDS'] as $oField) {
            if (in_array($oField->key, $aConfig['EDIT_IGNORE_FIELDS'])) {
                continue;
            }

            $aOut[$oField->key] = $oInput->post($oField->key);

            if ($oField->allow_null && empty($aOut[$oField->key])) {
                $aOut[$oField->key] = null;
            }

            //  Type casting
            switch ($oField->type) {
                case 'boolean':
                    $aOut[$oField->key] = (bool) $aOut[$oField->key];
                    break;
            }
        }

        if (classUses($oModel, Localised::class)) {
            $iExistingId = $oUri->segment(5);
            if ($oUri->segment(5)) {
                $aOut['id'] = $oUri->segment(5);
            }
        }

        return $aOut;
    }

    // --------------------------------------------------------------------------

    /**
     * Returns the item being requested, thorwing a 404 if it's not found
     *
     * @param array   $aData    Data to pass to the getById method
     * @param integer $iSegment The URL segment contianing the ID
     *
     * @return mixed
     * @throws FactoryException
     */
    protected function getItem(array $aData = [], int $iSegment = null, bool $bIncludeDeleted = false, bool $b404 = true)
    {
        $iSegment = $iSegment ?? 5;
        $oUri     = Factory::service('Uri');
        $oModel   = $this->getModel();
        $iItemId  = (int) $oUri->segment($iSegment);

        if (classUses($oModel, Localised::class) && $oUri->segment($iSegment + 1)) {
            $aData['USE_LOCALE'] = $oUri->segment($iSegment + 1);
        }

        if (!array_key_exists('where', $aData)) {
            $aData['where'] = [];
        }

        $aData['where'][] = ['id', $iItemId];

        $aItems = $oModel->getAll(
            null,
            null,
            $aData,
            $bIncludeDeleted
        );

        $oItem = reset($aItems);

        if ($b404 && empty($oItem)) {
            show404();
        }

        return $oItem;
    }

    // --------------------------------------------------------------------------

    /**
     * Determines whether the "View" row button is enabled
     *
     * @param Resource $oItem The row item
     *
     * @return bool
     */
    protected static function isViewButtonEnabled($oItem): bool
    {
        return property_exists($oItem, 'url');
    }

    // --------------------------------------------------------------------------

    /**
     * Determines whether the "Edit" row button is enabled
     *
     * @param Resource $oItem The row item
     *
     * @return bool
     */
    protected static function isEditButtonEnabled($oItem): bool
    {
        return static::CONFIG_CAN_EDIT && static::userCan('edit');
    }

    // --------------------------------------------------------------------------

    /**
     * Determines whether the "Delete" row button is enabled
     *
     * @param Resource $oItem The row item
     *
     * @return bool
     */
    protected static function isDeleteButtonEnabled($oItem): bool
    {
        return static::CONFIG_CAN_DELETE && static::userCan('delete');
    }

    // --------------------------------------------------------------------------

    /**
     * Returns the user to the index pagel if a referrer is available then go there instead
     * This is useful for returning the user to a filtered view
     *
     * @throws FactoryException
     */
    protected function returnToIndex(): void
    {
        $oInput    = Factory::service('Input');
        $sReferrer = $oInput->server('HTTP_REFERER');

        if (!empty($sReferrer)) {
            redirect($sReferrer);
        } else {
            redirect($this->getConfig()['BASE_URL']);
        }
    }

    // --------------------------------------------------------------------------

    protected function addHeaderButtons(array $aButtons): void
    {
        foreach ($aButtons as $aButton) {

            $sUrl          = getFromArray(0, $aButton);
            $sLabel        = getFromArray(1, $aButton);
            $sContext      = getFromArray(2, $aButton);
            $sConfirmTitle = getFromArray(3, $aButton);
            $sConfirmBody  = getFromArray(4, $aButton);

            Helper::addHeaderButton(
                $sUrl,
                $sLabel,
                $sContext,
                $sConfirmTitle,
                $sConfirmBody
            );
        }
    }
}


<?php

/**
 * Index filter for admin index views
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Factory
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin\Factory;

use Nails\Admin\Factory\IndexFilter\Option;
use Nails\Common\Exception\NailsException;
use Nails\Factory;

class IndexFilter
{
    /**
     * Stores an array of the getter/setters for the other properties
     *
     * @var array
     */
    protected $aMethods = [];

    /**
     * The label to give the filter
     *
     * @var string
     */
    protected $sLabel;

    /**
     * The column the filter acts on
     *
     * @var string
     */
    protected $sColumn;

    /**
     * An array of options to present to the user
     *
     * @var array
     */
    protected $aOptions = [];

    // --------------------------------------------------------------------------

    /**
     * Base constructor.
     */
    public function __construct()
    {
        $aVars = get_object_vars($this);
        unset($aVars['aMethods']);
        unset($aVars['aOptions']);
        $aVars = array_keys($aVars);

        foreach ($aVars as $sVar) {
            $sNormalised                          = substr($sVar, 1);
            $this->aMethods['set' . $sNormalised] = $sVar;
            $this->aMethods['get' . $sNormalised] = $sVar;
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Mimics setters and getters for class properties
     *
     * @param string $sMethod    The method being called
     * @param array  $aArguments Any passed arguments
     *
     * @return $this
     * @throws \Exception
     */
    public function __call($sMethod, $aArguments)
    {
        if (array_key_exists($sMethod, $this->aMethods)) {
            if (substr($sMethod, 0, 3) === 'set') {
                $this->{$this->aMethods[$sMethod]} = reset($aArguments);
                return $this;
            } else {
                return $this->{$this->aMethods[$sMethod]};
            }
        } else {
            throw new NailsException('Call to undefined method ' . get_called_class() . '::' . $sMethod . '()');
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Adds a new option
     *
     * @param string|Option $sLabel      The label to give the option, or an IndexFilterOption object
     * @param mixed         $mValue      The value to give the option
     * @param bool          $bIsSelected Whether the item is selected
     * @param bool          $bIsQuery    If true, treat the value as the entire query
     *
     * @return $this
     */
    public function addOption($sLabel, $mValue = null, $bIsSelected = false, $bIsQuery = null)
    {
        if ($sLabel instanceof Option) {
            $this->aOptions[] = $sLabel;
        } else {
            $this->aOptions[] = Factory::factory('IndexFilterOption', 'nails/module-admin')
                ->setLabel($sLabel)
                ->setValue($mValue)
                ->setIsSelected($bIsSelected)
                ->setIsQuery($bIsQuery);
        }

        return $this;
    }

    // --------------------------------------------------------------------------

    /**
     * Add multiple options
     *
     * @param array $aOptions An array of options to add
     *
     * @return $this
     */
    public function addOptions($aOptions)
    {
        foreach ($aOptions as $aOption) {
            if ($aOption instanceof Option) {
                $this->aOptions[] = $aOption;
            } else {
                $sLabel      = getFromArray('label', $aOption, getFromArray(0, $aOption));
                $mValue      = getFromArray('value', $aOption, getFromArray(1, $aOption));
                $bIsSelected = (bool) getFromArray('selected', $aOption, getFromArray(2, $aOption));
                $bIsQuery    = (bool) getFromArray('query', $aOption, getFromArray(3, $aOption));
                $this->addOption($sLabel, $mValue, $bIsSelected, $bIsQuery);
            }
        }

        return $this;
    }

    // --------------------------------------------------------------------------

    /**
     * Returns the options
     *
     * @return array
     */
    public function getOptions()
    {
        return $this->aOptions;
    }

    // --------------------------------------------------------------------------

    /**
     * Returns a specific option
     *
     * @param integer $iOptionIndex the option index to return
     *
     * @return mixed|null
     */
    public function getOption($iOptionIndex)
    {
        return array_key_exists($iOptionIndex, $this->aOptions) ? $this->aOptions[$iOptionIndex] : null;
    }
}


<?php
/**
 * This file is part of the ua-device-type package.
 *
 * Copyright (c) 2015-2019, Thomas Mueller <mimmi20@live.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types = 1);
namespace UaDeviceType;

use BrowserDetector\Loader\NotFoundException;

final class TypeLoader implements TypeLoaderInterface
{
    private const OPTIONS = [
        Bot::TYPE                    => Bot::class,
        Brailledisplay::TYPE         => Brailledisplay::class,
        Brailletouch::TYPE           => Brailletouch::class,
        Car::TYPE                    => Car::class,
        CarEntertainmentSystem::TYPE => CarEntertainmentSystem::class,
        Console::TYPE                => Console::class,
        Desktop::TYPE                => Desktop::class,
        DigitalCamera::TYPE          => DigitalCamera::class,
        EbookReader::TYPE            => EbookReader::class,
        FeaturePhone::TYPE           => FeaturePhone::class,
        FonePad::TYPE                => FonePad::class,
        FridgeFreezer::TYPE          => FridgeFreezer::class,
        Laptop::TYPE                 => Laptop::class,
        MediaPlayer::TYPE            => MediaPlayer::class,
        MobileConsole::TYPE          => MobileConsole::class,
        MobileDevice::TYPE           => MobileDevice::class,
        MobileMediaPlayer::TYPE      => MobileMediaPlayer::class,
        MobilePhone::TYPE            => MobilePhone::class,
        NonMobilePhone::TYPE         => NonMobilePhone::class,
        Phablet::TYPE                => Phablet::class,
        Printer::TYPE                => Printer::class,
        SmartDisplay::TYPE           => SmartDisplay::class,
        Smartphone::TYPE             => Smartphone::class,
        Speaker::TYPE                => Speaker::class,
        Tablet::TYPE                 => Tablet::class,
        Tv::TYPE                     => Tv::class,
        TvConsole::TYPE              => TvConsole::class,
        TvMediaPlayer::TYPE          => TvMediaPlayer::class,
        TvSettopBox::TYPE            => TvSettopBox::class,
        TvStick::TYPE                => TvStick::class,
        Unknown::TYPE                => Unknown::class,
        VideoPhone::TYPE             => VideoPhone::class,
        Watch::TYPE                  => Watch::class,
    ];

    /**
     * @param string $key
     *
     * @return bool
     */
    public function has(string $key): bool
    {
        return array_key_exists($key, self::OPTIONS);
    }

    /**
     * @param string $key
     *
     * @throws \BrowserDetector\Loader\NotFoundException
     *
     * @return \UaDeviceType\TypeInterface
     */
    public function load(string $key): TypeInterface
    {
        if (!$this->has($key)) {
            throw new NotFoundException('the device type with key "' . $key . '" was not found');
        }

        $class = self::OPTIONS[$key];

        return new $class();
    }
}


<?php

/**
 * Option for admin index filters
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Factory
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin\Factory\IndexFilter;

use Nails\Common\Exception\NailsException;

class Option
{
    /**
     * Stores an array of the getter/setters for the other properties
     *
     * @var array
     */
    protected $aMethods = [];

    /**
     * Stores an array of the getter/setters for boolean properties
     *
     * @var array
     */
    protected $aBoolMethods = [];

    /**
     * The label to give the option
     *
     * @var string
     */
    protected $sLabel;

    /**
     * The value to give the option
     *
     * @var mixed
     */
    protected $mValue;

    /**
     * Whether the item is selected or not
     *
     * @var boolean
     */
    protected $bIsSelected;

    /**
     * If true, treat the value as the entire query
     *
     * @var boolean
     */
    protected $bIsQuery;

    // --------------------------------------------------------------------------

    /**
     * Base constructor.
     */
    public function __construct()
    {
        $aVars = get_object_vars($this);
        unset($aVars['aMethods']);
        unset($aVars['aBoolMethods']);
        $aVars = array_keys($aVars);

        foreach ($aVars as $sVar) {
            $sNormalised = substr($sVar, 1);
            if (preg_match('/^Is[A-Z]/', $sNormalised)) {
                $this->aBoolMethods['set' . $sNormalised]  = $sVar;
                $this->aBoolMethods[lcfirst($sNormalised)] = $sVar;
            } else {
                $this->aMethods['set' . $sNormalised] = $sVar;
                $this->aMethods['get' . $sNormalised] = $sVar;
            }
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Mimics setters and getters for class properties
     *
     * @param string $sMethod    The method being called
     * @param array  $aArguments Any passed arguments
     *
     * @return $this
     * @throws \Exception
     */
    public function __call($sMethod, $aArguments)
    {
        if (array_key_exists($sMethod, $this->aMethods)) {
            return $this->handleMethodCall($this->aMethods, $sMethod, reset($aArguments));
        } elseif (array_key_exists($sMethod, $this->aBoolMethods)) {
            return $this->handleMethodCall($this->aBoolMethods, $sMethod, (bool) reset($aArguments));
        } else {
            throw new NailsException('Call to undefined method ' . get_called_class() . '::' . $sMethod . '()');
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Executes the appropriate "method" behaviour
     *
     * @param array  $aMethods The array of methods to check against
     * @param string $sMethod  The method being called
     * @param mixed  $mValue   The value to assign when setting
     *
     * @return $this|mixed
     */
    private function handleMethodCall($aMethods, $sMethod, $mValue)
    {
        if (substr($sMethod, 0, 3) !== 'set') {
            return $this->{$aMethods[$sMethod]};
        }

        $this->{$aMethods[$sMethod]} = $mValue;
        return $this;
    }
}


<?php

/**
 * Export Model
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Model
 * @author      Nails Dev Team
 */

namespace Nails\Admin\Model;

use Nails\Common\Model\Base;
use Nails\Factory;

class Export extends Base
{
    /**
     * The various statuses
     */
    const STATUS_PENDING  = 'PENDING';
    const STATUS_RUNNING  = 'RUNNING';
    const STATUS_COMPLETE = 'COMPLETE';
    const STATUS_FAILED   = 'FAILED';

    // --------------------------------------------------------------------------

    /**
     * Export constructor.
     */
    public function __construct()
    {
        parent::__construct();
        $this->table = NAILS_DB_PREFIX . 'admin_export';
    }

    // --------------------------------------------------------------------------

    /**
     * Updates the status of multiple request items in a batch
     *
     * @param array  $aIds    The requests to update
     * @param string $sStatus The status to set
     * @param string $sError  Any error message to set
     */
    public function setBatchStatus(array $aIds, $sStatus, $sError = '')
    {
        if (is_object(reset($aIds))) {
            $aIds = arrayExtractProperty($aIds, 'id');
        }
        if (!empty($aIds)) {
            $oDb = Factory::service('Database');
            $oDb->set('status', $sStatus);
            $oDb->set('modified', 'NOW()', false);
            if ($sError) {
                $oDb->set('error', $sError);
            }
            $oDb->where_in('id', $aIds);
            $oDb->update($this->getTableName());
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Updates the download ID of multiple requests
     *
     * @param array   $aIds        The requests to update
     * @param integer $iDownloadId The download ID
     */
    public function setBatchDownloadId(array $aIds, $iDownloadId)
    {
        if (is_object(reset($aIds))) {
            $aIds = arrayExtractProperty($aIds, 'id');
        }
        if (!empty($aIds)) {
            $oDb = Factory::service('Database');
            $oDb->set('download_id', $iDownloadId);
            $oDb->set('modified', 'NOW()', false);
            $oDb->where_in('id', $aIds);
            $oDb->update($this->getTableName());
        }
    }
}


<?php

/**
 * This class routes all requests in admin to the appropriate place
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Controller
 * @author      Nails Dev Team
 * @link
 */

use Nails\Admin\Exception\RouterException;
use Nails\Components;
use Nails\Factory;

// --------------------------------------------------------------------------

/**
 * Allow the app to add functionality, if needed
 */
if (class_exists('\App\Admin\Controller\BaseRouter')) {
    abstract class BaseMiddle extends \App\Admin\Controller\BaseRouter
    {
    }
} else {
    abstract class BaseMiddle extends \Nails\Common\Controller\Base
    {
    }
}

// --------------------------------------------------------------------------

class AdminRouter extends BaseMiddle
{
    protected $adminControllers;
    protected $adminControllersNav;
    protected $adminControllersNavSticky;
    protected $adminControllersNavStickyBottom;
    protected $adminControllersNavNotSortable;
    protected $adminControllersPermissions;

    // --------------------------------------------------------------------------

    /**
     * Construct the adminRouter, define the sticky groupings
     */
    public function __construct()
    {
        parent::__construct();

        /**
         * Admin nav groupings are sortable by default, if you wish to make any
         * "sticky" then define them here. The order here will be respected i.e.,
         * The first item will stick to the very top, the next beneath it and so
         * forth.
         */

        $this->adminControllersNavSticky = [
            'Dashboard',
            'Utilities',
            'Settings',
        ];

        /**
         * Sticky items by default stick to the top of the nav. If you wish for
         * any to stick to the bottom define them here. The order defined above
         * will be respected, i.e., the order below doesn't matter.
         */

        $this->adminControllersNavStickyBottom = [
            'Utilities',
            'Settings',
        ];

        /**
         * Load helpers we'll need
         */

        Factory::helper('directory');
    }

    // --------------------------------------------------------------------------

    /**
     * Initial touch point for admin, all requests are routed through here.
     *
     * @return void
     */
    public function index()
    {
        //  When executing on the CLI we don't need to perform a few bit's of sense checking
        $oInput = Factory::service('Input');
        if (!$oInput::isCli()) {

            //  Is there an AdminIP whitelist?
            $whitelistIp = (array) appSetting('whitelist', 'admin');

            if ($whitelistIp) {
                if (!isIpInRange($oInput->ipAddress(), $whitelistIp)) {
                    show404();
                }
            }

            //  Before we do anything, is the user an admin?
            if (!isAdmin()) {
                unauthorised();
            }
        }

        //  Determine which admin controllers are available to the system
        $this->findAdminControllers();

        // --------------------------------------------------------------------------

        /**
         * Sort the controllers into a view friendly array, taking into
         * consideration the user's order and state preferences.
         */

        $this->prepAdminControllersNav();

        //  Save adminControllers to controller data so everyone can use it
        $this->data['adminControllers']    = $this->adminControllers;
        $this->data['adminControllersNav'] = $this->adminControllersNav;

        // --------------------------------------------------------------------------

        //  Route the user's request
        $this->routeRequest();
    }

    // --------------------------------------------------------------------------

    /**
     * Searches modules and the app for valid admin controllers which the active
     * user has permission to access.
     *
     * @return void
     */
    protected function findAdminControllers()
    {
        //  Look in the admin module
        $this->loadAdminControllers(
            'admin',
            NAILS_PATH . 'module-admin/admin/controllers/',
            NAILS_APP_PATH . 'application/modules/admin/controllers/',
            ['adminRouter.php']
        );

        //  Look in all enabled modules
        foreach (Components::modules() as $module) {

            /**
             * Skip the admin module. We use the moduleName rather than the component name
             * so that we don't inadvertently load up the admin module (or any module identifying
             * itself as admin) and listing all the files contained therein; we only want
             * admin/controllers.
             */
            if ($module->moduleName == 'admin') {
                continue;
            }

            $this->loadAdminControllers(
                $module->moduleName,
                $module->path . 'admin/controllers/',
                NAILS_APP_PATH . 'application/modules/' . $module->moduleName . '/admin/controllers/'
            );
        }

        //  Finally, look for app admin controllers
        $this->loadAppAdminControllers();
    }

    // --------------------------------------------------------------------------

    /**
     * Looks for admin controllers in specific locations
     *
     * @param  string $moduleName     The name of the module currently being searched
     * @param  string $controllerPath The full path to the controllers
     * @param  string $appPath        If the app can extend these controllers, this is where it will place the file
     * @param  array  $ignore         An array of filenames to ignore
     *
     * @return void
     */
    protected function loadAdminControllers($moduleName, $controllerPath, $appPath, $ignore = [])
    {
        //  Does a path exist? Don't pollute the array with empty modules
        if (is_dir($controllerPath)) {
            //  Look for controllers
            $files = directory_map($controllerPath, 1);

            foreach ($files as $file) {
                if (in_array($file, $ignore)) {
                    continue;
                }

                $this->loadAdminController($file, $moduleName, $controllerPath, $appPath);
            }
        }
    }

    // --------------------------------------------------------------------------

    protected function loadAppAdminControllers()
    {
        $appControllerPath = NAILS_APP_PATH . 'application/modules/admin/controllers/';

        if (is_dir($appControllerPath)) {

            //  Look for controllers
            $files = directory_map($appControllerPath, 1);

            foreach ($files as $file) {

                //  Valid Admin file?
                if (!$this->isValidAdminFile($file)) {
                    continue;
                }

                $fileName = substr($file, 0, strpos($file, '.php'));

                //  Valid file, load it up and define the full class path and name
                require_once $appControllerPath . $file;
                $classPath = $appControllerPath . $file;
                $className = 'App\Admin\\App\\' . ucfirst($fileName);

                //  Load and process the class
                $this->loadAdminClass($fileName, $className, $classPath, 'app');
            }
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Loads a specific admin controller
     *
     * @param  string $file           The file to load
     * @param  string $moduleName     The name of the module currently being searched
     * @param  string $controllerPath The full path to the controller
     * @param  string $appPath        If the app can extend this controllers, this is where it will place the file
     *
     * @return void
     */
    protected function loadAdminController($file, $moduleName, $controllerPath, $appPath)
    {
        $fileName = substr($file, 0, strpos($file, '.php'));

        //  PHP file, no leading underscore
        if (!$this->isValidAdminFile($file)) {
            return;
        }

        //  Valid file, load it up and define the full class path and name
        require_once $controllerPath . $file;
        $classPath = $controllerPath . $file;
        $className = 'Nails\Admin\\' . ucfirst($moduleName) . '\\' . ucfirst($fileName);

        //  If there's an app version of this controller than we'll use that one instead.
        if (is_file($appPath . $file)) {

            require_once $appPath . $file;
            $classPath = $appPath . $file;
            $className = 'App\Admin\\' . ucfirst($moduleName) . '\\' . ucfirst($fileName);

            //  Does the expected class exist? If it doesn't fall back to the previous one
            if (!class_exists($className)) {
                $classPath = $controllerPath . $file;
                $className = 'Nails\Admin\\' . ucfirst($moduleName) . '\\' . ucfirst($fileName);
            }
        }

        //  Load and process the class
        $this->loadAdminClass($fileName, $className, $classPath, $moduleName);
    }

    // --------------------------------------------------------------------------

    /**
     * Determines whether the file being loaded has an acceptable filename.
     *
     * @param  String $file The filename to test
     *
     * @return boolean
     */
    protected function isValidAdminFile($file)
    {
        //  PHP file, no leading underscore
        return preg_match('/^[^_][a-zA-Z_]+\.php$/', $file);
    }

    // --------------------------------------------------------------------------

    /**
     * Attempts to load an AdminClass
     *
     * @param  string $fileName   The filename of the class being loaded
     * @param  string $className  The name of the class being loaded
     * @param  string $classPath  The path of the class being loaded
     * @param  string $moduleName The name of the module to which this class belongs
     *
     * @throws RouterException
     * @return boolean
     */
    protected function loadAdminClass($fileName, $className, $classPath, $moduleName)
    {
        //  Does the expected class exist?
        if (!class_exists($className)) {
            return false;
        }

        //  Does it have an announce method?
        if (!method_exists($className, 'announce')) {
            return false;
        }

        //  Cool! We have a controller which is valid, Add it to the stack!
        if (!isset($this->adminControllers[$moduleName])) {
            $this->adminControllers[$moduleName]              = new \stdClass();
            $this->adminControllers[$moduleName]->controllers = [];
        }

        $aNavGroupings = $className::announce();

        if (!empty($aNavGroupings) && !is_array($aNavGroupings) && !($aNavGroupings instanceof \Nails\Admin\Factory\Nav)) {

            /**
             * @todo Use an admin specific exception class, and autoload it.
             */

            throw new RouterException('Admin Nav groupings returned by ' . $className . '::announce() were invalid', 1);

        } elseif (!is_array($aNavGroupings)) {
            $aNavGroupings = array_filter([$aNavGroupings]);
        }

        $this->adminControllers[$moduleName]->controllers[$fileName] = [
            'className' => (string) $className,
            'path'      => (string) $classPath,
            'groupings' => $aNavGroupings,
        ];

        return true;
    }

    // --------------------------------------------------------------------------

    /**
     * Generates a "view friendly" array of the admin controllers, takes into
     * consideration the user's order and state preferences
     *
     * @return void
     */
    public function prepAdminControllersNav()
    {
        $adminControllersNav = [];

        foreach ($this->adminControllers as $module => $moduleDetails) {
            foreach ($moduleDetails->controllers as $controller => $controllerDetails) {
                foreach ($controllerDetails['groupings'] as $grouping) {

                    //  Begin by defining which group this action belongs in
                    $sGroupLabel = $grouping->getLabel();

                    if (!isset($adminControllersNav[md5($sGroupLabel)])) {
                        $adminControllersNav[md5($sGroupLabel)]           = new \stdClass();
                        $adminControllersNav[md5($sGroupLabel)]->label    = $sGroupLabel;
                        $adminControllersNav[md5($sGroupLabel)]->icon     = [];
                        $adminControllersNav[md5($sGroupLabel)]->sortable = true;
                        $adminControllersNav[md5($sGroupLabel)]->open     = false;
                        $adminControllersNav[md5($sGroupLabel)]->actions  = [];
                    }

                    /**
                     * Add the group icon to the icon array. Multiple calls to the same group
                     * will add their own icon, the most common icon will be used. In the event
                     * of a tie then the first icon defined will be used, unless one has been
                     * marked as !important.
                     */

                    $moduleIcon = $grouping->getIcon();

                    if (!empty($moduleIcon)) {

                        $adminControllersNav[md5($sGroupLabel)]->icon[] = $moduleIcon;
                    }

                    //  Group actions
                    $groupActions = $grouping->getActions();

                    foreach ($groupActions as $actionUrl => $actionDetails) {

                        $url = $module . '/' . lcfirst($controller);
                        $url .= empty($actionUrl) ? '' : '/';
                        $url .= $actionUrl;

                        $adminControllersNav[md5($sGroupLabel)]->actions[$url]         = new \stdClass();
                        $adminControllersNav[md5($sGroupLabel)]->actions[$url]->order  = $actionDetails->order;
                        $adminControllersNav[md5($sGroupLabel)]->actions[$url]->label  = $actionDetails->label;
                        $adminControllersNav[md5($sGroupLabel)]->actions[$url]->alerts = $actionDetails->alerts;
                        $adminControllersNav[md5($sGroupLabel)]->actions[$url]->class  = $controllerDetails['className'];
                        $adminControllersNav[md5($sGroupLabel)]->actions[$url]->path   = $controllerDetails['path'];
                    }
                }
            }
        }

        //  Reset the indexes
        $adminControllersNav = array_values($adminControllersNav);

        //  Remove navGroups with no actions and set as sortable
        $numGroups = count($adminControllersNav);
        for ($i = 0; $i < $numGroups; $i++) {

            //  Remove if empty
            if (empty($adminControllersNav[$i]->actions)) {
                $adminControllersNav[$i] = null;
                continue;
            }

            //  Set sortable
            if (in_array($adminControllersNav[$i]->label, $this->adminControllersNavSticky)) {
                $adminControllersNav[$i]->sortable = false;
            }
        }

        //  Filter and reset the indexes, again
        $adminControllersNav = array_filter($adminControllersNav);
        $adminControllersNav = array_values($adminControllersNav);

        //  Split the groups
        $stickyTop    = [];
        $middle       = [];
        $stickyBottom = [];

        foreach ($this->adminControllersNavSticky as $sticky) {
            foreach ($adminControllersNav as $group) {
                if ($group->label == $sticky) {
                    if (in_array($sticky, $this->adminControllersNavStickyBottom)) {
                        $stickyBottom[] = $group;
                    } else {
                        $stickyTop[] = $group;
                    }
                }
            }
        }

        foreach ($adminControllersNav as $group) {

            if (!in_array($group->label, $this->adminControllersNavSticky)) {
                $middle[] = $group;
            }
        }

        /**
         * Sort everything alphabetically, then loop through the user's prefs and sort
         * by their preferences. If any modules are left over they'll appear at the end,
         * in alphabetical order. Also, set the open state of the module.
         */

        //  Sort alphabetically
        arraySortMulti($middle, 'label');

        //  Get user's prefs
        $oAdminModel = Factory::model('Admin', 'nails/module-admin');
        $userNavPref = $oAdminModel->getAdminData('nav_state');

        if (!empty($userNavPref)) {

            $temp = [];

            foreach ($userNavPref as $groupMd5 => $state) {
                for ($i = 0; $i < count($middle); $i++) {

                    if (empty($middle[$i])) {
                        continue;
                    }

                    if ($groupMd5 == md5($middle[$i]->label)) {
                        if (!in_array($middle[$i]->label, $this->adminControllersNavSticky)) {
                            $temp[]     = $middle[$i];
                            $middle[$i] = null;
                        }
                    }
                }
            }

            //  Filter out the blanks
            $middle = array_filter($middle);
            $middle = array_values($middle);

            //  Merge
            $middle = array_merge($temp, $middle);
        }

        //  Save to the class
        $this->adminControllersNav = array_merge($stickyTop, $middle, $stickyBottom);

        //  Set the open states of the modules
        if (!empty($userNavPref)) {
            foreach ($userNavPref as $groupMd5 => $state) {
                for ($i = 0; $i < count($this->adminControllersNav); $i++) {
                    if ($groupMd5 == md5($this->adminControllersNav[$i]->label)) {
                        $this->adminControllersNav[$i]->open = $state->open;
                    }
                }
            }
        }

        /**
         * Finally, now that everything has been sorted, go through all the groupings
         * and sort their methods alphabetically so there's some feeling of order in
         * amongst all this chaos.
         */

        foreach ($this->adminControllersNav as $grouping) {
            arraySortMulti($grouping->actions, 'order');
        }
    }

    // --------------------------------------------------------------------------

    /**
     * Routes the request to the appropriate controller
     *
     * @return void
     */
    protected function routeRequest()
    {
        //  What are we trying to access?
        $oUri        = Factory::service('Uri');
        $sModule     = $oUri->rsegment(3) ? $oUri->rsegment(3) : '';
        $sController = ucfirst($oUri->rsegment(4) ? $oUri->rsegment(4) : $sModule);
        $sMethod     = $oUri->rsegment(5) ? $oUri->rsegment(5) : 'index';

        if (empty($sModule)) {

            $oSession = Factory::service('Session', 'nails/module-auth');
            $oSession->keepFlashData();
            redirect('admin/admin/dashboard');

        } elseif (isset($this->adminControllers[$sModule]->controllers[$sController])) {

            $aRequestController           = $this->adminControllers[$sModule]->controllers[$sController];
            $this->data['currentRequest'] = $aRequestController;
            $sControllerName              = $aRequestController['className'];

            if (is_callable([$sControllerName, $sMethod])) {
                $oController = new $sControllerName();
                $oController->$sMethod();
            } else {
                show404();
            }

        } else {
            show404();
        }
    }
}