<?php

namespace Charcoal\Object;

use InvalidArgumentException;

// From Pimple
use Pimple\Container;

// From 'charcoal-factory'
use Charcoal\Factory\FactoryInterface;

// From 'charcoal-core'
use Charcoal\Model\AbstractModel;

// From 'charcoal-translator'
use Charcoal\Translator\TranslatorAwareTrait;

// From 'charcoal-object'
use Charcoal\Object\ContentInterface;
use Charcoal\Object\AuthorableInterface;
use Charcoal\Object\AuthorableTrait;
use Charcoal\Object\RevisionableInterface;
use Charcoal\Object\RevisionableTrait;
use Charcoal\Object\TimestampableInterface;
use Charcoal\Object\TimestampableTrait;

/**
 *
 */
class Content extends AbstractModel implements
    AuthorableInterface,
    ContentInterface,
    RevisionableInterface,
    TimestampableInterface
{
    use AuthorableTrait;
    use RevisionableTrait;
    use TranslatorAwareTrait;
    use TimestampableTrait;

    /**
     * Objects are active by default
     * @var boolean
     */
    private $active = true;

    /**
     * The position is used for ordering lists
     * @var integer
     */
    private $position = 0;

    /**
     * @var FactoryInterface
     */
    private $modelFactory;

    /**
     * @var string[]
     */
    private $requiredAclPermissions = [];

    /**
     * Dependencies
     * @param Container $container DI Container.
     * @return void
     */
    protected function setDependencies(Container $container)
    {
        parent::setDependencies($container);

        $this->setTranslator($container['translator']);
        $this->setModelFactory($container['model/factory']);
    }

    /**
     * @param FactoryInterface $factory The factory used to create models.
     * @return Content Chainable
     */
    protected function setModelFactory(FactoryInterface $factory)
    {
        $this->modelFactory = $factory;
        return $this;
    }

    /**
     * @return FactoryInterface The model factory.
     */
    protected function modelFactory()
    {
        return $this->modelFactory;
    }

    /**
     * @param boolean $active The active flag.
     * @return Content Chainable
     */
    public function setActive($active)
    {
        $this->active = !!$active;
        return $this;
    }

    /**
     * @return boolean
     */
    public function active()
    {
        return $this->active;
    }

    /**
     * @param integer $position The position (for ordering purpose).
     * @throws InvalidArgumentException If the position is not an integer (or numeric integer string).
     * @return Content Chainable
     */
    public function setPosition($position)
    {
        if ($position === null) {
            $this->position = null;
            return $this;
        }

        if (!is_numeric($position)) {
            throw new InvalidArgumentException(sprintf(
                'Position must be an integer, received %s',
                is_object($position) ? get_class($position) : gettype($position)
            ));
        }

        $this->position = (int)$position;
        return $this;
    }

    /**
     * @return integer
     */
    public function position()
    {
        return $this->position;
    }

    /**
     * @throws InvalidArgumentException If the ACL permissions are invalid.
     * @param  string|string[] $permissions The required ACL permissions.
     * @return Content Chainable
     */
    public function setRequiredAclPermissions($permissions)
    {
        if ($permissions === null || !$permissions) {
            $this->requiredAclPermissions = [];
            return $this;
        }
        if (is_string($permissions)) {
            $permissions = explode(',', $permissions);
            $permissions = array_map('trim', $permissions);
        }
        if (!is_array($permissions)) {
            throw new InvalidArgumentException(
                sprintf('Invalid ACL permissions. Permissions need to be an array (%s given)', gettype($permissions))
            );
        }
        $this->requiredAclPermissions = $permissions;
        return $this;
    }

    /**
     * @return string[]
     */
    public function requiredAclPermissions()
    {
        return $this->requiredAclPermissions;
    }

    /**
     * StorableTrait > preSave(): Called automatically before saving the object to source.
     * For content object, set the `created` and `lastModified` properties automatically
     * @return boolean
     */
    protected function preSave()
    {
        parent::preSave();

        // Timestampable properties
        $this->setCreated('now');
        $this->setLastModified('now');

        return true;
    }

    /**
     * StorableTrait > preUpdate(): Called automatically before updating the object to source.
     * For content object, set the `lastModified` property automatically.
     *
     * @param array $properties The properties (ident) set for update.
     * @return boolean
     */
    protected function preUpdate(array $properties = null)
    {
        parent::preUpdate($properties);

        // Content is revisionable
        if ($this->revisionEnabled()) {
            $this->generateRevision();
        }

        // Timestampable propertiees
        $this->setLastModified('now');

        return true;
    }
}


<?php

namespace Charcoal\Object;

use InvalidArgumentException;

// From 'charcoal-core'
use Charcoal\Model\Collection as CharcoalCollection;

// From 'charcoal-object'
use Charcoal\Object\HierarchicalInterface;

/**
 * A Hierarchical Model Collection
 *
 * Sorts and flattens a collection of hierarchically-interrelated models.
 *
 * This class is not recommended. Currently, only designed and used by
 * {@see \Charcoal\Support\Property\HierarchicalObjectProperty} and
 * {@see \Charcoal\Support\Admin\Widget\HierarchicalTableWidget}.
 */
class HierarchicalCollection extends CharcoalCollection
{
    /**
     * The current page (slice).
     *
     * @var integer
     */
    protected $page = 0;

    /**
     * The number of objects per page (slice).
     *
     * @var integer
     */
    protected $numPerPage = 0;

    /**
     * Create a new hierarchically-sorted collection.
     *
     * @param  array|\Traversable|null $objs Array of objects to pre-populate this collection.
     * @param  boolean                 $sort Whether to sort the collection immediately.
     * @return void
     */
    public function __construct($objs = [], $sort = true)
    {
        if ($objs) {
            $this->merge($objs);

            if ($sort) {
                $this->sortTree();
            }
        }
    }

    /**
     * Sort the hierarchical collection of objects.
     *
     * @return self
     */
    public function sortTree()
    {
        $level   = 0;
        $count   = 0;
        $pageNum = $this->page();
        $perPage = $this->numPerPage();

        $sortedObjects = [];
        $rootObjects   = [];
        $childObjects  = [];

        foreach ($this->objects as $object) {
            // Repair bad hierarchy.
            if ($object->hasMaster() && $object->master()->id() === $object->id()) {
                $object->setMaster(0);
                $object->update([ 'master' ]);
            }

            if ($object->hasMaster()) {
                $childObjects[$object->master()->id()][] = $object;
            } else {
                $rootObjects[] = $object;
            }
        }

        if (empty($rootObjects) && !empty($childObjects)) {
            foreach ($childObjects as $parentId => $children) {
                $parentObj = $children[0]->master();
                $parentObj->auxiliary = true;

                $rootObjects[] = $parentObj;
            }
        }

        $this->objects = &$rootObjects;

        if ($perPage < 1) {
            foreach ($this->objects as $object) {
                $object->level = $level;
                $sortedObjects[$object->id()] = $object;

                $count++;

                if (isset($childObjects[$object->id()])) {
                    $this->sortDescendantObjects(
                        $object,
                        $childObjects,
                        $count,
                        ($level + 1),
                        $sortedObjects
                    );
                }
            }
        } else {
            $start = (( $pageNum - 1 ) * $perPage);
            $end   = ($start + $perPage);

            foreach ($this->objects as $object) {
                if ($count >= $end) {
                    break;
                }

                if ($count >= $start) {
                    $object->level = $level;
                    $sortedObjects[$object->id()] = $object;
                }

                $count++;

                if (isset($childObjects[$object->id()])) {
                    $this->sortDescendantObjects(
                        $object,
                        $childObjects,
                        $count,
                        ($level + 1),
                        $sortedObjects
                    );
                }
            }

            // If we are on the last page, display orphaned descendants.
            if ($childObjects && $count < $end) {
                foreach ($childObjects as $orphans) {
                    foreach ($orphans as $descendants) {
                        if ($count >= $end) {
                            break;
                        }

                        if ($count >= $start) {
                            $descendants->level = 0;
                            $sortedObjects[$descendants->id()] = $descendants;
                        }

                        $count++;
                    }
                }
            }
        }

        $this->objects = $sortedObjects;

        return $this;
    }

    /**
     * Given an object, display the nested hierarchy of descendants.
     *
     * @param  HierarchicalInterface   $parentObj     The parent object from which to append its
     *     descendants for display.
     * @param  HierarchicalInterface[] $childObjects  The list of descendants by parent object ID.
     *     Passed by reference.
     * @param  integer                 $count         The current count of objects to display,
     *     for pagination. Passed by reference.
     * @param  integer                 $level         The level directly below the $parentObj.
     * @param  HierarchicalInterface[] $sortedObjects The list of objects to be displayed.
     *     Passed by reference.
     * @return void
     */
    private function sortDescendantObjects(
        HierarchicalInterface $parentObj,
        array &$childObjects,
        &$count,
        $level,
        array &$sortedObjects
    ) {
        $pageNum = $this->page();
        $perPage = $this->numPerPage();

        if ($perPage < 1) {
            foreach ($childObjects[$parentObj->id()] as $object) {
                if ($count === 0 && $object->hasMaster()) {
                    $myParents = [];
                    $myParent  = $object->master();
                    while ($myParent) {
                        $myParents[] = $myParent;

                        if (!$myParent->hasMaster()) {
                            break;
                        }

                        $myParent = $myParent->master();
                    }

                    $numParents = count($myParents);
                    while ($myParent = array_pop($myParents)) {
                        $myParent->level = ($level - $numParents);
                        $sortedObjects[$myParent->id()] = $myParent;
                        $numParents--;
                    }
                }

                $object->level = $level;
                $sortedObjects[$object->id()] = $object;

                $count++;

                if (isset($childObjects[$object->id()])) {
                    $this->sortDescendantObjects(
                        $object,
                        $childObjects,
                        $count,
                        ($level + 1),
                        $sortedObjects
                    );
                }
            }
        } else {
            $start = (( $pageNum - 1 ) * $perPage);
            $end   = ($start + $perPage);

            foreach ($childObjects[$parentObj->id()] as $object) {
                if ($count >= $end) {
                    break;
                }

                // If the page starts in a subtree, print the parents.
                if ($count === $start && $object->hasMaster()) {
                    $myParents = [];
                    $myParent  = $object->master();
                    while ($myParent) {
                        $myParents[] = $myParent;

                        if (!$myParent->hasMaster()) {
                            break;
                        }

                        $myParent = $myParent->master();
                    }

                    $numParents = count($myParents);
                    while ($myParent = array_pop($myParents)) {
                        $myParent->level = ($level - $numParents);
                        $sortedObjects[$myParent->id()] = $myParent;
                        $numParents--;
                    }
                }

                if ($count >= $start) {
                    $object->level = $level;
                    $sortedObjects[$object->id()] = $object;
                }

                $count++;

                if (isset($childObjects[$object->id()])) {
                    $this->sortDescendantObjects(
                        $object,
                        $childObjects,
                        $count,
                        ($level + 1),
                        $sortedObjects
                    );
                }
            }
        }

        // Required in order to keep track of orphans
        unset($childObjects[$parentObj->id()]);
    }

    /**
     * @param  integer $page The current page. Start at 0.
     * @throws InvalidArgumentException If the parameter is not numeric or < 0.
     * @return self
     */
    public function setPage($page)
    {
        if (!is_numeric($page)) {
            throw new InvalidArgumentException(
                'Page number needs to be numeric.'
            );
        }

        $page = (int)$page;
        if ($page < 0) {
            throw new InvalidArgumentException(
                'Page number needs to be >= 0.'
            );
        }

        $this->page = $page;

        return $this;
    }

    /**
     * @return integer
     */
    public function page()
    {
        return $this->page;
    }

    /**
     * @param  integer $num The number of results to retrieve, per page.
     * @throws InvalidArgumentException If the parameter is not numeric or < 0.
     * @return self
     */
    public function setNumPerPage($num)
    {
        if (!is_numeric($num)) {
            throw new InvalidArgumentException(
                'Num-per-page needs to be numeric.'
            );
        }

        $num = (int)$num;

        if ($num < 0) {
            throw new InvalidArgumentException(
                'Num-per-page needs to be >= 0.'
            );
        }

        $this->numPerPage = $num;

        return $this;
    }

    /**
     * @return integer
     */
    public function numPerPage()
    {
        return $this->numPerPage;
    }

    /**
     * Determine if the given value is acceptable for the collection.
     *
     * @param  mixed $value The value being vetted.
     * @return boolean
     */
    public function isAcceptable($value)
    {
        return ($value instanceof HierarchicalInterface);
    }
}


<?php

/**
 * This class renders the admin help section
 *
 * @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\Factory;

class Help extends Base
{
    /**
     * Announces this controller's navGroups
     * @return stdClass
     */
    public static function announce()
    {
        $oHelpModel = Factory::model('Help', 'nails/module-admin');

        if (userHasPermission('admin:admin:help:view') && $oHelpModel->countAll()) {

            $oNavGroup = Factory::factory('Nav', 'nails/module-admin');
            $oNavGroup->setLabel('Dashboard');
            $oNavGroup->setIcon('fa-home');
            $oNavGroup->addAction('Help Videos');

            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['view'] = 'Can view help videos';

        return $aPermissions;
    }

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

    /**
     * Renders the admin help pagge
     * @return void
     */
    public function index()
    {
        if (!userHasPermission('admin:admin:help:view')) {
            unauthorised();
        }

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

        //  Page Title
        $this->data['page']->title = 'Help Videos';

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

        //  Get data
        $oInput      = Factory::service('Input');
        $oHelpModel  = Factory::model('Help', 'nails/module-admin');
        $sTableAlias = $oHelpModel->getTableAlias();

        //  Get pagination and search/sort variables
        $iPage      = (int) $oInput->get('page') ?: 0;
        $iPerPage   = (int) $oInput->get('perPage') ?: 50;
        $sSortOn    = $oInput->get('sortOn') ?: $sTableAlias . '.label';
        $sSortOrder = $oInput->get('sortOrder') ?: 'asc';
        $sKeywords  = $oInput->get('keywords') ?: '';

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

        //  Define the sortable columns
        $aSortColumns = [
            $sTableAlias . '.label'    => 'Label',
            $sTableAlias . '.duration' => 'Duration',
            $sTableAlias . '.created'  => 'Added',
            $sTableAlias . '.modified' => 'Modified',
        ];

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

        //  Define the $aData variable for the queries
        $aData = [
            'sort'     => [
                [$sSortOn, $sSortOrder],
            ],
            'keywords' => $sKeywords,
        ];

        //  Get the items for the page
        $iTotalRows           = $oHelpModel->countAll($aData);
        $this->data['videos'] = $oHelpModel->getAll($iPage, $iPerPage, $aData);

        //  Set Search and Pagination objects for the view
        $this->data['search']     = Helper::searchObject(true, $aSortColumns, $sSortOn, $sSortOrder, $iPerPage, $sKeywords);
        $this->data['pagination'] = Helper::paginationObject($iPage, $iPerPage, $iTotalRows);

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

        $oAsset = Factory::service('Asset');
        $oAsset->inline('$(\'a.video-button\').fancybox({ type : \'iframe\' });', 'JS');

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

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


<?php

/**
 * This class renders Admin Utilities
 *
 * @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\Common\Exception\NailsException;
use Nails\Factory;

/**
 * Class Utilities
 * @package Nails\Admin\Admin
 */
class Utilities extends Base
{
    protected $aExportSources;
    protected $aExportFormats;

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

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

        if (userHasPermission('admin:admin:utilities:rewriteRoutes')) {
            $oNavGroup->addAction('Rewrite Routes', 'rewrite_routes');
        }

        if (userHasPermission('admin:admin:utilities:export')) {
            $oNavGroup->addAction('Export Data', 'export');
        }

        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['rewriteRoutes'] = 'Can Rewrite Routes';
        $aPermissions['export']        = 'Can Export Data';

        return $aPermissions;
    }

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

    /**
     * Rewrite the app's routes
     * @return void
     */
    public function rewrite_routes()
    {
        if (!userHasPermission('admin:admin:utilities:rewriteRoutes')) {
            unauthorised();
        }

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

        $oInput = Factory::service('Input');
        if ($oInput->post('go')) {
            $oRoutesService = Factory::service('Routes');
            if ($oRoutesService->update()) {
                $this->data['success'] = 'Routes rewritten successfully.';
            } else {
                $this->data['error'] = 'There was a problem writing the routes. ';
                $this->data['error'] .= $oRoutesService->lastError();
            }
        }

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

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

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

    /**
     * Export data
     * @return void
     */
    public function export()
    {
        if (!userHasPermission('admin:admin:utilities:export')) {
            unauthorised();
        }

        $oDataExport = Factory::service('DataExport', 'nails/module-admin');
        $aSources    = $oDataExport->getAllSources();
        $aFormats    = $oDataExport->getAllFormats();

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

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

            try {

                $oFormValidation = Factory::service('FormValidation');
                $oFormValidation->set_rules('source', '', 'required');
                $oFormValidation->set_rules('format', '', 'required');
                $oFormValidation->set_message('required', lang('fv_required'));

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

                //  Validate source
                $oSelectedSource = $oDataExport->getSourceBySlug($oInput->post('source'));
                if (empty($oSelectedSource)) {
                    throw new NailsException('Invalid data source');
                }

                //  Validate format
                $oSelectedFormat = $oDataExport->getFormatBySlug($oInput->post('format'));
                if (empty($oSelectedFormat)) {
                    throw new NailsException('Invalid data format');
                }

                //  Prepare options
                $aOptions       = [];
                $aPostedOptions = getFromArray($oSelectedSource->slug, (array) $oInput->post('options'));
                foreach ($oSelectedSource->options as $aOption) {
                    $sKey            = getFromArray('key', $aOption);
                    $aOptions[$sKey] = getFromArray($sKey, $aPostedOptions);
                }

                $oDataExportModel = Factory::model('Export', 'nails/module-admin');
                $aData            = [
                    'source'  => $oSelectedSource->slug,
                    'options' => json_encode($aOptions),
                    'format'  => $oSelectedFormat->slug,
                ];
                if (!$oDataExportModel->create($aData)) {
                    throw new NailsException('Failed to schedule export.');
                }

                $this->data['success'] = 'Export Scheduled';

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

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

        $oModel  = Factory::model('Export', 'nails/module-admin');
        $aRecent = $oModel->getAll([
            'where' => [['created_by', activeUser('id')]],
            'sort'  => [['created', 'desc']],
            'limit' => 10,
        ]);

        //  Pretty source labels, format labels, and options
        $aRecent = array_map(function ($oItem) use ($aSources, $aFormats) {

            //  Sources
            foreach ($aSources as $oSource) {
                if ($oSource->slug === $oItem->source) {
                    $oItem->source = $oSource->label;
                }
            }

            if (empty($oItem->source)) {
                $oItem->source = 'Unknown';
            }

            //  Formats
            foreach ($aFormats as $oFormat) {
                if ($oFormat->slug === $oItem->format) {
                    $oItem->format = $oFormat->label;
                }
            }

            if (empty($oItem->format)) {
                $oItem->format = 'Unknown';
            }

            //  Options
            $oOptions = json_decode($oItem->options);
            if ($oOptions) {
                $oItem->options = '
' . json_encode($oOptions, JSON_PRETTY_PRINT) . '
';
            } else {
                $oItem->options = '';
            }

            return $oItem;
        }, $aRecent);

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

        //  Cron running?
        $sLastRun   = appSetting('data-export-cron-last-run', 'nails/module-admin');
        $bIsRunning = false;
        if ($sLastRun) {
            $oNow       = Factory::factory('DateTime');
            $oLastRun   = new \DateTime($sLastRun);
            $iDiff      = $oNow->getTimestamp() - $oLastRun->getTimestamp();
            $bIsRunning = $iDiff <= 300;
        }
        if (!$bIsRunning) {
            $this->data['warning'] = 'The data export cron job is not running';
            $this->data['warning'] .= '
The cron job has not been executed within the past 5 minutes.';
        }

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

        //  Set view data
        $this->data['page']->title = 'Export Data';
        $this->data['aSources']    = $aSources;
        $this->data['aFormats']    = $aFormats;
        $this->data['aRecent']     = $aRecent;

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

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

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


<?php

namespace Nails\Admin\DataExport;

class SourceResponse
{
    /**
     * The human-friendly label
     *
     * @var string
     */
    protected $sLabel = '';

    // --------------------------------------------------------------------------
    /**
     * The exported file name
     *
     * @var string
     */
    protected $sFileName = '';

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

    /**
     * The field names
     *
     * @var array
     */
    protected $aFields = [];

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

    /**
     * The rows of data, if not using $oSource
     *
     * @var array
     */
    protected $aData = [];

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

    /**
     * A data resource if not using $aData
     *
     * @var \PDOStatement|\CI_DB_mysqli_result
     */
    protected $oSource;

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

    /**
     * A function which formats each row
     *
     * @var callable
     */
    protected $cFormatter;

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

    /**
     * Sets the data set's label
     *
     * @param string $sLabel The data set's label
     *
     * @return $this
     */
    public function setLabel($sLabel)
    {
        $this->sLabel = $sLabel;
        return $this;
    }

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

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

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

    /**
     * Sets the data set's file name
     *
     * @param string $sFileName The data set's file name
     *
     * @return $this
     */
    public function setFileName($sFileName)
    {
        $this->sFileName = $sFileName;
        return $this;
    }

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

    /**
     * Returns the data set's file name
     *
     * @return string
     */
    public function getFileName()
    {
        return $this->sFileName;
    }

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

    /**
     * The fields/columns for the export
     *
     * @param array $aFields An array of fields/column names
     *
     * @return $this
     */
    public function setFields(array $aFields = [])
    {
        $this->aFields = $aFields;
        return $this;
    }

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

    /**
     * Returns the field array
     *
     * @return array
     */
    public function getFields()
    {
        return $this->aFields;
    }

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

    /**
     * Sets the data for the export
     *
     * @param array $aData The data to export
     *
     * @return $this
     */
    public function setData(array $aData)
    {
        $this->aData = $aData;
        return $this;
    }

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

    /**
     * Returns the data array
     *
     * @return array
     */
    public function getData()
    {
        return $this->aData;
    }

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

    /**
     * Set the resource
     *
     * @param \PDOStatement|\CI_DB_mysqli_result $oSource The data source
     *
     * @return $this
     */
    public function setSource($oSource)
    {
        //  @todo (Pablo - 2018-02-19) - verify is correct type
        $this->oSource = $oSource;
        return $this;
    }

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

    /**
     * Returns the source
     *
     * @return \PDOStatement|\CI_DB_mysqli_result
     */
    public function getSource()
    {
        return $this->oSource;
    }

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

    /**
     * Sets the formatting function to use
     *
     * @param callable $cFormatter The formatting function
     *
     * @return $this
     */
    public function setFormatter($cFormatter)
    {
        $this->cFormatter = $cFormatter;
        return $this;
    }

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

    /**
     * Resets the pointer in the data set to the beginning
     */
    public function reset()
    {
        if (!empty($this->aData)) {
            reset($this->aData);
        } elseif ($this->oSource instanceof \PDOStatement) {
            //  unsupported
        } elseif ($this->oSource instanceof \CI_DB_mysqli_result) {
            $this->oSource->data_seek(0);
        }
    }

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

    /**
     * Returns the next item in the data set, passing it to the formatter, if defined
     *
     * @return \stdClass
     */
    public function getNextItem()
    {
        $oRow = null;

        if (!empty($this->aData)) {
            $oRow = current($this->aData);
            next($this->aData);
        } elseif ($this->oSource instanceof \PDOStatement) {
            $oRow = $this->oSource->fetch(\PDO::FETCH_ASSOC);
        } elseif ($this->oSource instanceof \CI_DB_mysqli_result) {
            $oRow = $this->oSource->unbuffered_row();
        }

        return is_callable($this->cFormatter) ? call_user_func($this->cFormatter, $oRow) : $oRow;
    }
}


<?php

/**
 * This class renders the log browsers
 *
 * @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\Factory;

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

        if (userHasPermission('admin:admin:logs:site:browse')) {
            $oNavGroup->addAction('Browse Site Logs', 'site');
        }

        if (userHasPermission('admin:admin:logs:event:browse')) {
            $oNavGroup->addAction('Browse Event Logs', 'event');
        }

        if (userHasPermission('admin:admin:logs:change:browse')) {
            $oNavGroup->addAction('Browse Admin Logs', 'changelog');
        }

        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['site:browse']     = 'Can browse site logs';
        $aPermissions['event:browse']    = 'Can browse event logs';
        $aPermissions['event:download']  = 'Can download event logs';
        $aPermissions['change:browse']   = 'Can browse change logs';
        $aPermissions['change:download'] = 'Can download change logs';

        return $aPermissions;
    }

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

    /**
     * Route site log pages
     * @throws \Nails\Common\Exception\FactoryException
     */
    public function site()
    {
        if (!userHasPermission('admin:admin:logs:site:browse')) {
            unauthorised();
        }

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

        Factory::helper('string');
        $oUri    = Factory::service('Uri');
        $sMethod = $oUri->segment(5) ? $oUri->segment(5) : 'index';
        $sMethod = 'site' . underscoreToCamelcase(strtolower($sMethod), false);

        if (method_exists($this, $sMethod)) {
            $this->{$sMethod}();
        } else {
            show404('', true);
        }
    }

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

    /**
     * Browse site log files
     * @return void
     */
    protected function siteIndex()
    {
        if (!userHasPermission('admin:admin:logs:site:browse')) {
            unauthorised();
        }

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

        $this->data['page']->title = 'Browse Logs';

        $oAsset = Factory::service('Asset');
        $oAsset->library('MUSTACHE');
        $oAsset->load('nails.admin.logs.site.min.js', 'NAILS');
        $oAsset->inline('logsSite = new NAILS_Admin_Logs_Site();', 'JS');

        Helper::loadView('site/index');
    }

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

    /**
     * View a single log file
     */
    protected function siteView()
    {
        if (!userHasPermission('admin:admin:logs:site:browse')) {
            unauthorised();
        }

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

        $oUri                      = Factory::service('Uri');
        $sFile                     = $oUri->segment(6);
        $this->data['page']->title = 'Browse Logs › ' . $sFile;

        $oSiteLogModel      = Factory::model('SiteLog', 'nails/module-admin');
        $this->data['logs'] = $oSiteLogModel->readLog($sFile);

        if (!$this->data['logs']) {
            show404();
        }

        Helper::loadView('site/view');
    }

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

    /**
     * Browse Site Events
     * @return void
     */
    public function event()
    {
        if (!userHasPermission('admin:admin:logs:event:browse')) {
            unauthorised();
        }

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

        //  Set method info
        $this->data['page']->title = 'Browse Events';

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

        $sTableAlias = $this->event->getTableAlias();

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

        //  Get pagination and search/sort variables
        $oInput     = Factory::service('Input');
        $iPage      = (int) $oInput->get('page') ?: 0;
        $iPerPage   = (int) $oInput->get('perPage') ?: 50;
        $sSortOn    = $oInput->get('sortOn') ?: $sTableAlias . '.created';
        $sSortOrder = $oInput->get('sortOrder') ?: 'desc';
        $sKeywords  = $oInput->get('keywords') ?: '';

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

        //  Define the sortable columns
        $aSortColumns = [
            $sTableAlias . '.created' => 'Created',
            $sTableAlias . '.type'    => 'Type',
        ];

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

        //  Define the $aData variable for the queries
        $aData = [
            'sort'     => [
                [$sSortOn, $sSortOrder],
            ],
            'keywords' => $sKeywords,
        ];

        //  Are we downloading? Or viewing?
        if ($oInput->get('dl') && userHasPermission('admin:admin:logs:event:download')) {

            //  Get all items for the search, the view will iterate over the resultset
            $oEvents = $this->event->getAllRawQuery(null, null, $aData);

            Helper::loadCsv($oEvents, 'export-events-' . toUserDatetime(null, 'Y-m-d_h-i-s') . '.csv');

        } else {

            //  Get the items for the page
            $iTotalRows           = $this->event->countAll($aData);
            $this->data['events'] = $this->event->getAll($iPage, $iPerPage, $aData);

            //  Set Search and Pagination objects for the view
            $this->data['search']     = Helper::searchObject(true, $aSortColumns, $sSortOn, $sSortOrder, $iPerPage, $sKeywords);
            $this->data['pagination'] = Helper::paginationObject($iPage, $iPerPage, $iTotalRows);

            //  Add the header button for downloading
            if (userHasPermission('admin:admin:logs:event:download')) {

                //  Build the query string, so that the same search is applies
                $oInput               = Factory::service('Input');
                $aParams              = [];
                $aParams['dl']        = true;
                $aParams['sortOn']    = $oInput->get('sortOn');
                $aParams['sortOrder'] = $oInput->get('sortOrder');
                $aParams['keywords']  = $oInput->get('keywords');

                $aParams = array_filter($aParams);
                $aParams = http_build_query($aParams);

                Helper::addHeaderButton('admin/admin/logs/event?' . $aParams, 'Download As CSV');
            }

            Helper::loadView('event/index');
        }
    }

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

    /**
     * Browse Admin Changelog
     * @return void
     */
    public function changelog()
    {
        if (!userHasPermission('admin:admin:logs:change:browse')) {
            unauthorised();
        }

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

        //  Set method info
        $this->data['page']->title = 'Browse Changelog';

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

        $oInput          = Factory::service('Input');
        $oChangeLogModel = Factory::model('ChangeLog', 'nails/module-admin');
        $sTableAlias     = $oChangeLogModel->getTableAlias();

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

        //  Get pagination and search/sort variables
        $iPage      = (int) $oInput->get('page') ?: 0;
        $iPerPage   = (int) $oInput->get('perPage') ?: 50;
        $sSortOn    = $oInput->get('sortOn') ?: $sTableAlias . '.created';
        $sSortOrder = $oInput->get('sortOrder') ?: 'desc';
        $sKeywords  = $oInput->get('keywords') ?: '';

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

        //  Define the sortable columns
        $aSortColumns = [
            $sTableAlias . '.created' => 'Created',
            $sTableAlias . '.type'    => 'Type',
        ];

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

        //  Define the $aData variable for the queries
        $aData = [
            'sort'     => [
                [$sSortOn, $sSortOrder],
            ],
            'keywords' => $sKeywords,
        ];

        //  Are we downloading? Or viewing?
        if ($oInput->get('dl') && userHasPermission('admin:admin:logs:change:download')) {

            //  Get all items for the search, the view will iterate over the resultset
            $oChangelog = $oChangeLogModel->getAllRawQuery(null, null, $aData);

            Helper::loadCsv($oChangelog, 'export-changelog-' . toUserDatetime(null, 'Y-m-d_h-i-s') . '.csv');

        } else {

            //  Get the items for the page
            $iTotalRows              = $oChangeLogModel->countAll($aData);
            $this->data['changelog'] = $oChangeLogModel->getAll($iPage, $iPerPage, $aData);

            //  Set Search and Pagination objects for the view
            $this->data['search']     = Helper::searchObject(false, $aSortColumns, $sSortOn, $sSortOrder, $iPerPage, $sKeywords);
            $this->data['pagination'] = Helper::paginationObject($iPage, $iPerPage, $iTotalRows);

            //  Add the header button for downloading
            if (userHasPermission('admin:admin:logs:change:download')) {

                //  Build the query string, so that the same search is applies
                $aParams              = [];
                $aParams['dl']        = true;
                $aParams['sortOn']    = $oInput->get('sortOn');
                $aParams['sortOrder'] = $oInput->get('sortOrder');
                $aParams['keywords']  = $oInput->get('keywords');

                $aParams = array_filter($aParams);
                $aParams = http_build_query($aParams);

                Helper::addHeaderButton('admin/admin/logs/changelog?' . $aParams, 'Download As CSV');
            }

            Helper::loadView('changelog/index');
        }
    }
}