<?php

/**
 * This class provides a number of reuseable static methods which Admin Controllers can make use of
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Helper
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin;

use Nails\Common\Exception\NailsException;
use Nails\Common\Exception\ViewNotFoundException;
use Nails\Factory;

class Helper
{
    protected static $aHeaderButtons = [];

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

    /**
     * Loads a view in admin taking into account the module being accessed. Passes controller
     * data and optionally loads the header and footer views.
     *
     * @param  string  $sViewFile      The view to load
     * @param  boolean $bLoadStructure Whether or not to include the header and footers in the output
     * @param  boolean $bReturnView    Whether to return the view or send it to the Output class
     *
     * @return mixed                  String when $bReturnView is true, void otherwise
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadView($sViewFile, $bLoadStructure = true, $bReturnView = false)
    {
        $aData  =& getControllerData();
        $oInput = Factory::service('Input');
        $oView  = Factory::service('View');

        //  Are we in a modal?
        if ($oInput->get('isModal')) {

            if (!isset($aData['headerOverride']) && !isset($aData['isModal'])) {
                $aData['isModal'] = true;
            }

            if (empty($aData['headerOverride'])) {
                $aData['headerOverride'] = '_components/structure/headerBlank';
            }

            if (empty($aData['footerOverride'])) {
                $aData['footerOverride'] = '_components/structure/footerBlank';
            }
        }

        $aHeaderViews = array_filter([
            $bLoadStructure && !empty($aData['headerOverride']) ? $aData['headerOverride'] : '',
            $bLoadStructure && empty($aData['headerOverride']) ? '_components/structure/header' : '',
        ]);
        $sHeaderView  = reset($aHeaderViews);

        $aFooterViews = array_filter([
            $bLoadStructure && !empty($aData['footerOverride']) ? $aData['footerOverride'] : '',
            $bLoadStructure && empty($aData['footerOverride']) ? '_components/structure/footer' : '',
        ]);
        $sFooterView  = reset($aFooterViews);

        if ($bReturnView) {
            $sOut = $oView->load($sHeaderView, $aData, true);
            $sOut .= static::loadInlineView($sViewFile, $aData, true);
            $sOut .= $oView->load($sFooterView, $aData, true);
            return $sOut;
        } else {
            $oView->load($sHeaderView, $aData);
            static::loadInlineView($sViewFile, $aData);
            $oView->load($sFooterView, $aData);
        }
    }

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

    /**
     * Generates a CSV and sends to the browser, if a filename is given then it's
     * sent as a download
     *
     * @param  mixed   $mData      The data to render, either an array or a DB query object
     * @param  string  $sFilename  The filename to give the file if downloading
     * @param  boolean $bHeaderRow The first element in the $mData resultset is a header row
     *
     * @return void
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadCsv($mData, $sFilename = '', $bHeaderRow = true)
    {
        //  Determine what type of data has been supplied
        if (is_array($mData) || get_class($mData) == 'CI_DB_mysqli_result') {

            //  If filename has been specified then set some additional headers
            if (!empty($sFilename)) {

                $oInput  = Factory::service('Input');
                $oOutput = Factory::service('Output');

                //  Common headers
                $oOutput->set_content_type('text/csv');
                $oOutput->set_header('Content-Disposition: attachment; filename="' . $sFilename . '"');
                $oOutput->set_header('Expires: 0');
                $oOutput->set_header("Content-Transfer-Encoding: binary");

                //  Handle IE, classic.
                $userAgent = $oInput->server('HTTP_USER_AGENT');

                if (strpos($userAgent, "MSIE") !== false) {

                    $oOutput->set_header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
                    $oOutput->set_header('Pragma: public');

                } else {

                    $oOutput->set_header('Pragma: no-cache');
                }
            }

            //  Not using self::loadInlineView() as this may be called from many contexts
            $oView = Factory::service('View');
            if (is_array($mData)) {
                $oView->load('admin/_components/csv/array', ['data' => $mData, 'header' => $bHeaderRow]);
            } elseif (get_class($mData) == 'CI_DB_mysqli_result') {
                $oView->load('admin/_components/csv/dbResult', ['data' => $mData, 'header' => $bHeaderRow]);
            }

        } else {
            throw new NailsException('Unsupported object type passed to ' . get_class() . '::loadCSV');
        }
    }

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

    /**
     * Load a single view taking into account the module being accessed.
     *
     * @param  string  $sViewFile   The view to load
     * @param  array   $aViewData   The data to pass to the view
     * @param  boolean $bReturnView Whether to return the view or send it to the Output class
     *
     * @throws \Exception
     *
     * @return mixed               String when $bReturnView is true, void otherwise
     */
    public static function loadInlineView($sViewFile, $aViewData = [], $bReturnView = false)
    {
        $aCtrlData   =& getControllerData();
        $sCtrlPath   = !empty($aCtrlData['currentRequest']['path']) ? $aCtrlData['currentRequest']['path'] : '';
        $sCtrlName   = basename($sCtrlPath, '.php');
        $aCtrlPath   = explode(DIRECTORY_SEPARATOR, $sCtrlPath);
        $aCtrlPath   = array_splice($aCtrlPath, 0, count($aCtrlPath) - 2);
        $aCtrlPath[] = 'views';
        $aCtrlPath[] = $sCtrlName;
        $aCtrlPath[] = $sViewFile;
        $sViewPath   = implode(DIRECTORY_SEPARATOR, $aCtrlPath) . '.php';

        $oView = Factory::service('View');

        //  Load the view
        try {
            return $oView->load($sViewPath, $aViewData, $bReturnView);
        } catch (ViewNotFoundException $e) {
            //  If it fails, and the controller is a default admin controller then load up that view
            $sClassName = $aCtrlData['currentRequest']['className'];
            if (!classExtends($sClassName, 'Nails\\Admin\\Controller\\DefaultController')) {
                throw new ViewNotFoundException(
                    $e->getMessage(),
                    $e->getCode()
                );
            }

            //  Step through the class hierarchy and look there
            $aParents = class_parents($sClassName);
            foreach ($aParents as $sParent) {
                try {

                    if ($sParent !== 'Nails\\Admin\\Controller\\DefaultController') {

                        $oReflection = new \ReflectionClass('\\' . $sParent);
                        $sViewPath   = realpath(dirname($oReflection->getFileName()) . '/../views') . '/';
                        $aClassBits  = explode('\\', $oReflection->getName());
                        $sViewPath   .= end($aClassBits) . '/';

                    } else {
                        $sViewPath     = 'admin/DefaultController/';
                        $bTriedDefault = true;
                    };

                    return $oView->load(
                        $sViewPath . $sViewFile,
                        $aViewData,
                        $bReturnView
                    );
                } catch (ViewNotFoundException $e) {
                    //  Allow the loop to continue, unless we've already tried the default views
                    if (!empty($bTriedDefault)) {
                        throw $e;
                    }
                }
            }
        }
    }

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

    /**
     * Loads the admin "search" component
     *
     * @param  \stdClass $oSearchObj  An object as created by self::searchObject();
     * @param  boolean   $bReturnView Whether to return the view to the caller, or output to the browser
     *
     * @return mixed                  String when $bReturnView is true, void otherwise
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadSearch($oSearchObj, $bReturnView = true)
    {
        $aData = [
            'searchable'     => isset($oSearchObj->searchable) ? $oSearchObj->searchable : true,
            'sortColumns'    => isset($oSearchObj->sortColumns) ? $oSearchObj->sortColumns : [],
            'sortOn'         => isset($oSearchObj->sortOn) ? $oSearchObj->sortOn : null,
            'sortOrder'      => isset($oSearchObj->sortOrder) ? $oSearchObj->sortOrder : null,
            'perPage'        => isset($oSearchObj->perPage) ? $oSearchObj->perPage : 50,
            'keywords'       => isset($oSearchObj->keywords) ? $oSearchObj->keywords : '',
            'checkboxFilter' => isset($oSearchObj->checkboxFilter) ? $oSearchObj->checkboxFilter : [],
            'dropdownFilter' => isset($oSearchObj->dropdownFilter) ? $oSearchObj->dropdownFilter : [],
        ];

        //  Not using self::loadInlineView() as this may be called from many contexts
        $oView = Factory::service('View');
        return $oView->load('admin/_components/search', $aData, $bReturnView);
    }

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

    /**
     * Creates a standard object designed for use with self::loadSearch()
     *
     * @param  boolean $bSearchable     Whether the result set is keyword searchable
     * @param  array   $aSortColumns    An array of columns to sort results by
     * @param  string  $sSortOn         The column to sort on
     * @param  string  $sSortOrder      The order to sort results in
     * @param  integer $iPerPage        The number of results to show per page
     * @param  string  $sKeywords       Keywords to apply to the search result
     * @param  array   $aCheckboxFilter An array of filters to filter the results by, presented as checkboxes
     * @param  array   $aDropdownFilter An array of filters to filter the results by, presented as a dropdown
     *
     * @return \stdClass
     */
    public static function searchObject(
        $bSearchable,
        $aSortColumns,
        $sSortOn,
        $sSortOrder,
        $iPerPage,
        $sKeywords = '',
        $aCheckboxFilter = [],
        $aDropdownFilter = []
    ) {
        return (object) [
            'searchable'     => $bSearchable,
            'sortColumns'    => $aSortColumns,
            'sortOn'         => $sSortOn,
            'sortOrder'      => $sSortOrder,
            'perPage'        => $iPerPage,
            'keywords'       => $sKeywords,
            'checkboxFilter' => $aCheckboxFilter,
            'dropdownFilter' => $aDropdownFilter,
        ];
    }

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

    /**
     * Creates a standard object designed for use with self::searchObject()'s
     * $checkboxFilter and $dropdownFilter parameters
     *
     * @param  string $sColumn  The name of the column to filter on, leave blank if you do not wish to use Nails's
     *                          automatic filtering
     * @param  string $sLabel   The label to give the filter group
     * @param  array  $aOptions An array of options for the dropdown, either key => value pairs or a 3 element array: 0
     *                          = label, 1 = value, 2 = default check status
     *
     * @return \stdClass
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function searchFilterObject($sColumn, $sLabel, $aOptions)
    {
        //  @todo (Pablo - 2018-04-10) - DonRemove this helper and use factories directly
        $oFilter = Factory::factory('IndexFilter', 'nails/module-admin')
            ->setLabel($sLabel)
            ->setColumn($sColumn);

        foreach ($aOptions as $sIndex => $mOption) {

            if (is_array($mOption)) {
                $sLabel   = getFromArray(0, $mOption, null);
                $mValue   = getFromArray(1, $mOption, null);
                $bChecked = getFromArray(2, $mOption, false);
                $bQuery   = getFromArray(3, $mOption, false);
            } else {
                $sLabel   = $mOption;
                $mValue   = $sIndex;
                $bChecked = false;
                $bQuery   = false;
            }

            $oFilter->addOption($sLabel, $mValue, $bChecked, $bQuery);
        }

        return $oFilter;
    }

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

    /**
     * Creates a standard object which is an option for self::searchFilterObject()
     *
     * @param  string  $sLabel   The label to give the option
     * @param  string  $sValue   The value to give the option (filters self::searchFilterObject's $sColumn parameter)
     * @param  boolean $bChecked Whether the value is checked by default
     * @param bool     $bQuery   Whether the supplied value is an SQL query
     *
     * @return \stdClass
     */
    public static function searchFilterObjectOption($sLabel = '', $sValue = '', $bChecked = false, $bQuery = false)
    {
        return (object) [
            'label'   => $sLabel,
            'value'   => $sValue,
            'checked' => $bChecked,
            'query'   => $bQuery,
        ];
    }

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

    /**
     * Returns a value from a filter object at a specific key
     *
     * @param  \stdClass $oFilterObj The filter object to search
     * @param  integer   $iKey       The key to inspect
     *
     * @return mixed                  Mixed on success, null on failure
     */
    public static function searchFilterGetValueAtKey($oFilterObj, $iKey)
    {
        return isset($oFilterObj->options[$iKey]->value) ? $oFilterObj->options[$iKey]->value : null;
    }

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

    /**
     * Loads the admin "pagination" component
     *
     * @param  \stdClass $oPaginationObject An object as created by self::paginationObject();
     * @param  boolean   $bReturnView       Whether to return the view to the caller, or output to the browser
     *
     * @return mixed                      String when $bReturnView is true, void otherwise
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadPagination($oPaginationObject, $bReturnView = true)
    {
        $aData = [
            'page'      => isset($oPaginationObject->page) ? $oPaginationObject->page : null,
            'perPage'   => isset($oPaginationObject->perPage) ? $oPaginationObject->perPage : null,
            'totalRows' => isset($oPaginationObject->totalRows) ? $oPaginationObject->totalRows : null,
        ];

        //  Not using self::loadInlineView() as this may be called from many contexts
        $oView = Factory::service('View');
        return $oView->load('admin/_components/pagination', $aData, $bReturnView);
    }

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

    /**
     * Creates a standard object designed for use with self::loadPagination();
     *
     * @param  integer $iPage      The current page number
     * @param  integer $iPerPage   The number of results per page
     * @param  integer $iTotalRows The total number of results in the result set
     *
     * @return \stdClass
     */
    public static function paginationObject($iPage, $iPerPage, $iTotalRows)
    {
        return (object) [
            'page'      => $iPage,
            'perPage'   => $iPerPage,
            'totalRows' => $iTotalRows,
        ];
    }

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

    /**
     * Automatically decides what type of cell to load
     *
     * @param mixed  $mValue          The value of the cell
     * @param string $sCellClass      Any classes to add to the cell
     * @param string $sCellAdditional Any additional HTML to add to the cell (after the value)
     *
     * @return string
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadCellAuto($mValue, $sCellClass = '', $sCellAdditional = '')
    {
        //  @todo - handle more field types
        if (is_bool($mValue)) {
            return Helper::loadBoolCell($mValue);
        } elseif (preg_match('/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $mValue)) {
            return Helper::loadDateTimeCell($mValue);
        } elseif (preg_match('/\d\d\d\d-\d\d-\d\d/', $mValue)) {
            return Helper::loadDateCell($mValue);
        } else {
            return '' . $mValue . $sCellAdditional . '	';
        }
    }

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

    /**
     * Load the admin "user" table cell component
     *
     * @param  mixed $mUser The user object or the User's ID/email/username
     *
     * @return string
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadUserCell($mUser)
    {
        if (is_numeric($mUser)) {

            $oUserModel = Factory::model('User', 'nails/module-auth');
            $oUser      = $oUserModel->getById($mUser);

        } elseif (is_string($mUser)) {

            $oUserModel = Factory::model('User', 'nails/module-auth');
            $oUser      = $oUserModel->getByEmail($mUser);

            if (empty($oUser)) {
                $oUser = $oUserModel->getByUsername($mUser);
            }

        } else {
            $oUser = $mUser;
        }

        $aUser = [
            'id'          => !empty($oUser->id) ? $oUser->id : null,
            'profile_img' => !empty($oUser->profile_img) ? $oUser->profile_img : null,
            'gender'      => !empty($oUser->gender) ? $oUser->gender : null,
            'first_name'  => !empty($oUser->first_name) ? $oUser->first_name : null,
            'last_name'   => !empty($oUser->last_name) ? $oUser->last_name : null,
            'email'       => !empty($oUser->email) ? $oUser->email : null,
        ];

        $oView = Factory::service('View');
        return $oView->load('admin/_components/table-cell-user', $aUser, true);
    }

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

    /**
     * Load the admin "date" table cell component
     *
     * @param  string $sDate   The date to render
     * @param  string $sNoData What to render if the date is invalid or empty
     *
     * @return string
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadDateCell($sDate, $sNoData = '—')
    {
        $aData = [
            'date'   => $sDate,
            'noData' => $sNoData,
        ];

        $oView = Factory::service('View');
        return $oView->load('admin/_components/table-cell-date', $aData, true);
    }

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

    /**
     * Load the admin "dateTime" table cell component
     *
     * @param  string $sDateTime The dateTime to render
     * @param  string $sNoData   What to render if the datetime is invalid or empty
     *
     * @return string
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadDateTimeCell($sDateTime, $sNoData = '—')
    {
        $aData = [
            'dateTime' => $sDateTime,
            'noData'   => $sNoData,
        ];

        $oView = Factory::service('View');
        return $oView->load('admin/_components/table-cell-datetime', $aData, true);
    }

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

    /**
     * Load the admin "boolean" table cell component
     *
     * @param  string $value     The value to 'truthy' test
     * @param  string $sDateTime A datetime to show (for truthy values only)
     *
     * @return string
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadBoolCell($value, $sDateTime = null)
    {
        $aData = [
            'value'    => $value,
            'dateTime' => $sDateTime,
        ];

        $oView = Factory::service('View');
        return $oView->load('admin/_components/table-cell-boolean', $aData, true);
    }

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

    /**
     * Load the admin "settings component table" component
     *
     * @param  string $sComponentService The component service to use
     * @param  array  $sProvider         The model provider
     * @param  string $sComponentType    The type of component being loaded
     *
     * @return string
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadSettingsComponentTable($sComponentService, $sProvider, $sComponentType = 'component')
    {
        $oModel          = Factory::service($sComponentService, $sProvider);
        $sKey            = $oModel->getSettingKey();
        $aComponents     = $oModel->getAll();
        $aEnabled        = (array) $oModel->getEnabledSlug();
        $bEnableMultiple = $oModel->isMultiple();

        $aData = [
            'key'               => $sKey,
            'components'        => $aComponents,
            'enabled'           => $aEnabled,
            'canSelectMultiple' => $bEnableMultiple,
            'componentType'     => $sComponentType,
        ];

        $oView = Factory::service('View');
        return $oView->load('admin/_components/settings-component-table', $aData, true);
    }

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

    /**
     * Alias to loadSettingsComponentTable()
     *
     * @param  string $sDriverService The driver service to use
     * @param  array  $sProvider      The model provider
     *
     * @return string
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadSettingsDriverTable($sDriverService, $sProvider)
    {
        return self::loadSettingsComponentTable($sDriverService, $sProvider, 'driver');
    }

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

    /**
     * Alias to loadSettingsComponentTable()
     *
     * @param  string $sSkinService The skin service to use
     * @param  array  $sProvider    The model provider
     *
     * @return string
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function loadSettingsSkinTable($sSkinService, $sProvider)
    {
        return self::loadSettingsComponentTable($sSkinService, $sProvider, 'skin');
    }

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

    /**
     * Adds a button to Admin's header area
     *
     * @param string $sUrl          The button's URL
     * @param string $sLabel        The button's label
     * @param string $sContext      The button's context
     * @param string $sConfirmTitle If a confirmation is required, the title to use
     * @param string $sConfirmBody  If a confirmation is required, the body to use
     */
    public static function addHeaderButton(
        $sUrl,
        $sLabel,
        $sContext = null,
        $sConfirmTitle = null,
        $sConfirmBody = null
    ) {
        $sContext = empty($sContext) ? 'primary' : $sContext;

        self::$aHeaderButtons[] = [
            'url'          => $sUrl,
            'label'        => $sLabel,
            'context'      => $sContext,
            'confirmTitle' => $sConfirmTitle,
            'confirmBody'  => $sConfirmBody,
        ];
    }

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

    /**
     * Returns the admin header buttons
     *
     * @return array
     */
    public static function getHeaderButtons()
    {
        return self::$aHeaderButtons;
    }

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

    /**
     * Generates a dynamic table
     *
     * @param string $sKey    The key to give items in the table
     * @param array  $aFields The fields to render
     * @param array  $aData   Data to populate the table with
     *
     * @return string
     * @throws \Nails\Common\Exception\FactoryException
     */
    public static function dynamicTable($sKey, array $aFields, array $aData = [])
    {
        return Factory::service('View')
            ->load(
                'admin/_components/dynamic-table',
                [
                    'sKey'    => $sKey,
                    'aFields' => $aFields,
                    'aData'   => $aData,
                ],
                true
            );
    }
}


<?php

/**
 * Admin API end points: Nav
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Controller
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin\Api\Controller;

use Nails\Admin\Controller\BaseApi;
use Nails\Api\Exception\ApiException;
use Nails\Factory;

class Nav extends BaseApi
{
    const REQUIRE_AUTH = true;

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

    /**
     * Determines whether the user is authenticated or not
     *
     * @param string $sHttpMethod The HTTP Method protocol being used
     * @param string $sMethod     The controller method being executed
     *
     * @return bool
     */
    public static function isAuthenticated($sHttpMethod = '', $sMethod = '')
    {
        return parent::isAuthenticated($sHttpMethod, $sMethod) && isAdmin();
    }

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

    /**
     * Saves the user's admin nav preferences
     * @return \Nails\Api\Factory\ApiResponse
     */
    public function postSave()
    {
        $oInput   = Factory::service('Input');
        $aPrefRaw = array_filter((array) $oInput->post('preferences'));
        $oPref    = new \stdClass();

        foreach ($aPrefRaw as $sModule => $aOptions) {
            $oPref->{$sModule}       = new \stdClass();
            $oPref->{$sModule}->open = stringToBoolean($aOptions['open']);
        }

        $oAdminModel = Factory::model('Admin', 'nails/module-admin');
        $oAdminModel->setAdminData('nav_state', $oPref);
        return Factory::factory('ApiResponse', 'nails/module-api');
    }

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

    /**
     * Resets a user's admin nav preferences
     * @return \Nails\Api\Factory\ApiResponse
     */
    public function postReset()
    {
        $oAdminModel = Factory::model('Admin', 'nails/module-admin');
        $oAdminModel->unsetAdminData('nav_state');
        return Factory::factory('ApiResponse', 'nails/module-api');
    }
}


<?php

namespace Nails\Admin\Console\Command\DataExport;

use Nails\Admin\Exception\Console\ControllerExistsException;
use Nails\Common\Exception\NailsException;
use Nails\Console\Command\BaseMaker;
use Nails\Factory;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class Create extends BaseMaker
{
    const RESOURCE_PATH = NAILS_PATH . 'module-admin/resources/console/';
    const EXPORT_PATH   = NAILS_APP_PATH . 'src/DataExport/Source/';

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

    /**
     * Configure the command
     */
    protected function configure(): void
    {
        $this
            ->setName('make:dataexport')
            ->setDescription('Creates a new Admin DataExport Source')
            ->addArgument(
                'name',
                InputArgument::OPTIONAL,
                'Define the name of the data export'
            )
            ->addArgument(
                'description',
                InputArgument::OPTIONAL,
                'Define the description of the data export'
            );
    }

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

    /**
     * Executes the app
     *
     * @param InputInterface  $oInput  The Input Interface provided by Symfony
     * @param OutputInterface $oOutput The Output Interface provided by Symfony
     *
     * @return int
     */
    protected function execute(InputInterface $oInput, OutputInterface $oOutput): int
    {
        parent::execute($oInput, $oOutput);

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

        try {
            //  Ensure the paths exist
            $this->createPath(self::EXPORT_PATH);
            //  Create the DataExport source
            $this->createSource();
        } catch (\Exception $e) {
            return $this->abort(
                self::EXIT_CODE_FAILURE,
                [$e->getMessage()]
            );
        }

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

        //  Cleaning up
        $oOutput->writeln('');
        $oOutput->writeln('<comment>Cleaning up...</comment>');

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

        //  And we're done
        $oOutput->writeln('');
        $oOutput->writeln('Complete!');

        return self::EXIT_CODE_SUCCESS;
    }

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

    /**
     * Create the Model
     *
     * @return void
     * @throws \Exception
     */
    private function createSource(): void
    {
        $aFields               = $this->getArguments();
        $aFields['CLASS_NAME'] = str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z ]/', '', $aFields['NAME'])));
        $aFields['FILENAME']   = strtolower(url_title($aFields['NAME']));

        try {

            $this->oOutput->write('Creating DataExport Source <comment>' . $aFields['CLASS_NAME'] . '</comment>... ');

            //  Check for existing DataExport source
            $sPath = static::EXPORT_PATH . $aFields['CLASS_NAME'] . '.php';
            if (file_exists($sPath)) {
                throw new ControllerExistsException(
                    'DataExport Source "' . $aFields['CLASS_NAME'] . '" exists already at path "' . $sPath . '"'
                );
            }

            $this->createFile($sPath, $this->getResource('template/data_export_source.php', $aFields));
            $aCreated[] = $sPath;
            $this->oOutput->writeln('<info>done!</info>');

        } catch (\Exception $e) {
            $this->oOutput->writeln('<error>failed!</error>');
            throw new NailsException($e->getMessage());
        }
    }
}


<?php

/**
 * Admin help model
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Model
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin\Model;

use Nails\Common\Model\Base;

class Help extends Base
{
    /**
     * Constructs the model
     */
    public function __construct()
    {
        parent::__construct();
        $this->table      = NAILS_DB_PREFIX . 'admin_help_video';
        $this->tableAlias = 'hv';
    }

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

    /**
     * This method applies the conditionals which are common across the get_*()
     * methods and the count() method.
     *
     * @param array $data Data passed from the calling method
     *
     * @return void
     **/
    protected function getCountCommon(array $data = [])
    {
        if (!empty($data['keywords'])) {

            if (empty($data['or_like'])) {

                $data['or_like'] = [];
            }

            $data['or_like'][] = [
                'column' => $this->tableAlias . '.label',
                'value'  => $data['keywords'],
            ];
            $data['or_like'][] = [
                'column' => $this->tableAlias . '.description',
                'value'  => $data['keywords'],
            ];
        }

        parent::getCountCommon($data);
    }
}


<?php

namespace Nails\Admin\DataExport\Format;

use Nails\Admin\DataExport\SourceResponse;
use Nails\Admin\Interfaces\DataExport\Format;

/**
 * Class Csv
 * @package Nails\Auth\DataFormat
 */
class Csv implements Format
{
    /**
     * Returns the format's label
     * @return string
     */
    public function getLabel()
    {
        return 'CSV';
    }

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

    /**
     * Returns the format's description
     * @return string
     */
    public function getDescription()
    {
        return 'Easily imports to many software packages, including Microsoft Excel.';
    }

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

    /**
     * Returns the format's file extension
     * @return string
     */
    public function getFileExtension()
    {
        return 'csv';
    }

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

    /**
     * Takes a SourceResponse object and transforms it into the appropriate format.
     *
     * @param SourceResponse $oSourceResponse A SourceResponse object
     * @param resource       $rFile           The file resource to write to
     */
    public function execute($oSourceResponse, $rFile)
    {
        //  Write the field names to the file
        fwrite($rFile, $this->formatRow($oSourceResponse->getFields()));

        //  Write the data to the file
        while ($oRow = $oSourceResponse->getNextItem()) {
            fwrite($rFile, $this->formatRow($oRow));
        }
    }

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

    /**
     * Escapes items to be written to the CSV
     *
     * @param \stdClass|array $oRow The row to format
     *
     * @return string
     */
    protected function formatRow($oRow)
    {
        $aItems = array_map(
            function ($sItem) {
                return str_replace('"', '""', trim($sItem));
            },
            (array) $oRow
        );

        return '"' . implode('","', $aItems) . '"' . "\n";
    }
}


<?php

/**
 * This class is used for building navGroups in admin
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Helper
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin;

class Nav
{
    protected $label;
    protected $icon;
    protected $actions;

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

    /**
     * Construct the navGroup
     *
     * @param string $label The label to give the navGroup
     * @param string $icon  The icon to give the navGroup
     */
    public function __construct($label = '', $icon = '')
    {
        $this->setLabel($label);
        $this->setIcon($icon);
        $this->actions = [];
    }

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

    /**
     * Set the navGroup's label
     *
     * @param string $label
     *
     * @return $this
     */
    public function setLabel($label = '')
    {
        $this->label = $label;
        return $this;
    }

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

    /**
     * Returns the navGroup's label
     * @return string
     */
    public function getLabel()
    {
        return $this->label;
    }

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

    /**
     * Set the navGroup's icon
     *
     * @param string $icon
     *
     * @return string
     */
    public function setIcon($icon)
    {
        $this->icon = $icon;
        return $this;
    }

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

    /**
     * Returns the navGroup's icon
     * @return string
     */
    public function getIcon()
    {
        return $this->icon;
    }

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

    /**
     * Returns the navGroup's actions
     * @return array
     */
    public function getActions()
    {
        return $this->actions;
    }

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

    /**
     * Adds a new action to the navGroup. An action is menu item essentially.
     *
     * @param string $label  The label to give the action
     * @param string $url    The url this action applies to
     * @param array  $alerts An array of alerts to have along side this action
     * @param mixed  $order  An optional order index, used to push menu items up and down the group
     *
     * @return Nav      $this, for chaining
     */
    public function addAction($label, $url = 'index', $alerts = [], $order = null)
    {
        $this->actions[$url]         = new \stdClass();
        $this->actions[$url]->label  = $label;
        $this->actions[$url]->alerts = !is_array($alerts) ? [$alerts] : $alerts;

        if (is_null($order)) {
            $this->actions[$url]->order = count($this->actions);
        } else {
            $this->actions[$url]->order = $order;
        }

        return $this;
    }

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

    /**
     * Removes a action
     *
     * @param  string $url The URL/key of the action to remove
     *
     * @return Nav
     */
    public function removeAction($url)
    {
        unset($this->actions[$url]);

        return $this;
    }
}


<?php

/**
 * Admin model
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Model
 * @author      Nails Dev Team
 * @todo (Pablo - 2019-03-22) - This isn't really a model and should be moved to a service
 */

namespace Nails\Admin\Model;

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

class Admin extends Base
{
    protected $oUserMetaService;
    protected $aJsonFields;

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

    /**
     * Admin constructor.
     *
     * @throws \Nails\Common\Exception\FactoryException
     */
    public function __construct()
    {
        $this->oUserMetaService = Factory::service('UserMeta', 'nails/module-auth');
        $this->aJsonFields      = [
            'nav_state',
        ];
    }

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

    /**
     * Sets a piece of admin data
     *
     * @param  string $key    The key to set
     * @param  mixed  $value  The value to set
     * @param  mixed  $userId The user's ID, if null active user is used.
     *
     * @return boolean
     */
    public function setAdminData($key, $value, $userId = null)
    {
        return $this->setUnsetAdminData($key, $value, $userId, true);
    }

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

    /**
     * Unsets a piece of admin data
     *
     * @param  string $key    The key to set
     * @param  mixed  $userId The user's ID, if null active user is used.
     *
     * @return boolean
     */
    public function unsetAdminData($key, $userId = null)
    {
        return $this->setUnsetAdminData($key, null, $userId, false);
    }

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

    /**
     * Handles the setting and unsetting of admin data
     *
     * @param  string  $key    The key to set
     * @param  mixed   $value  The value to set
     * @param  mixed   $userId The user's ID, if null active user is used.
     * @param  boolean $set    Whether the data is being set or unset
     *
     * @return boolean
     */
    protected function setUnsetAdminData($key, $value, $userId, $set)
    {
        //  Get the user ID
        $userId = $this->adminDataGetUserId($userId);

        //  Get the existing data for this user
        $existing = $this->getAdminData(null, $userId);

        if ($set) {

            //  Set the new key
            if (in_array($key, $this->aJsonFields)) {
                $value = json_encode($value);
            }
            $existing[$key] = $value;

        } else {

            //  Unset the existing key
            $existing[$key] = null;
        }

        //  Save to the DB
        $bResult = $this->oUserMetaService->update(
            NAILS_DB_PREFIX . 'user_meta_admin',
            $userId,
            $existing
        );
    }

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

    /**
     * Gets items from the admin data, or the entire array of $key is null
     *
     * @param  string $key    The key to set
     * @param  mixed  $userId The user's ID, if null active user is used.
     *
     * @return mixed
     */
    public function getAdminData($key = null, $userId = null)
    {
        //  Get the user ID
        $userId = $this->adminDataGetUserId($userId);

        //  Check if data is already in the cache
        $cacheKey = 'admin-data-' . $userId;
        $cache    = $this->getCache($cacheKey);

        if ($cache) {

            $data = $cache;

        } else {

            $oRow = $this->oUserMetaService->get(NAILS_DB_PREFIX . 'user_meta_admin', $userId);

            if (!empty($oRow)) {

                foreach ($oRow as $sKey => &$mValue) {
                    //  Hat-tip: http://stackoverflow.com/a/6041773
                    if (in_array($sKey, $this->aJsonFields)) {
                        $mValue = json_decode($mValue);
                    }
                }
                $data = (array) $oRow;

            } else {

                $data = [];
            }

            $this->setCache($cacheKey, $data);
        }

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

        /**
         * If no key is returned, return the entire data array, alternatively return
         * the key if it exists.
         */

        if (is_null($key)) {

            return $data;

        } elseif (isset($data[$key])) {

            return $data[$key];

        } else {

            return null;
        }
    }

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

    /**
     * Completely clears out the admin array
     *
     * @param  mixed $userId The user's ID, if null active user is used.
     *
     * @return boolean
     */
    public function clearAdminData($userId)
    {
        //  Get the user ID
        $userId = $this->adminDataGetUserId($userId);

        $bResult = $this->oUserMetaService->update(
            NAILS_DB_PREFIX . 'user_meta_admin',
            $userId,
            [
                'nav_state' => null,
            ]
        );

        if ($bResult) {
            $this->unsetCache('admin-data-' . $userId);
        }

        return $bResult;
    }

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

    /**
     * Extracts the user ID to use
     *
     * @param  int $userId The User ID, or null for active user
     *
     * @return int
     */
    protected function adminDataGetUserId($userId)
    {
        if (is_null($userId)) {

            $userId = activeUser('id');

        } else {

            $userId = $userId;
        }

        return $userId;
    }
}


<?php

/**
 * This class renders the admin dashboard
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    AdminController
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin\Admin;

use Nails\Factory;
use Nails\Admin\Controller\Base;
use Nails\Admin\Helper;

class Dashboard extends Base
{
    /**
     * Announces this controller's navGroups
     * @return stdClass
     */
    public static function announce()
    {
        $oNavGroup = Factory::factory('Nav', 'nails/module-admin');
        $oNavGroup->setLabel('Dashboard');
        $oNavGroup->setIcon('fa-home');
        $oNavGroup->addAction('Site Overview');

        return $oNavGroup;
    }

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

    /**
     * The admin homepage/dashboard
     * @return void
     */
    public function index()
    {
        //  Page Data
        $this->data['page']->title = 'Welcome';

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

        //  Choose a hello phrase
        $phrases   = array();
        $phrases[] = 'Be awesome.';
        $phrases[] = 'You look nice!';
        $phrases[] = 'What are we doing today?';

        if (activeUser('first_name')) {

            $phrases[] = 'Today is gonna be a good day, ' . activeUser('first_name') . '.';
            $phrases[] = 'Hey, ' . activeUser('first_name') . '!';

        } else {

            $phrases[] = 'Today is gonna be a good day.';
            $phrases[] = 'Hey!';
        }

        $this->data['phrase'] = random_element($phrases);

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

        //  Assets
        $oAsset = Factory::service('Asset');
        $oAsset->load('nails.admin.dashboard.min.js', true);

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

        //  Load views
        Helper::loadView('index');
    }
}


<?php

/**
 * Admin site log model
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Model
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin\Model;

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

class SiteLog extends Base
{
    protected $logPath;

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

    public function __construct()
    {
        parent::__construct();
        Factory::helper('directory');

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

        $config        =& get_config();
        $this->logPath = $config['log_path'] != '' ? $config['log_path'] : NAILS_APP_PATH . 'application/logs/';
    }

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

    /**
     * Get a list of log files
     * @return void
     */
    public function getAll($iPage = null, $iPerPage = null, array $aData = [], $bIncludeDeleted = false): array
    {
        $dirMap        = directory_map($this->logPath, 0);
        $logFiles      = [];
        $filenameRegex = '/^log\-(\d{4}\-\d{2}\-\d{2})\.php$/';

        foreach ($dirMap as $logFile) {

            if (preg_match($filenameRegex, $logFile)) {

                $logFiles[] = $logFile;
            }
        }

        arsort($logFiles);
        $logFiles = array_values($logFiles);

        $out = [];

        foreach ($logFiles as $file) {

            $temp        = new \stdClass();
            $temp->date  = preg_replace($filenameRegex, '$1', $file);
            $temp->file  = $file;
            $temp->lines = $this->countLines($this->logPath . $file);

            $out[] = $temp;
        }

        return $out;
    }

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

    public function readLog($file)
    {
        if (!is_file($this->logPath . $file)) {

            $this->setError('Not a valid log file.');
            return false;
        }

        $fh      = fopen($this->logPath . $file, 'rb');
        $out     = [];
        $counter = 0;

        while (!feof($fh)) {

            $counter++;
            $line = trim(fgets($fh));

            if ($counter == 1 || empty($line)) {

                continue;
            }
            $out[] = $line;
        }

        fclose($fh);

        return $out;
    }

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

    protected function countLines($file)
    {
        $fh    = fopen($file, 'rb');
        $lines = 0;

        while (!feof($fh)) {

            $line = fgets($fh);

            if (empty($line)) {

                continue;
            }

            $lines++;
        }

        fclose($fh);

        //  subtract 1, account for the opening <?php line
        return $lines - 1;
    }
}


<?php

namespace Nails\Admin\DataExport\Format;

use Nails\Admin\DataExport\SourceResponse;
use Nails\Admin\Interfaces\DataExport\Format;

/**
 * Class Json
 * @package Nails\Auth\DataFormat
 */
class Json implements Format
{
    /**
     * Returns the format's label
     * @return string
     */
    public function getLabel()
    {
        return 'JSON';
    }

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

    /**
     * Returns the format's description
     * @return string
     */
    public function getDescription()
    {
        return 'Export as a JSON file, useful for transporting to other systems';
    }

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

    /**
     * Returns the format's file extension
     * @return string
     */
    public function getFileExtension()
    {
        return 'json';
    }

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

    /**
     * Takes a SourceResponse object and transforms it into the appropriate format.
     *
     * @param SourceResponse $oSourceResponse A SourceResponse object
     * @param resource       $rFile           The file resource to write to
     */
    public function execute($oSourceResponse, $rFile)
    {
        fwrite($rFile, '[');

        //  Write the data to the file
        while ($oRow = $oSourceResponse->getNextItem()) {
            fwrite($rFile, trim(json_encode($oRow) . ','));
        }

        $aStats = fstat($rFile);
        $aStats = array_slice($aStats, 13);
        if ($aStats['size'] > 1) {
            ftruncate($rFile, $aStats['size'] - 1);
            fseek($rFile, $aStats['size'] - 1);
        }

        fwrite($rFile, ']');
    }
}


<?php

/**
 * Admin API end points: CKEditor
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Controller
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin\Api\Controller;

use Nails\Admin\Controller\BaseApi;
use Nails\Factory;

class Ckeditor extends BaseApi
{
    const REQUIRE_AUTH = true;

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

    /**
     * Determines whether the user is authenticated or not
     *
     * @param string $sHttpMethod The HTTP Method protocol being used
     * @param string $sMethod     The controller method being executed
     *
     * @return bool
     */
    public static function isAuthenticated($sHttpMethod = '', $sMethod = '')
    {
        return parent::isAuthenticated($sHttpMethod, $sMethod) && isAdmin();
    }

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

    /**
     * Returns the URL of the config to use for CKEditor instances
     * @return \Nails\Api\Factory\ApiResponse
     */
    public function getConfigs()
    {
        return Factory::factory('ApiResponse', 'nails/module-api')
                      ->setData([
                          'basic'   => $this->findConfig('ckeditor.config.basic.min.js'),
                          'default' => $this->findConfig('ckeditor.config.default.min.js'),
                      ]);
    }

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

    /**
     * Looks for an app version of the file
     *
     * @param string $sFile The file name to search for
     *
     * @return string
     */
    protected function findConfig($sFile)
    {
        //  @todo (Pablo - 2018-07-13) - The paths and URLs should probably be determined by the Asset service
        if (file_exists(NAILS_APP_PATH . 'assets/build/js/' . $sFile)) {
            return site_url('assets/build/js/' . $sFile);
        } elseif (file_exists(NAILS_APP_PATH . 'assets/js/' . $sFile)) {
            return site_url('assets/js/' . $sFile);
        } else {
            return NAILS_ASSETS_URL . 'js/' . $sFile;
        }
    }
}


<?php

/**
 * This class is the base class of all Admin controllers, it defines some basic
 * methods which should exist.
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Controller
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin\Controller;

use Nails\Admin\Events;
use Nails\Components;
use Nails\Factory;

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

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

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

abstract class Base extends BaseMiddle
{
    public $data;

    /**
     * Construct the controller, load all the admin assets, etc
     */
    public function __construct()
    {
        parent::__construct();

        //  Setup Events
        $oEventService = Factory::service('Event');

        //  Call the ADMIN:STARTUP event, admin is constructing
        $oEventService->trigger(Events::ADMIN_STARTUP, Events::getEventNamespace());

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

        //  Provide access to the main controller's data property
        $this->data =& getControllerData();

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

        $this->loadConfigs();
        $this->loadHelpers();
        $this->loadLanguages();

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

        //  Unload any previously loaded assets, admin handles its own assets
        $oAsset = Factory::service('Asset');
        $oAsset->clear();

        //  @todo (Pablo - 2017-06-08) - Try and reduce the number of things being loaded, or theme it
        $this->loadLibraries();
        $this->loadJs();
        $this->loadCss();
        $this->autoLoad();

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

        \Nails\Common\Controller\Base::populateUserFeedback($this->data);

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

        //  @todo (Pablo - 2017-06-08) - Remove this
        \Nails\Common\Controller\Base::backwardsCompatibility($this);
        static::backwardsCompatibility($this);

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

        //  Call the ADMIN:READY event, admin is all geared up and ready to go
        $oEventService->trigger(Events::ADMIN_READY, Events::getEventNamespace());
    }

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

    protected function loadConfigs()
    {
        $oConfig = Factory::service('Config');

        $aPaths = [
            NAILS_APP_PATH . 'application/config/admin.php',
            NAILS_APP_PATH . 'application/modules/admin/config/admin.php',
        ];

        foreach ($aPaths as $sPath) {
            if (file_exists($sPath)) {
                $oConfig->load($sPath);
            }
        }
    }

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

    protected function loadHelpers()
    {
        Factory::helper('admin', 'nails/module-admin');
    }

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

    protected function loadLanguages()
    {
        //  @todo (Pablo - 2018-09-24) - Remove this
        get_instance()->lang->load('admin/admin_generic');
    }

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

    /**
     * Load all Admin orientated JS
     *
     * @throws \Nails\Common\Exception\FactoryException
     */
    protected function loadJs()
    {
        \Nails\Common\Controller\Base::setNailsJs();

        $oAsset = Factory::service('Asset');

        //  Module assets
        $oAsset->load('admin.min.js', 'nails/module-admin');
        $oAsset->load('nails.default.min.js', 'NAILS');
        $oAsset->load('nails.admin.js', 'NAILS');
        $oAsset->load('nails.forms.min.js', 'NAILS');
        $oAsset->load('nails.api.min.js', 'NAILS');

        //  Component assets
        foreach (Components::available() as $oComponent) {

            if (!empty($oComponent->data->{'nails/module-admin'}->autoload)) {

                $oAutoLoad = $oComponent->data->{'nails/module-admin'}->autoload;
                if (!empty($oAutoLoad->assets->js)) {
                    foreach ($oAutoLoad->assets->js as $mAsset) {

                        if (is_string($mAsset)) {
                            $sAsset    = $mAsset;
                            $sLocation = $oComponent->slug;
                        } else {
                            $sAsset    = !empty($mAsset[0]) ? $mAsset[0] : null;
                            $sLocation = !empty($mAsset[1]) ? $mAsset[1] : null;
                        }

                        $oAsset->load($sAsset, $sLocation, 'JS');
                    }
                }

                //  JS Inline
                if (!empty($oAutoLoad->assets->jsInline)) {
                    foreach ($oAutoLoad->assets->jsInline as $sAsset) {
                        $oAsset->inline($sAsset, 'JS');
                    }
                }
            }
        }

        //  Global JS
        $sAdminJsPath = defined('APP_ADMIN_JS_PATH') ? APP_ADMIN_JS_PATH : NAILS_APP_PATH . 'assets/build/js/admin.min.js';
        $sAdminJsUrl  = defined('APP_ADMIN_JS_URL') ? APP_ADMIN_JS_URL : 'admin.min.js';
        if (file_exists($sAdminJsPath)) {
            $oAsset->load($sAdminJsUrl);
        }

        //  Inline assets
        $sJs = 'var _nails,_nails_admin,_nails_api, _nails_forms;';

        $sJs .= 'if (typeof(NAILS_JS) === \'function\'){';
        $sJs .= '_nails = new NAILS_JS();';
        $sJs .= '}';

        $sJs .= 'if (typeof(NAILS_API) === \'function\'){';
        $sJs .= '_nails_api = new NAILS_API();';
        $sJs .= '}';

        $sJs .= 'if (typeof(NAILS_Admin) === \'function\'){';
        $sJs .= '_nails_admin = new NAILS_Admin();';
        $sJs .= '}';

        $sJs .= 'if (typeof(NAILS_Forms) === \'function\'){';
        $sJs .= '_nails_forms = new NAILS_Forms();';
        $sJs .= '}';

        $oAsset->inline($sJs, 'JS');
    }

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

    /**
     * Load all Admin orientated CSS
     *
     * @throws \Nails\Common\Exception\FactoryException
     */
    protected function loadCss()
    {
        $oAsset = Factory::service('Asset');

        //  Module assets
        $oAsset->load('nails.admin.css', 'NAILS');
        $oAsset->load('admin.css', 'nails/module-admin');

        //  Component assets
        foreach (Components::available() as $oComponent) {
            if (!empty($oComponent->data->{'nails/module-admin'}->autoload)) {

                $oAutoLoad = $oComponent->data->{'nails/module-admin'}->autoload;

                if (!empty($oAutoLoad->assets->css)) {
                    foreach ($oAutoLoad->assets->css as $mAsset) {

                        if (is_string($mAsset)) {
                            $sAsset    = $mAsset;
                            $sLocation = $oComponent->slug;
                        } else {
                            $sAsset    = !empty($mAsset[0]) ? $mAsset[0] : null;
                            $sLocation = !empty($mAsset[1]) ? $mAsset[1] : null;
                        }

                        $oAsset->load($sAsset, $sLocation, 'CSS');
                    }
                }

                //  CSS Inline
                if (!empty($oAutoLoad->assets->cssInline)) {
                    foreach ($oAutoLoad->assets->cssInline as $sAsset) {
                        $oAsset->inline($sAsset, 'CSS');
                    }
                }
            }
        }

        //  Global CSS
        $sAdminCssPath = defined('APP_ADMIN_CSS_PATH') ? APP_ADMIN_CSS_PATH : NAILS_APP_PATH . 'assets/build/css/admin.min.css';
        $sAdminCssUrl  = defined('APP_ADMIN_CSS_URL') ? APP_ADMIN_CSS_URL : 'admin.min.css';
        if (file_exists($sAdminCssPath)) {
            $oAsset->load($sAdminCssUrl);
        }
    }

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

    /**
     * Load services required by admin
     *
     * @throws \Nails\Common\Exception\FactoryException
     */
    protected function loadLibraries()
    {
        $oAsset = Factory::service('Asset');

        //  jQuery
        $oAsset->load('jquery/dist/jquery.min.js', 'NAILS-BOWER');

        //  Fancybox
        $oAsset->load('fancybox/source/jquery.fancybox.pack.js', 'NAILS-BOWER');
        $oAsset->load('fancybox/source/jquery.fancybox.css', 'NAILS-BOWER');

        //  jQuery Toggles
        $oAsset->load('jquery-toggles/toggles.min.js', 'NAILS-BOWER');
        $oAsset->load('jquery-toggles/css/toggles.css', 'NAILS-BOWER');
        $oAsset->load('jquery-toggles/css/themes/toggles-modern.css', 'NAILS-BOWER');

        //  Tipsy
        $oAsset->load('tipsy/src/javascripts/jquery.tipsy.js', 'NAILS-BOWER');
        $oAsset->load('tipsy/src/stylesheets/tipsy.css', 'NAILS-BOWER');

        //  scrollTo
        $oAsset->load('jquery.scrollTo/jquery.scrollTo.min.js', 'NAILS-BOWER');

        //  jQuery Cookies
        $oAsset->load('jquery-cookie/jquery.cookie.js', 'NAILS-BOWER');

        //  Retina.js
        $oAsset->load('retina.js/dist/retina.min.js', 'NAILS-BOWER');

        //  Bootstrap
        $oAsset->load('bootstrap/js/dropdown.js', 'NAILS-BOWER');

        //  Fontawesome
        $oAsset->load('fontawesome/css/font-awesome.min.css', 'NAILS-BOWER');

        //  Asset libraries
        $oAsset->library('jqueryui');
        $oAsset->library('select2');
        $oAsset->library('ckeditor');
        $oAsset->library('uploadify');
        $oAsset->library('knockout');
        $oAsset->library('moment');
        $oAsset->library('mustache');
    }

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

    /**
     * Autoload component items
     *
     * @throws \Nails\Common\Exception\FactoryException
     */
    protected function autoLoad()
    {
        foreach (Components::available() as $oComponent) {
            if (!empty($oComponent->data->{'nails/module-admin'}->autoload)) {

                $oAutoLoad = $oComponent->data->{'nails/module-admin'}->autoload;

                //  Libraries
                if (!empty($oAutoLoad->services)) {
                    foreach ($oAutoLoad->services as $sService) {
                        Factory::service($sService, $oComponent->slug);
                    }
                }

                //  Models
                if (!empty($oAutoLoad->models)) {
                    foreach ($oAutoLoad->models as $sModel) {
                        Factory::model($sModel, $oComponent->slug);
                    }
                }

                //  Helpers
                if (!empty($oAutoLoad->helpers)) {
                    foreach ($oAutoLoad->helpers as $sHelper) {
                        Factory::helper($sHelper, $oComponent->slug);
                    }
                }
            }
        }
    }

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

    /**
     * Provides some backwards compatability
     *
     * @param \stdClass $oBindTo The class to bind to
     */
    public static function backwardsCompatibility(&$oBindTo)
    {
        //  @todo (Pablo - 2017-06-08) - Try and remove these dependencies
        $oBindTo->load =& get_instance()->load;
        $oBindTo->lang =& get_instance()->lang;
    }

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

    /**
     * Defines the admin controller
     *
     * @return array
     */
    public static function announce()
    {
        return [];
    }

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

    /**
     * Returns an array of permissions which can be configured for the user
     *
     * @return array
     */
    public static function permissions(): array
    {
        return [];
    }
}


<?php

/**
 * This class handles setting of notification recipients
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    AdminController
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin\Admin;

use Nails\Admin\Controller\Base;
use Nails\Admin\Helper;
use Nails\Components;
use Nails\Factory;

class Settings extends Base
{
    /**
     * @var array
     */
    protected $aFields;

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

    /**
     * Announces this controller's navGroups
     *
     * @return \stdClass
     */
    public static function announce()
    {
        $oNavGroup = Factory::factory('Nav', 'nails/module-admin');
        $oNavGroup->setLabel('Settings');
        $oNavGroup->setIcon('fa-wrench');

        if (userHasPermission('admin:admin:settings:admin:.*')) {
            $oNavGroup->addAction('Admin', 'admin');
        }

        if (userHasPermission('admin:admin:settings:site:.*')) {
            $oNavGroup->addAction('Site', 'site');
        }

        if (userHasPermission('admin:admin:settings:notifications')) {
            $oNavGroup->addAction('Notifications', 'notifications');
        }

        return $oNavGroup;
    }

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

    /**
     * Returns an array of permissions which can be configured for the user
     *
     * @return array
     */
    public static function permissions(): array
    {
        $aPermissions = parent::permissions();

        $aPermissions['admin:branding']   = 'Configure Admin Branding';
        $aPermissions['admin:whitelist']  = 'Configure Admn Whitelist';
        $aPermissions['site:customjscss'] = 'Configure Site Custom JS and CSS';
        $aPermissions['site:analytics']   = 'Configure Site analytics';
        $aPermissions['site:maintenance'] = 'Configure Maintenance Mode';
        $aPermissions['notifications']    = 'Configure Notifications';

        return $aPermissions;
    }

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

    /**
     * Manage Admin settings
     *
     * @return void
     */
    public function admin()
    {
        if (!userHasPermission('admin:admin:settings:admin:.*')) {
            unauthorised();
        }

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

        $oInput = Factory::service('Input');
        if ($oInput->post()) {

            $aSettings = [];

            if (userHasPermission('admin:admin:settings:admin:branding')) {
                $aSettings['primary_colour']   = $oInput->post('primary_colour');
                $aSettings['secondary_colour'] = $oInput->post('secondary_colour');
                $aSettings['highlight_colour'] = $oInput->post('highlight_colour');
            }

            if (userHasPermission('admin:admin:settings:admin:branding')) {
                $aSettings['whitelist'] = $this->prepareWhitelist($oInput->post('whitelist'));
            }

            if (!empty($aSettings)) {

                $oAppSettingService = Factory::service('AppSetting');
                if ($oAppSettingService->set($aSettings, 'admin')) {
                    $this->data['success'] = 'Admin settings have been saved.';
                } else {
                    $this->data['error'] = 'There was a problem saving admin settings.';
                }

            } else {
                $this->data['message'] = 'No settings to save.';
            }
        }

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

        //  Get data
        $this->data['settings'] = appSetting(null, 'admin', true);

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

        //  Set page title
        $this->data['page']->title = 'Settings › Admin';

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

        //  Load assets
        $oAsset = Factory::service('Asset');
        $oAsset->load('nails.admin.settings.min.js', 'NAILS');

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

        //  Load views
        Helper::loadView('admin');
    }

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

    /**
     * Manage Site settings
     *
     * @return void
     */
    public function site()
    {
        if (!userHasPermission('admin:admin:settings:site:.*')) {
            unauthorised();
        }

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

        $oInput = Factory::service('Input');
        if ($oInput->post()) {

            $aSettings = [];

            if (userHasPermission('admin:admin:settings:site:customjscss')) {
                $aSettings['site_custom_js']     = $oInput->post('site_custom_js');
                $aSettings['site_custom_css']    = $oInput->post('site_custom_css');
                $aSettings['site_custom_markup'] = $oInput->post('site_custom_markup');
            }

            if (userHasPermission('admin:admin:settings:site:analytics')) {
                $aSettings['google_analytics_account'] = $oInput->post('google_analytics_account');
            }

            if (userHasPermission('admin:admin:settings:site:maintenance')) {
                $sRawIPs                                 = $oInput->post('maintenance_mode_whitelist');
                $aSettings['maintenance_mode_enabled']   = (bool) $oInput->post('maintenance_mode_enabled');
                $aSettings['maintenance_mode_whitelist'] = $this->prepareWhitelist($sRawIPs);
                $aSettings['maintenance_mode_title']     = $oInput->post('maintenance_mode_title');
                $aSettings['maintenance_mode_body']      = $oInput->post('maintenance_mode_body');
            }

            if (!empty($aSettings)) {

                $oAppSettingService = Factory::service('AppSetting');
                if ($oAppSettingService->set($aSettings, 'site')) {
                    $this->data['success'] = 'Site settings have been saved.';
                } else {
                    $this->data['error'] = 'There was a problem saving site settings.';
                }

            } else {
                $this->data['message'] = 'No settings to save.';
            }
        }

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

        //  Get data
        $this->data['settings'] = appSetting(null, 'app', true);

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

        //  Set page title
        $this->data['page']->title = 'Settings › Site';

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

        //  Load assets
        $oAsset = Factory::service('Asset');
        $oAsset->load('nails.admin.settings.min.js', 'NAILS');
        $oAsset->load('nails.admin.admin.settings.min.js', 'NAILS');

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

        //  Load views
        Helper::loadView('site');
    }

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

    /**
     * Manage notifications
     *
     * @return void
     */
    public function notifications()
    {
        if (!userHasPermission('admin:admin:settings:notifications')) {
            unauthorised();
        }

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

        $oAppNotificationModel = Factory::model('AppNotification');

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

        //  Page Title
        $this->data['page']->title = 'Manage Notifications';

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

        $oInput = Factory::service('Input');
        if ($oInput->post()) {

            $aNotification = $oInput->post('notification');

            if (is_array($aNotification)) {

                $aSet = [];

                foreach ($aNotification as $sGroup => $aOptions) {

                    $aSet[$sGroup] = [];

                    foreach ($aOptions as $sKey => $sEmails) {

                        $aEmails = explode(',', $sEmails);
                        $aEmails = array_filter($aEmails);
                        $aEmails = array_unique($aEmails);

                        foreach ($aEmails as &$sEmail) {
                            $sEmail = trim($sEmail);
                            if (!valid_email($sEmail)) {
                                $error = '"<strong>' . $sEmail . '</strong>" is not a valid email.';
                                break 3;
                            }
                        }

                        $aSet[$sGroup][$sKey] = $aEmails;
                    }
                }

                if (empty($error)) {

                    foreach ($aSet as $sGroup => $aOptions) {
                        $oAppNotificationModel->set($aOptions, $sGroup);
                    }

                    $this->data['success'] = 'Notifications were updated successfully.';

                } else {
                    $this->data['error'] = $error;
                }
            }
        }

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

        $this->data['notifications'] = $oAppNotificationModel->getDefinitions();

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

        //  Load views
        Helper::loadView('notifications');
    }

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

    /**
     * Takes a multi line input and converts it into an array
     *
     * @param  string $sInput The input string
     *
     * @return array
     */
    protected function prepareWhitelist($sInput)
    {
        $sWhitelistRaw = $sInput;
        $sWhitelistRaw = str_replace("\n\r", "\n", $sWhitelistRaw);
        $aWhitelistRaw = explode("\n", $sWhitelistRaw);
        $aWhitelist    = [];

        foreach ($aWhitelistRaw as $sLine) {
            $aWhitelist = array_merge(explode(',', $sLine), $aWhitelist);
        }

        $aWhitelist = array_unique($aWhitelist);
        $aWhitelist = array_filter($aWhitelist);
        $aWhitelist = array_map('trim', $aWhitelist);
        $aWhitelist = array_values($aWhitelist);

        return $aWhitelist;
    }

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

    /**
     * Configure modules which have settings described in their composer.json/config.json file
     *
     * @return void
     */
    public function module()
    {
        $this->component('Module');
    }

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

    /**
     * Configure skins which have settings described in their composer.json/config.json file
     *
     * @return void
     */
    public function skin()
    {
        $this->component('Skin');
    }

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

    /**
     * Configure drivers which have settings described in their composer.json/config.json file
     *
     * @return void
     */
    public function driver()
    {
        $this->component('Driver');
    }

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

    /**
     * Configure components which have settings described in their composer.json/config.json file
     *
     * @param string $sType The type of component
     *
     * @return void
     */
    public function component($sType = 'component')
    {
        $oInput             = Factory::service('Input');
        $this->data['slug'] = $oInput->get('slug');

        $oComponent = Components::getBySlug($this->data['slug']);

        if (empty($oComponent->data->settings)) {
            show404();
        }

        //  Move all the settings which aren't already in fieldsets/groups into groups
        $this->data['fieldsets'] = [];
        $this->aFields           = [];
        $this->extractFieldsets($oComponent->slug, $oComponent->data->settings);
        $this->data['fieldsets'] = array_values($this->data['fieldsets']);

        if ($oInput->post()) {

            //  Validate
            $oFormValidation = Factory::service('FormValidation');
            $aRules          = [];

            foreach ($this->aFields as $oField) {

                $aFieldRule   = ['trim'];
                $aFieldRule[] = !empty($oField->required) ? 'required' : '';

                if (!empty($oField->validation_rules)) {
                    $aFieldRule = array_merge($aFieldRule, explode('|', $oField->validation_rules));
                }

                $aFieldRule = array_filter($aFieldRule);
                $aFieldRule = array_unique($aFieldRule);

                $aRules[] = [
                    'field' => $oField->key,
                    'label' => $oField->label,
                    'rules' => implode('|', $aFieldRule),
                ];
            }

            $oFormValidation->set_rules($aRules);

            if ($oFormValidation->run()) {

                $aSettings          = [];
                $aSettingsEncrypted = [];

                foreach ($this->aFields as $oField) {

                    //  @todo respect data types

                    //  Encrypted or not?
                    if (!empty($oField->encrypted)) {
                        $aSettingsEncrypted[$oField->key] = $oInput->post($oField->key);
                    } else {
                        $aSettings[$oField->key] = $oInput->post($oField->key);
                    }
                }

                //  Begin transaction
                $oAppSettingService = Factory::service('AppSetting');
                $oDb                = Factory::service('Database');
                $oDb->trans_begin();

                //  Normal settings
                if (!$oAppSettingService->set($aSettings, $oComponent->slug)) {
                    $sError = $oAppSettingService->lastError();
                }

                //  Encrypted settings
                if (!$oAppSettingService->set($aSettingsEncrypted, $oComponent->slug, null, true)) {
                    $sError = $oAppSettingService->lastError();
                }

                if (empty($sError)) {
                    $oDb->trans_commit();
                    $this->data['success'] = $sType . ' settings were saved.';
                } else {
                    $oDb->trans_rollback();
                    $this->data['error'] = 'There was a problem saving shop settings. ' . $sError;
                }

            } else {
                $this->data['error'] = lang('fv_there_were_errors');
            }
        }

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

        //  Get all the settings for this component
        $this->data['settings'] = appSetting(null, $oComponent->slug);

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

        $this->data['page']->title = 'Configure ' . $sType . ' › ' . $oComponent->name;

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

        Helper::loadView('component');
    }

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

    /**
     * Recursively gets all the settings from the settings array
     *
     * @param string $sComponentSlug The component's slug
     * @param array  $aSettings      The array of fieldsets and/or settings
     * @param int    $fieldSetIndex  The index of the fieldset
     */
    protected function extractFieldsets($sComponentSlug, $aSettings, $fieldSetIndex = 0)
    {
        foreach ($aSettings as $oSetting) {

            //  If the object contains a `fields` property then consider this a fieldset and inception
            if (isset($oSetting->fields)) {

                $fieldSetIndex++;

                if (!isset($this->data['fieldsets'][$fieldSetIndex])) {
                    $this->data['fieldsets'][$fieldSetIndex] = [
                        'legend' => $oSetting->legend,
                        'fields' => [],
                    ];
                }

                $this->extractFieldsets($sComponentSlug, $oSetting->fields, $fieldSetIndex);

            } else {

                $sValue = appSetting($oSetting->key, $sComponentSlug);
                if (!is_null($sValue)) {
                    $oSetting->default = $sValue;
                }

                if (!isset($this->data['fieldsets'][$fieldSetIndex])) {
                    $this->data['fieldsets'][$fieldSetIndex] = [
                        'legend' => '',
                        'fields' => [],
                    ];
                }

                $this->data['fieldsets'][$fieldSetIndex]['fields'][] = $oSetting;
                $this->aFields[]                                     = $oSetting;
            }
        }
    }
}


<?php

/**
 * This service handles exporting data
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Library
 * @author      Nails Dev Team
 * @link
 */

namespace Nails\Admin\Service;

use Nails\Common\Exception\NailsException;
use Nails\Components;
use Nails\Factory;
use Nails\Admin\DataExport\SourceResponse;

/**
 * Class DataExport
 *
 * @package Nails\Admin\Service
 */
class DataExport
{
    protected $aSources = [];
    protected $aFormats = [];
    protected $aCacheFiles = [];

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

    /**
     * DataExport constructor.
     */
    public function __construct()
    {
        $this->aSources = [];
        $this->aFormats = [];

        foreach (Components::available() as $oComponent) {

            $sPath             = $oComponent->path;
            $sNamespace        = $oComponent->namespace;
            $aComponentSources = [];
            $aComponentFormats = [];

            if (is_dir($sPath . 'src/DataExport/Source')) {
                $aComponentSources = array_filter((array) directory_map($sPath . 'src/DataExport/Source'));
            }

            if (is_dir($sPath . 'src/DataExport/Format')) {
                $aComponentFormats = array_filter((array) directory_map($sPath . 'src/DataExport/Format'));
            }

            foreach ($aComponentSources as $sSource) {

                $sClass    = $sNamespace . 'DataExport\\Source\\' . basename($sSource, '.php');
                $oInstance = new $sClass();

                if ($oInstance->isEnabled()) {
                    $this->aSources[] = (object) [
                        'slug'        => $oComponent->slug . '::' . basename($sSource, '.php'),
                        'label'       => $oInstance->getLabel(),
                        'description' => $oInstance->getDescription(),
                        'options'     => $oInstance->getOptions(),
                        'instance'    => $oInstance,
                    ];
                }
            }

            foreach ($aComponentFormats as $sFormat) {

                $sClass    = $sNamespace . 'DataExport\\Format\\' . basename($sFormat, '.php');
                $oInstance = new $sClass();

                $this->aFormats[] = (object) [
                    'slug'        => $oComponent->slug . '::' . basename($sFormat, '.php'),
                    'label'       => $oInstance->getLabel(),
                    'description' => $oInstance->getDescription(),
                    'instance'    => $oInstance,
                ];
            }
        }

        arraySortMulti($this->aSources, 'label');
        arraySortMulti($this->aFormats, 'label');

        $this->aSources = array_values($this->aSources);
        $this->aFormats = array_values($this->aFormats);
    }

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

    /**
     * Returns all the available sources
     *
     * @return array
     */
    public function getAllSources()
    {
        return $this->aSources;
    }

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

    /**
     * Returns a specific source by its slug
     *
     * @param $sSlug
     *
     * @return \stdClass|null
     */
    public function getSourceBySlug($sSlug)
    {
        foreach ($this->aSources as $oSource) {
            if ($sSlug === $oSource->slug) {
                return $oSource;
            }
        }

        return null;
    }

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

    /**
     * Returns all the available formats
     *
     * @return array
     */
    public function getAllFormats()
    {
        return $this->aFormats;
    }

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

    /**
     * Returns a specific format by its slug
     *
     * @param $sSlug
     *
     * @return \stdClass|null
     */
    public function getFormatBySlug($sSlug)
    {
        foreach ($this->aFormats as $oFormat) {
            if ($sSlug === $oFormat->slug) {
                return $oFormat;
            }
        }

        return null;
    }

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

    /**
     * Executes a DateExport source then passes to a DataExport format. Once complete
     * the resulting file is uploaded to the CDN and the object's ID returned.
     *
     * @param string $sSourceSlug The slug of the source to use
     * @param string $sFormatSlug The slug of the format to use
     * @param array  $aOptions    Additional options to pass to the source
     *
     * @return integer
     * @throws NailsException
     */
    public function export($sSourceSlug, $sFormatSlug, $aOptions = [])
    {
        $oSource = $this->getSourceBySlug($sSourceSlug);
        if (empty($oSource)) {
            throw new NailsException('Invalid data source "' . $sSourceSlug . '"');
        }

        $oFormat = $this->getFormatBySlug($sFormatSlug);
        if (empty($oFormat)) {
            throw new NailsException('Invalid data format "' . $sFormatSlug . '"');
        }

        $oSourceResponse = $oSource->instance->execute($aOptions);
        if (!is_array($oSourceResponse)) {
            $aSourceResponses = [$oSourceResponse];
        } else {
            $aSourceResponses = $oSourceResponse;
        }

        //  Create temporary working directory
        $sTempDir = CACHE_PATH . 'data-export-' . md5(microtime(true)) . mt_rand() . '/';
        mkdir($sTempDir);

        //  Process each file
        $aFiles = [];
        try {

            foreach ($aSourceResponses as $oSourceResponse) {

                if (!($oSourceResponse instanceof SourceResponse)) {
                    throw new NailsException('Source must return an instance of SourceResponse');
                }

                //  Create a new file
                $sFile    = $sTempDir . $oSourceResponse->getFilename() . '.' . $oFormat->instance->getFileExtension();
                $aFiles[] = $sFile;
                $rFile    = fopen($sFile, 'w+');
                //  Write to the file
                $oSourceResponse->reset();
                $oFormat->instance->execute($oSourceResponse, $rFile);
                //  Close the file
                fclose($rFile);
            }

            //  Compress if > 1
            if (count($aFiles) > 1) {
                $sArchiveFile = $sTempDir . 'export.zip';
                $oZip         = Factory::service('Zip');
                foreach ($aFiles as $sFile) {
                    $oZip->read_file($sFile);
                }
                $oZip->archive($sArchiveFile);
                $aFiles[] = $sArchiveFile;
            }
            $sFile = end($aFiles);

            //  Save to CDN
            $oCdn    = Factory::service('Cdn', 'nails/module-cdn');
            $oObject = $oCdn->objectCreate($sFile, 'data-export');

            if (empty($oObject)) {
                throw new NailsException('Failed to upload exported file. ' . $oCdn->lastError());
            }
        } finally {
            //  Tidy up
            foreach ($aFiles as $sFile) {
                if (file_exists($sFile)) {
                    unlink($sFile);
                }
            }
            rmdir($sTempDir);
        }

        return $oObject->id;
    }

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

    /**
     * Cleans up any generated cache files
     */
    public function __destruct()
    {
        foreach ($this->aCacheFiles as $sCacheFile) {
            @unlink($sCacheFile);
        }
    }
}


<?php

/**
 * Cron controller responsible for generating admin exports
 *
 * @package     Nails
 * @subpackage  module-admin
 * @category    Controller
 * @author      Nails Dev Team
 */

namespace Nails\Cron\Admin;

use Nails\Admin\Exception\DataExport\FailureException;
use Nails\Cron\Controller\Base;
use Nails\Factory;

class Export extends Base
{
    public function index()
    {
        $this->writeLog('Generating exports');

        $oNow = Factory::factory('DateTime');
        setAppSetting('data-export-cron-last-run', 'nails/module-admin', $oNow->format('Y-m-d H:i:s'));

        $oService  = Factory::service('DataExport', 'nails/module-admin');
        $oModel    = Factory::model('Export', 'nails/module-admin');
        $aRequests = $oModel->getAll(['where' => [['status', $oModel::STATUS_PENDING]]]);

        if (!empty($aRequests)) {

            $this->writeLog(count($aRequests) . ' requests');
            $this->writeLog('Marking as "RUNNING"');
            $oModel->setBatchStatus($aRequests, $oModel::STATUS_RUNNING);

            //  Group identical requests
            $aGroupedRequests = [];
            foreach ($aRequests as $oRequest) {
                $aHash = [$oRequest->source, $oRequest->format, $oRequest->options];
                $sHash = md5(json_encode($aHash));
                if (array_key_exists($sHash, $aGroupedRequests)) {
                    $aGroupedRequests[$sHash]->recipients[] = $oRequest->created_by;
                    $aGroupedRequests[$sHash]->ids[]        = $oRequest->id;
                } else {
                    $aGroupedRequests[$sHash] = (object) [
                        'source'     => $oRequest->source,
                        'format'     => $oRequest->format,
                        'options'    => json_decode($oRequest->options, JSON_OBJECT_AS_ARRAY),
                        'recipients' => [$oRequest->created_by],
                        'ids'        => [$oRequest->id],
                    ];
                }
            }

            //  Process each request
            $oEmail = Factory::factory('EmailDataExport', 'nails/module-admin');
            foreach ($aGroupedRequests as $oRequest) {
                try {

                    $this->writeLog(
                        'Starting ' . $oRequest->source . '->' . $oRequest->format . ' (' . json_encode($oRequest->options) . ')'
                    );
                    $oModel->setBatchDownloadId(
                        $oRequest->ids,
                        $oService->export($oRequest->source, $oRequest->format, $oRequest->options)
                    );
                    $oModel->setBatchStatus($oRequest->ids, $oModel::STATUS_COMPLETE);
                    $this->writeLog('Completed ' . $oRequest->source . '->' . $oRequest->format);
                    $this->writeLog('Sending emails');

                    $oEmail
                        ->data([
                            'status' => $oModel::STATUS_COMPLETE,
                            'error'  => null,
                        ]);

                    foreach ($oRequest->recipients as $iRecipient) {
                        $oEmail->to($iRecipient)->send();
                    }

                } catch (FailureException $e) {
                    $this->executionFailed($e, $oRequest, $oModel, $oEmail);
                } catch (\Exception $e) {
                    $this->executionFailed($e, $oRequest, $oModel, $oEmail);
                    //  Let unexpected exceptions bubble up
                    throw $e;
                }
            }

        } else {
            $this->writeLog('Nothing to do');
        }

        $this->writeLog('Complete');
    }

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

    /**
     * Marks a request as failed, recording why it failed and informs the recipients
     *
     * @param \Exception                            $oException The exception whichw as thrown
     * @param \stdClass                             $oRequest   The current request
     * @param \Nails\Admin\Model\Export             $oModel     The data export model
     * @param \Nails\Admin\Factory\Email\DataExport $oEmail     The email object
     */
    protected function executionFailed(
        \Exception $oException,
        \stdClass $oRequest,
        \Nails\Admin\Model\Export $oModel,
        \Nails\Admin\Factory\Email\DataExport $oEmail
    ) {
        $this->writeLog('Exception: ' . $oException->getMessage());
        $oModel->setBatchStatus($oRequest->ids, $oModel::STATUS_FAILED, $oException->getMessage());

        $oEmail
            ->data([
                'status' => $oModel::STATUS_FAILED,
                'error'  => $oException->getMessage(),
            ]);

        foreach ($oRequest->recipients as $iRecipient) {
            $oEmail->to($iRecipient)->send();
        }
    }
}