<?php

namespace Ekyna\Component\Commerce\Shipment\Model;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;

/**
 * Trait ShipmentSubjectTrait
 * @package Ekyna\Component\Commerce\Shipment\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
trait ShipmentSubjectTrait
{
    /**
     * @var string
     */
    protected $shipmentState;

    /**
     * @var \Doctrine\Common\Collections\Collection|ShipmentInterface[]
     */
    protected $shipments;

    /**
     * Initializes the shipments.
     */
    protected function initializeShipmentSubject()
    {
        $this->shipmentState = ShipmentStates::STATE_NONE;
        $this->shipments = new ArrayCollection();
    }

    /**
     * @inheritdoc
     */
    public function setShipmentState($state)
    {
        $this->shipmentState = $state;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getShipmentState()
    {
        return $this->shipmentState;
    }

    /**
     * Returns whether the order has shipments or not.
     *
     * @return bool
     */
    public function hasShipments()
    {
        return 0 < $this->shipments->count();
    }

    /**
     * Returns the shipments.
     *
     * @param bool $filter TRUE for shipments, FALSE for returns, NULL for all
     *
     * @return \Doctrine\Common\Collections\Collection|ShipmentInterface[]
     */
    public function getShipments($filter = null)
    {
        if (null === $filter) {
            return $this->shipments;
        }

        return $this->shipments->filter(function(ShipmentInterface $shipment) use ($filter) {
            return $filter xor $shipment->isReturn();
        });
    }

    /**
     * Returns the shipment date.
     *
     * @param bool $latest Whether to return the last shipment date instead of the first
     *
     * @return \DateTime|null
     */
    public function getShippedAt($latest = false)
    {
        if (0 == $this->shipments->count()) {
            return null;
        }

        $criteria = Criteria::create();
        $criteria
            ->andWhere(Criteria::expr()->eq('return', false))
            ->andWhere(Criteria::expr()->in('state', [ShipmentStates::STATE_READY, ShipmentStates::STATE_SHIPPED]))
            ->orderBy(['createdAt' => $latest ? Criteria::DESC : Criteria::ASC]);

        /** @var ArrayCollection $shipments */
        $shipments = $this->shipments;
        $shipments = $shipments->matching($criteria);

        /** @var ShipmentInterface $shipment */
        if (false !== $shipment = $shipments->first()) {
            return $shipment->getCreatedAt();
        }

        return null;
    }
}


<?php

namespace Ekyna\Component\Commerce\Payment\Resolver;

use Ekyna\Component\Commerce\Common\Resolver\StateResolverInterface;
use Ekyna\Component\Commerce\Common\Util\Money;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceStates;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceSubjectInterface;
use Ekyna\Component\Commerce\Payment\Calculator\PaymentCalculatorInterface;
use Ekyna\Component\Commerce\Payment\Model\PaymentStates;
use Ekyna\Component\Commerce\Payment\Model\PaymentSubjectInterface;

/**
 * Class PaymentSubjectStateResolver
 * @package Ekyna\Component\Commerce\Payment\Resolver
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class PaymentSubjectStateResolver implements StateResolverInterface
{
    /**
     * @var PaymentCalculatorInterface
     */
    protected $paymentCalculator;

    /**
     * Constructor.
     *
     * @param PaymentCalculatorInterface $paymentCalculator
     */
    public function __construct(PaymentCalculatorInterface $paymentCalculator)
    {
        $this->paymentCalculator = $paymentCalculator;
    }

    /**
     * @inheritDoc
     */
    public function resolve($subject)
    {
        if (!$subject instanceof PaymentSubjectInterface) {
            throw new InvalidArgumentException("Expected instance of " . PaymentSubjectInterface::class);
        }

        if (0 === $subject->getPayments()->count()) {
            // CANCELED subject is invoiceable and is fully credited
            if ($subject instanceof InvoiceSubjectInterface) {
                if ($subject->getInvoiceState() === InvoiceStates::STATE_CREDITED) {
                    // TODO Keep refunded state
                    return $this->setState($subject, PaymentStates::STATE_CANCELED);
                }
            }

            // NEW by default
            return $this->setState($subject, PaymentStates::STATE_NEW);
        }

        // This method uses the calculated sale's payment totals.
        // Makes sure to update them before any call of this method.

        $grandTotal = $subject->getGrandTotal();
        $currency = $subject->getCurrency()->getCode();

        // COMPLETED paid total equals grand total and no accepted/expired outstanding
        if ($this->hasDifferentCurrencies($subject)) {
            $diff = Money::round($subject->getPaidTotal() - $grandTotal, $currency);
            $delta = pow(0.1, Money::getPrecision($currency));
            $fullyPaid = (-$delta <= $diff) && ($diff <= $delta);
        } else {
            $fullyPaid = 0 === Money::compare($subject->getPaidTotal(), $grandTotal, $currency);
        }
        if ($fullyPaid && (0 == $subject->getOutstandingAccepted()) && (0 == $subject->getOutstandingExpired())) {
            return $this->setState($subject, PaymentStates::STATE_COMPLETED);
        }

        $fullFill = function ($amount) use ($grandTotal, $currency) {
            return 0 <= Money::compare($amount, $grandTotal, $currency);
        };

        // CAPTURED paid total plus accepted outstanding total is greater than grand total
        if ($fullFill($subject->getPaidTotal() + $subject->getOutstandingAccepted())) {
            return $this->setState($subject, PaymentStates::STATE_CAPTURED);
        }

        // DEPOSIT paid total is greater than deposit total
        if (0 < $subject->getDepositTotal()) {
            if (0 <= Money::compare($subject->getPaidTotal(), $subject->getDepositTotal(), $currency)) {
                return $this->setState($subject, PaymentStates::STATE_DEPOSIT);
            }
        }

        // OUTSTANDING expired total is greater than zero
        if (0 < $subject->getOutstandingExpired()) {
            return $this->setState($subject, PaymentStates::STATE_OUTSTANDING);
        }

        // PENDING total is greater than zero
        if ($fullFill($subject->getPaidTotal() + $subject->getOutstandingAccepted() + $subject->getPendingTotal())) {
            return $this->setState($subject, PaymentStates::STATE_PENDING);
        }

        // REFUNDED total is greater than or equals the grand total
        if ($fullFill($this->paymentCalculator->calculateRefundedTotal($subject))) {
            return $this->setState($subject, PaymentStates::STATE_REFUNDED);
        }

        // CANCELED subject has invoice(s) and is fully credited
        if ($subject instanceof InvoiceSubjectInterface) {
            if ($subject->getInvoiceState() === InvoiceStates::STATE_CREDITED) {
                return $this->setState($subject, PaymentStates::STATE_CANCELED);
            }
        }

        // FAILED total is greater than or equals the grand total
        if ($fullFill($this->paymentCalculator->calculateFailedTotal($subject))) {
            return $this->setState($subject, PaymentStates::STATE_FAILED);
        }

        // CANCELED total is greater than or equals the grand total
        if ($fullFill($this->paymentCalculator->calculateCanceledTotal($subject))) {
            return $this->setState($subject, PaymentStates::STATE_CANCELED);
        }

        // NEW by default
        return $this->setState($subject, PaymentStates::STATE_NEW);
    }

    /**
     * Returns whether the subject has payment with different currency than its.
     *
     * @param PaymentSubjectInterface $subject
     *
     * @return bool
     */
    protected function hasDifferentCurrencies(PaymentSubjectInterface $subject)
    {
        $currency = $subject->getCurrency()->getCode();

        foreach ($subject->getPayments() as $payment) {
            if ($payment->getCurrency()->getCode() !== $currency) {
                return true;
            }
        }

        return false;
    }

    /**
     * Sets the payment state.
     *
     * @param PaymentSubjectInterface $subject
     * @param string                  $state
     *
     * @return bool Whether the shipment state has been updated.
     */
    protected function setState(PaymentSubjectInterface $subject, $state)
    {
        if ($state !== $subject->getPaymentState()) {
            $subject->setPaymentState($state);

            return true;
        }

        return false;
    }
}


<?php

namespace PeskyCMF\UI;

use PeskyCMF\Config\CmfConfig;
use PeskyCMF\HttpCode;
use PeskyCMF\Scaffold\KeyValueTableScaffoldConfig;
use PeskyCMF\Scaffold\ScaffoldConfig;
use PeskyCMF\Scaffold\ScaffoldConfigInterface;
use PeskyORM\ORM\TableInterface;
use Swayok\Utils\File;

class CmfUIModule {

    protected $cmfConfig;

    protected $scaffoldTemplatesForNormalTableViewPath = 'cmf::scaffold.templates';
    protected $scaffoldTemplatesForKeyValueTableViewPath = 'cmf::scaffold.templates';

    protected $defaultSidebarLogo = '';

    protected $UIViews = [
        'layout' => 'cmf::layout',
        'ui' => 'cmf::ui.ui',
        'footer' => 'cmf::ui.footer',
        'sidebar_user_info' => 'cmf::ui.sidebar_user_info',
        'sidebar_menu' => 'cmf::ui.sidebar_menu',
        'top_navbar' => 'cmf::ui.top_navbar',
    ];
    protected $isUIViewsLoadedFromConfigs = false;

    protected $resources = null;
    protected $tables = null;
    protected $scaffoldConfigs = [];

    protected $menuItemsFromScaffoldConfigs;
    protected $customMenuItems = [];
    protected $allMenuItems;

    public function __construct(CmfConfig $cmfConfig) {
        $this->cmfConfig = $cmfConfig;
    }

    /**
     * @return CmfConfig
     */
    public function getCmfConfig() {
        return $this->cmfConfig;
    }

    public function renderLayoutView(): string {
        return $this->renderUIView('layout', $this->getDataForLayout());
    }

    protected function getDataForLayout(): array {
        $uiSkin = $this->getSkinName();
        return [
            'skin' => $uiSkin,
            'coreAssets' => $this->getCoreAssetsForLayout($uiSkin),
            'customAssets' => $this->getCustomAssetsForLayout(),
            'scriptsVersion' => '2.3.3',
            'jsAppSettings' => $this->getJsAppSettings(),
            'jsAppData' => $this->getJsAppData(),
        ];
    }

    public function getSkinName(): string {
        return $this->getCmfConfig()->config('ui.skin', 'skin-blue');
    }

    public function getCoreAssetsForLayout(string $skin): array {
        $assetsMode = config('peskycmf.assets');
        $subFolder = 'min';
        $minSuffix = '.min';
        $isSrcMode = stripos($assetsMode, 'src') === 0;
        if ($isSrcMode || $assetsMode === 'packed') {
            $minSuffix = '';
            $subFolder = 'packed';
        }
        $locale = $this->getCmfConfig()->getLocaleWithSuffix('_');
        $ret = [
            'js-head' => [
                "/packages/cmf/raw/js/jquery{$minSuffix}.js",
            ],
            'js' => [
                'cmf-libs' => "/packages/cmf/{$subFolder}/js/cmf-libs.js",
                'cmf-jquery-and-bootstrap-plugins' => "/packages/cmf/{$subFolder}/js/cmf-jquery-and-bootstrap-plugins.js",
                'cmf-core' => "/packages/cmf/{$subFolder}/js/cmf-core.js",
                'localization' => "/packages/cmf/{$subFolder}/js/locale/{$locale}.js",
                'app' => '/packages/cmf/raw/js/cmf-app.js',
            ],
            'css' => [
                '/packages/cmf/raw/css/fonts/Roboto/roboto.css',
                '/packages/cmf/raw/css/bootstrap/bootstrap.css',
                '/packages/cmf/raw/css/adminlte/AdminLTE.css',
                "/packages/cmf/raw/css/adminlte/skins/{$skin}.css",
                'cmf-libs' => "/packages/cmf/{$subFolder}/css/cmf-libs.css",
                'cmf-core' => "/packages/cmf/{$subFolder}/css/cmf.css",
                "/packages/cmf/raw/font-awesome/css/font-awesome{$minSuffix}.css",
            ],
        ];

        if ($isSrcMode) {
            $files = File::readJson(__DIR__ . '/../../../npm/config/cmf-assets.json');
            $isCore = $assetsMode === 'src-core';
            $packs = $isCore ? ['cmf-core'] : ['cmf-libs', 'cmf-core', 'cmf-jquery-and-bootstrap-plugins'];
            foreach ($packs as $packName) {
                if (isset($ret['js'][$packName])) {
                    $ret['js'][$packName] = [];
                    foreach ($files['scripts'][$packName]['files'] as $path) {
                        $ret['js'][$packName][] = '/packages/cmf/src/' . $path;
                    }
                }
                if (isset($ret['css'][$packName])) {
                    $ret['css'][$packName] = [];
                    foreach ($files['stylesheets'][$packName]['files'] as $path) {
                        $ret['css'][$packName][] = '/packages/cmf/src/' . $path;
                    }
                }
            }
            $ret['js']['app'] = '/packages/cmf/src/src/js/cmf.app.js';
            if (!$isCore && isset($files['localizations'][$locale])) {
                $ret['js']['localization'] = [];
                foreach ($files['localizations'][$locale]['files'] as $path) {
                    $ret['js']['localization'][] = '/packages/cmf/src/' . $path;
                }
            }
        }
        return $ret;
    }

    public function getCustomAssetsForLayout(): array {
        return [
            'js' => (array)$this->getCmfConfig()->config('ui.js_files', []),
            'css' => (array)$this->getCmfConfig()->config('ui.css_files', []),
            'js_code_blocks' => (array)$this->getCmfConfig()->config('ui.js_code_blocks', []),
        ];
    }

    /**
     * Prefix to load custom views from.
     * For example
     * - if custom views stored in /resources/views/admin - prefix should be "admin."
     * - if you placed views under namespace "admin" - prefix should be "admin:"
     * @return string
     */
    public function getCustomViewsPrefix(): string {
        return $this->getCmfConfig()->config('ui.views_subfolder', 'admin') . '.';
    }

    public function renderBasicUIView(): string {
        return $this->renderUIView('ui', [
            'sidebarLogo' => $this->getSidebarLogo(),
        ]);
    }

    public function getSidebarLogo(): string {
        return (string)($this->getCmfConfig()->config('ui.sidebar_logo') ?: $this->defaultSidebarLogo);
    }

    public function renderScaffoldTemplates(ScaffoldConfigInterface $scaffoldConfig) {
        $view = $scaffoldConfig instanceof KeyValueTableScaffoldConfig
            ? $this->scaffoldTemplatesForNormalTableViewPath
            : $this->scaffoldTemplatesForKeyValueTableViewPath;
        return view(
            $view,
            array_merge(
                $scaffoldConfig->getConfigsForTemplatesRendering(),
                ['tableNameForRoutes' => $scaffoldConfig::getResourceName()]
            )
        )->render();
    }

    protected function loadUIViewsFromConfig() {
        if (!$this->isUIViewsLoadedFromConfigs) {
            $this->UIViews = array_replace(
                $this->UIViews,
                (array)$this->getCmfConfig()->config('ui.views', [])
            );
        }
    }

    public function getUIView(string $viewName) {
        $this->loadUIViewsFromConfig();
        if (!isset($this->UIViews[$viewName])) {
            abort(HttpCode::NOT_FOUND, "There is no UI view with name [$viewName]");
        }
        return $this->UIViews[$viewName];
    }

    public function renderUIView(string $viewName, array $data = []): string {
        return view($this->getUIView($viewName), $data)->render();
    }

    public function getResources(): array {
        if ($this->resources === null) {
            $this->resources = [];
            /** @var ScaffoldConfigInterface $scaffoldConfigClass */
            foreach ((array)$this->getCmfConfig()->config('ui.resources', []) as $scaffoldConfigClass) {
                $this->resources[$scaffoldConfigClass::getResourceName()] = $scaffoldConfigClass;
            }
        }
        return $this->resources;
    }

    /**
     * Get ScaffoldConfig instance
     * @param string $resourceName - table name passed via route parameter, may differ from $table->getTableName()
     *      and added here to be used in child configs when you need to use scaffolds with fake table names.
     *      It should be used together with static::getModelByTableName() to provide correct model for a fake table name
     * @return ScaffoldConfigInterface
     * @throws \InvalidArgumentException
     */
    public function getScaffoldConfig(string $resourceName): ScaffoldConfigInterface {
        if (!isset($this->scaffoldConfigs[$resourceName])) {
            $className = $this->getScaffoldConfigClass($resourceName);
            $this->scaffoldConfigs[$resourceName] = new $className();
        }
        return $this->scaffoldConfigs[$resourceName];
    }

    /**
     * @param string $resourceName
     * @return string|ScaffoldConfig
     * @throws \InvalidArgumentException
     */
    public function getScaffoldConfigClass(string $resourceName): string {
        return array_get($this->getResources(), $resourceName, function () use ($resourceName) {
            throw new \InvalidArgumentException(
                'There is no known ScaffoldConfig class for resource "' . $resourceName . '"'
            );
        });
    }

    /**
     * Get values for menu items counters (details in CmfConfig::menu())
     * @return array like ['pending_orders' => '2']
     */
    public function getValuesForMenuItemsCounters(): array {
        $counters = [];
        /** @var ScaffoldConfigInterface $scaffoldConfigClass */
        foreach ($this->getResources() as $scaffoldConfigClass) {
            $counterClosure = $scaffoldConfigClass::getMenuItemCounterValue();
            if (!empty($counterClosure)) {
                $counters[$scaffoldConfigClass::getMenuItemCounterName()] = value($counterClosure);
            }
        }
        return $counters;
    }

    /**
     * Get TableInterface instance for $tableName
     * Note: can be ovewritted to allow usage of fake tables in resources routes
     * It is possible to use this with static::getScaffoldConfig() to alter default scaffold configs
     * @param string $resourceName
     * @return TableInterface
     */
    public function getTableByResourceName(string $resourceName): TableInterface {
        if (!isset($this->tables[$resourceName])) {
            $scaffoldConfigClass = $this->getScaffoldConfigClass($resourceName);
            $this->tables[$resourceName] = $scaffoldConfigClass::getTable();
        }
        return $this->tables[$resourceName];
    }

    /**
     * JS application settings (accessed via CmfSettings global variable)
     * @return array
     */
    public function getJsAppSettings(): array {
        $cmfConfig = $this->getCmfConfig();
        return [
            'isDebug' => config('app.debug'),
            'rootUrl' => '/' . trim($cmfConfig::url_prefix(), '/'),
            'enablePing' => (int)$cmfConfig::config('ping_interval') > 0,
            'pingInterval' => (int)$cmfConfig::config('ping_interval') * 1000,
            'uiUrl' => $cmfConfig::route('cmf_main_ui', [], false),
            'userDataUrl' => $cmfConfig::route('cmf_profile_data', [], false),
            'menuCountersDataUrl' => $cmfConfig::route('cmf_menu_counters_data', [], false),
            'defaultPageTitle' => $cmfConfig::default_page_title(),
            'pageTitleAddition' => $cmfConfig::page_title_addition(),
            'localizationStrings' => $cmfConfig::transGeneral('ui.js_component'),
        ];
    }

    /**
     * Variables that will be sent to js and stored into AppData
     * To access data from js code use AppData.key_name
     * @return array
     */
    public function getJsAppData(): array {
        return [];
    }

    /**
     * @param string $itemKey
     * @param array|\Closure $menuItem - format: see menu()
     */
    public function addCustomMenuItem(string $itemKey, $menuItem) {
        $this->customMenuItems[$itemKey] = $menuItem;
        if ($this->allMenuItems) {
            $menuItem = value($menuItem);
            if (is_array($menuItem) && !empty($menuItem)) {
                $this->allMenuItems[$itemKey] = $menuItem;
            }
        }
    }

    public function getMenuItems(): array {
        if (!$this->allMenuItems) {
            $this->allMenuItems = $this->getMenuItemsFromScaffoldConfigs();
            foreach ($this->customMenuItems as $key => $menuItem) {
                $menuItem = value($menuItem);
                if (is_array($menuItem) && !empty($menuItem)) {
                    $this->allMenuItems[$key] = $menuItem;
                }
            }
        }
        return $this->allMenuItems;
    }

    protected function getMenuItemsFromScaffoldConfigs(): array {
        if ($this->menuItemsFromScaffoldConfigs === null) {
            $this->menuItemsFromScaffoldConfigs = [];
            /** @var ScaffoldConfig $scaffoldConfigClass */
            foreach ($this->getResources() as $resourceName => $scaffoldConfigClass) {
                $this->menuItemsFromScaffoldConfigs[$resourceName] = $scaffoldConfigClass::getMainMenuItem();
            }
        }
        return $this->menuItemsFromScaffoldConfigs;
    }

}


<?php

/**
 * This file is part of MetaModels/attribute_translatedtags.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/attribute_translatedtags
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @author     Richard Henkenjohann <richardhenkenjohann@googlemail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/attribute_translatedtags/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\AttributeTranslatedTagsBundle\EventListener\DcGeneral\Table\Attribute;

use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\GetPropertyOptionsEvent;
use Doctrine\DBAL\Connection;
use MetaModels\IFactory;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\TranslatorInterface;

/**
 * Class PropertyOptionsListener
 *
 * Loads the property options.
 */
class PropertyOptionsListener
{

    /**
     * MetaModels factory.
     *
     * @var IFactory
     */
    private $factory;

    /**
     * Database connection.
     *
     * @var Connection
     */
    private $connection;

    /**
     * Translator.
     *
     * @var TranslatorInterface
     */
    private $translator;

    /**
     * PropertyOptionsListener constructor.
     *
     * @param IFactory            $factory    The MetaModels factory.
     * @param Connection          $connection The database connection.
     * @param TranslatorInterface $translator The translator.
     */
    public function __construct(IFactory $factory, Connection $connection, TranslatorInterface $translator)
    {
        $this->factory    = $factory;
        $this->connection = $connection;
        $this->translator = $translator;
    }

    /**
     * Retrieve all column names for the current selected table.
     *
     * @param GetPropertyOptionsEvent $event The event.
     *
     * @return void
     */
    public function getLangColumnNames(GetPropertyOptionsEvent $event)
    {
        if (($event->getPropertyName() !== 'tag_langcolumn')
            || ('tl_metamodel_attribute' !== $event->getEnvironment()->getDataDefinition()->getName())
        ) {
            return;
        }

        $table = $event->getModel()->getProperty('tag_table');
        if (0 === \strpos($table, 'mm_')) {
            $attributes = $this->getAttributeNamesFrom($table);
            \asort($attributes);

            $sqlKey       = $this->translator->trans(
                'tl_metamodel_attribute.tag_column_type.sql',
                [],
                'tl_metamodel_attribute'
            );
            $attributeKey = $this->translator->trans(
                'tl_metamodel_attribute.tag_column_type.attribute',
                [],
                'tl_metamodel_attribute'
            );

            $event->setOptions(
                [
                    $sqlKey       => \array_diff_key(
                        $this->getColumnNamesFrom($table),
                        \array_flip(\array_keys($attributes))
                    ),
                    $attributeKey => $attributes,
                ]
            );

            return;
        }
        $result = $this->getColumnNamesFrom($table);
        if (!empty($result)) {
            \asort($result);
            $event->setOptions($result);
        }
    }

    /**
     * Retrieve all database table names.
     *
     * @param GetPropertyOptionsEvent $event The event.
     *
     * @return void
     *
     * @throws InvalidArgumentException If the locale contains invalid characters.
     */
    public function handleSrcTableNames(GetPropertyOptionsEvent $event)
    {
        if (($event->getPropertyName() !== 'tag_srctable')
            || ($event->getEnvironment()->getDataDefinition()->getName() !== 'tl_metamodel_attribute')) {
            return;
        }

        $sqlTable     = $this->translator->trans(
            'tl_metamodel_attribute.tag_table_type.sql-table',
            [],
            'contao_tl_metamodel_attribute'
        );
        $translated   = $this->translator->trans(
            'tl_metamodel_attribute.tag_table_type.translated',
            [],
            'contao_tl_metamodel_attribute'
        );
        $untranslated = $this->translator->trans(
            'tl_metamodel_attribute.tag_table_type.untranslated',
            [],
            'contao_tl_metamodel_attribute'
        );

        $result = $this->getMetaModelTableNames($translated, $untranslated);
        foreach ($this->connection->getSchemaManager()->listTableNames() as $table) {
            if (0 !== \strpos($table, 'mm_')) {
                $result[$sqlTable][$table] = $table;
            }
        }

        if (\is_array($result[$translated])) {
            \asort($result[$translated]);
        }

        if (\is_array($result[$untranslated])) {
            \asort($result[$untranslated]);
        }

        if (\is_array($result[$sqlTable])) {
            \asort($result[$sqlTable]);
        }

        $event->setOptions($result);
    }

    /**
     * Retrieve all column names of type int for the current selected table.
     *
     * @param GetPropertyOptionsEvent $event The event.
     *
     * @return void
     */
    public function getSourceColumnNames(GetPropertyOptionsEvent $event)
    {
        if (($event->getPropertyName() !== 'tag_srcsorting')
            || ($event->getEnvironment()->getDataDefinition()->getName() !== 'tl_metamodel_attribute')) {
            return;
        }

        $model = $event->getModel();
        $table = $model->getProperty('select_srctable');

        if (!$table || !$this->connection->getSchemaManager()->tablesExist([$table])) {
            return;
        }

        $result = [];

        $indexes = $this->connection->getSchemaManager()->listTableIndexes($table);
        foreach ($this->connection->getSchemaManager()->listTableColumns($table) as $column) {
            if (\array_key_exists($column->getName(), $indexes)) {
                continue;
            }
            $colName = $column->getName();

            $result[$colName] = $colName;
        }

        $event->setOptions($result);
    }

    /**
     * Retrieve all MetaModels table names.
     *
     * @param string $keyTranslated   The array key to use for translated MetaModels.
     *
     * @param string $keyUntranslated The array key to use for untranslated MetaModels.
     *
     * @return array
     */
    private function getMetaModelTableNames($keyTranslated, $keyUntranslated)
    {
        $result = [];
        foreach ($this->factory->collectNames() as $table) {
            $metaModel = $this->factory->getMetaModel($table);
            if (null === $metaModel) {
                continue;
            }

            if ($metaModel->isTranslated()) {
                $result[$keyTranslated][$table] = \sprintf('%s (%s)', $metaModel->get('name'), $table);
            } else {
                $result[$keyUntranslated][$table] = \sprintf('%s (%s)', $metaModel->get('name'), $table);
            }
        }

        return $result;
    }

    /**
     * Retrieve all attribute names from a given MetaModel name.
     *
     * @param string $metaModelName The name of the MetaModel.
     *
     * @return string[]
     */
    private function getAttributeNamesFrom($metaModelName)
    {
        $metaModel = $this->factory->getMetaModel($metaModelName);
        $result    = [];
        if (null === $metaModel) {
            return $result;
        }

        foreach ($metaModel->getAttributes() as $attribute) {
            $name   = $attribute->getName();
            $column = $attribute->getColName();
            $type   = $attribute->get('type');

            $result[$column] = \sprintf('%s (%s - %s)', $name, $column, $type);
        }

        return $result;
    }

    /**
     * Retrieve all column names for the given table.
     *
     * @param string $table The table name.
     *
     * @return array
     */
    private function getColumnNamesFrom($table)
    {
        if (0 === \strpos($table, 'mm_')) {
            $attributes = $this->getAttributeNamesFrom($table);
            \asort($attributes);

            $sql       = $this->translator->trans(
                'tl_metamodel_attribute.tag_column_type.sql',
                [],
                'contao_tl_metamodel_attribute'
            );
            $attribute = $this->translator->trans(
                'tl_metamodel_attribute.tag_column_type.attribute',
                [],
                'contao_tl_metamodel_attribute'
            );

            return [
                $sql       => \array_diff_key(
                    $this->getColumnNamesFromTable($table),
                    \array_flip(\array_keys($attributes))
                ),
                $attribute => $attributes,
            ];
        }

        return $this->getColumnNamesFromTable($table);
    }

    /**
     * Retrieve all columns from a database table.
     *
     * @param string     $tableName  The database table name.
     *
     * @param array|null $typeFilter Optional of types to filter for.
     *
     * @return string[]
     */
    private function getColumnNamesFromTable($tableName, $typeFilter = null)
    {
        if (!$this->connection->getSchemaManager()->tablesExist([$tableName])) {
            return [];
        }

        $result = [];
        foreach ($this->connection->getSchemaManager()->listTableColumns($tableName) as $column) {
            if (($typeFilter === null) || \in_array($column->getType()->getName(), $typeFilter, true)) {
                $result[$column->getName()] = $column->getName();
            }
        }

        if (!empty($result)) {
            \asort($result);

            return $result;
        }

        return $result;
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Symfony\Serializer\Normalizer;

use Ekyna\Component\Commerce\Common\Util\FormatterAwareTrait;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentStates;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentSubjectInterface;
use Ekyna\Component\Commerce\Stock\Model\StockAssignmentInterface;
use Ekyna\Component\Resource\Serializer\AbstractResourceNormalizer;

/**
 * Class StockAssignmentNormalizer
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Serializer\Normalizer
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class StockAssignmentNormalizer extends AbstractResourceNormalizer
{
    use FormatterAwareTrait;

    /**
     * @inheritdoc
     *
     * @param StockAssignmentInterface $assignment
     */
    public function normalize($assignment, $format = null, array $context = [])
    {
        $data = [];

        if ($this->contextHasGroup(['StockView', 'StockAssignment'], $context)) {
            $formatter = $this->getFormatter();

            $data = array_replace($data, [
                'sold'    => $formatter->number($assignment->getSoldQuantity()),
                'shipped' => $formatter->number($assignment->getShippedQuantity()),
            ]);

            if ($this->contextHasGroup('StockView', $context)) {
                $sale = $assignment->getSaleItem()->getSale();
                $data['order_id'] = $sale->getId();
                $data['preparation'] =
                    $sale instanceof ShipmentSubjectInterface &&
                    $sale->getShipmentState() === ShipmentStates::STATE_PREPARATION;
            }

            if ($this->contextHasGroup('StockAssignment', $context)) {
                $data['unit'] = $this->normalizeObject($assignment->getStockUnit(), $format, $context);
            }
        }

        return $data;
    }

    /**
     * @inheritdoc
     */
    public function denormalize($data, $class, $format = null, array $context = [])
    {
        //$object = parent::denormalize($data, $class, $format, $context);

        throw new \Exception('Not yet implemented');
    }

    /**
     * @inheritdoc
     */
    public function supportsNormalization($data, $format = null)
    {
        return $data instanceof StockAssignmentInterface;
    }

    /**
     * @inheritdoc
     */
    public function supportsDenormalization($data, $type, $format = null)
    {
        return class_exists($type) && is_subclass_of($type, StockAssignmentInterface::class);
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Payum\Action;

use Ekyna\Component\Commerce\Bridge\Payum\Request\Status;
use Ekyna\Component\Commerce\Payment\Model\PaymentInterface;
use Payum\Core\Action\ActionInterface;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\GatewayAwareInterface;
use Payum\Core\GatewayAwareTrait;
use Payum\Core\Request\GetHumanStatus;
use Payum\Core\Request\Sync;

/**
 * Class StatusAction
 * @package Ekyna\Component\Commerce\Bridge\Payum\Action
 * @author  Étienne Dauvergne <contact@ekyna.com>
 */
class StatusAction implements ActionInterface, GatewayAwareInterface
{
    use GatewayAwareTrait;

    /**
     * {@inheritDoc}
     *
     * @param Status $request
     */
    public function execute($request)
    {
        RequestNotSupportedException::assertSupports($this, $request);

        /** @var PaymentInterface $payment */
        $payment = $request->getModel();

        $this->gateway->execute(new Sync($payment));

        $status = new GetHumanStatus($payment);
        $this->gateway->execute($status);

        $payment->setState($status->getValue());
    }

    /**
     * {@inheritDoc}
     */
    public function supports($request)
    {
        return $request instanceof Status
            //&& $request->getToken()
            && $request->getModel() instanceof PaymentInterface;
    }
}


<?php

namespace Ekyna\Component\Commerce\Document\Builder;

use Ekyna\Component\Commerce\Common\Model as Common;
use Ekyna\Component\Commerce\Document\Model as Document;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Commerce\Exception\LogicException;
use Ekyna\Component\Commerce\Shipment\Model\RelayPointInterface;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
use Symfony\Component\Intl\Intl;

/**
 * Class DocumentBuilder
 * @package Ekyna\Component\Commerce\Common\Builder
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class DocumentBuilder implements DocumentBuilderInterface
{
    /**
     * @var PhoneNumberUtil
     */
    private $phoneNumberUtil;

    /**
     * Constructor.
     *
     * @param PhoneNumberUtil $phoneNumberUtil
     */
    public function __construct(PhoneNumberUtil $phoneNumberUtil = null)
    {
        $this->phoneNumberUtil = $phoneNumberUtil ?: PhoneNumberUtil::getInstance();
    }

    /**
     * @inheritDoc
     */
    public function build(Document\DocumentInterface $document)
    {
        if (null === $sale = $document->getSale()) {
            throw new LogicException("Document's sale must be set at this point.");
        }

        $this->update($document);

        // Goods lines
        $this->buildGoodsLines($document);

        // Discounts lines
        $this->buildDiscountsLines($document);

        // Shipment line
        $this->buildShipmentLine($document);
    }

    /**
     * @inheritdoc
     */
    public function update(Document\DocumentInterface $document)
    {
        if (null === $sale = $document->getSale()) {
            throw new LogicException("Invoice's sale must be set at this point.");
        }

        $changed = false;

        // Currency
        $code = $sale->getCurrency()->getCode();
        if ($document->getCurrency() !== $code) {
            $document->setCurrency($code);
            $changed = true;
            // TODO Convert prices / Recalculate ?
        }

        // Customer
        $data = $this->buildCustomerData($sale);
        if ($document->getCustomer() !== $data) {
            $document->setCustomer($data);
            $changed = true;
        }

        // Invoice address
        $data = $this->buildAddressData($sale->getInvoiceAddress(), $sale->getLocale());
        if ($document->getInvoiceAddress() !== $data) {
            $document->setInvoiceAddress($data);
            $changed = true;
        }

        // Delivery address
        $data = $sale->getDeliveryAddress()
            ? $this->buildAddressData($sale->getDeliveryAddress(), $sale->getLocale())
            : null;
        if ($document->getDeliveryAddress() !== $data) {
            $document->setDeliveryAddress($data);
            $changed = true;
        }

        // RelayPoint
        if (null !== $data = $sale->getRelayPoint()) {
            $data = $this->buildAddressData($data, $sale->getLocale());
        }
        if ($document->getRelayPoint() !== $data) {
            $document->setRelayPoint($data);
            $changed = true;
        }

        // Comment
        if (empty($document->getComment()) && !empty($comment = $sale->getDocumentComment())) {
            $document->setComment($comment);
            $changed = true;
        }

        return $changed;
    }

    /**
     * @inheritdoc
     */
    public function buildGoodLine(Common\SaleItemInterface $item, Document\DocumentInterface $document)
    {
        $line = null;

        if ($item->isPrivate()) {
            return null;
        }

        // Skip compound with only public children
        if (!($item->isCompound() && !$item->hasPrivateChildren())) {
            // Existing line lookup
            foreach ($document->getLinesByType(Document\DocumentLineTypes::TYPE_GOOD) as $documentLine) {
                if ($documentLine->getSaleItem() === $item) {
                    $line = $documentLine;
                }
            }
            // Not found, create it
            if (null === $line) {
                $line = $this->createLine($document);
                $line
                    ->setDocument($document)
                    ->setType(Document\DocumentLineTypes::TYPE_GOOD)
                    ->setSaleItem($item)
                    ->setDesignation($item->getDesignation())
                    ->setDescription($item->getDescription())
                    ->setReference($item->getReference())
                    ->setQuantity($item->getTotalQuantity());
            }
        }

        if ($item->hasChildren()) {
            foreach ($item->getChildren() as $child) {
                $this->buildGoodLine($child, $document);
            }
        }

        return $line;
    }

    /**
     * @inheritdoc
     */
    public function buildDiscountLine(Common\SaleAdjustmentInterface $adjustment, Document\DocumentInterface $document)
    {
        if ($adjustment->getType() !== Common\AdjustmentTypes::TYPE_DISCOUNT) {
            throw new InvalidArgumentException("Unexpected adjustment type.");
        }

        $line = null;
        // Existing line lookup
        foreach ($document->getLinesByType(Document\DocumentLineTypes::TYPE_DISCOUNT) as $documentLine) {
            if ($documentLine->getSaleAdjustment() === $adjustment) {
                $line = $documentLine;
            }
        }
        // Not found, create it
        if (null === $line) {
            $line = $this->createLine($document);
            $line
                ->setDocument($document)
                ->setType(Document\DocumentLineTypes::TYPE_DISCOUNT)
                ->setSaleAdjustment($adjustment)
                ->setDesignation($adjustment->getDesignation());
        }

        return $line;
    }

    /**
     * @inheritdoc
     */
    public function buildShipmentLine(Document\DocumentInterface $document)
    {
        $sale = $document->getSale();

        if (null === $method = $sale->getShipmentMethod()) {
            return null;
        }

        // Existing line lookup
        $shipmentLines = $document->getLinesByType(Document\DocumentLineTypes::TYPE_SHIPMENT);
        $line = !empty($shipmentLines) ? current($shipmentLines) : null;

        // Not found, create it
        if (null === $line) {
            $line = $this->createLine($document);
            $line
                ->setDocument($document)
                ->setType(Document\DocumentLineTypes::TYPE_SHIPMENT)
                ->setDesignation($method->getTitle());
        }

        return $line;
    }

    /**
     * Builds the document's customer data.
     *
     * @param Common\SaleInterface $sale
     *
     * @return array
     */
    protected function buildCustomerData(Common\SaleInterface $sale)
    {
        if (null !== $customer = $sale->getCustomer()) {
            return [
                'number'    => $customer->getNumber(),
                'company'   => $customer->getCompany(),
                'full_name' => trim($customer->getFirstName() . ' ' . $customer->getLastName()),
                'email'     => $customer->getEmail(),
                'phone'     => $this->formatPhoneNumber($customer->getPhone()),
                'mobile'    => $this->formatPhoneNumber($customer->getMobile()),
            ];
        } else {
            return [
                'number'    => null,
                'company'   => $sale->getCompany(),
                'full_name' => trim($sale->getFirstName() . ' ' . $sale->getLastName()),
                'email'     => $sale->getEmail(),
                'phone'     => null,
                'mobile'    => null,
            ];
        }
    }

    /**
     * Builds the document's address data.
     *
     * @param Common\AddressInterface $address
     * @param string                  $locale
     *
     * @return array
     */
    protected function buildAddressData(Common\AddressInterface $address, string $locale)
    {
        // TODO localize
        $country = Intl::getRegionBundle()->getCountryName($address->getCountry()->getCode(), $locale);

        $fullName = trim($address->getFirstName() . ' ' . $address->getLastName());

        // TODO if empty full customer name

        $data = [
            'company'     => $address->getCompany(),
            'full_name'   => $fullName,
            'street'      => $address->getStreet(),
            'complement'  => $address->getComplement(),
            'supplement'  => $address->getSupplement(),
            'postal_code' => $address->getPostalCode(),
            'city'        => $address->getCity(),
            'country'     => $country,
            'state'       => '',
            'phone'       => $this->formatPhoneNumber($address->getPhone()),
            'mobile'      => $this->formatPhoneNumber($address->getMobile()),
        ];

        if ($address instanceof RelayPointInterface) {
            $data['number'] = $address->getNumber();
        }

        return $data;
    }

    /**
     * Formats the given phone number.
     *
     * @param PhoneNumber $number
     *
     * @return string
     */
    protected function formatPhoneNumber(PhoneNumber $number = null)
    {
        if ($number) {
            return $this->phoneNumberUtil->format($number, PhoneNumberFormat::INTERNATIONAL);
        }

        return null;
    }

    /**
     * Builds the document's goods lines.
     *
     * @param Document\DocumentInterface $document
     */
    protected function buildGoodsLines(Document\DocumentInterface $document)
    {
        foreach ($document->getSale()->getItems() as $item) {
            $this->buildGoodLine($item, $document);
        }
    }

    /**
     * Builds the document's discounts lines.
     *
     * @param Document\DocumentInterface $document
     */
    protected function buildDiscountsLines(Document\DocumentInterface $document)
    {
        $sale = $document->getSale();

        if (!$sale->hasAdjustments(Common\AdjustmentTypes::TYPE_DISCOUNT)) {
            return;
        }

        $adjustments = $sale->getAdjustments();
        foreach ($adjustments as $adjustment) {
            if ($adjustment->getType() === Common\AdjustmentTypes::TYPE_DISCOUNT) {
                $this->buildDiscountLine($adjustment, $document);
            }
        }
    }

    /**
     * Creates a new line.
     *
     * @param Document\DocumentInterface $document
     *
     * @return Document\DocumentLineInterface
     */
    protected function createLine(Document\DocumentInterface $document)
    {
        return new Document\DocumentLine();
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints;

use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumber as PhoneNumberObject;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberType;
use libphonenumber\PhoneNumberUtil;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
 * Phone number validator.
 */
class PhoneNumberValidator extends ConstraintValidator
{
    /**
     * {@inheritdoc}
     *
     * @var PhoneNumber $constraint
     */
    public function validate($value, Constraint $constraint)
    {
        if (null === $value || '' === $value) {
            return;
        }

        if (!is_scalar($value) && !(is_object($value) && method_exists($value, '__toString'))) {
            throw new UnexpectedTypeException($value, 'string');
        }

        $phoneUtil = PhoneNumberUtil::getInstance();

        if (false === $value instanceof PhoneNumberObject) {
            $value = (string) $value;

            try {
                $phoneNumber = $phoneUtil->parse($value, $constraint->defaultRegion);
            } catch (NumberParseException $e) {
                $this->addViolation($value, $constraint);

                return;
            }
        } else {
            $phoneNumber = $value;
            $value = $phoneUtil->format($phoneNumber, PhoneNumberFormat::INTERNATIONAL);
        }

        if (false === $phoneUtil->isValidNumber($phoneNumber)) {
            $this->addViolation($value, $constraint);

            return;
        }

        $validTypes = array();

        foreach ($constraint->getTypes() as $type) {
            switch ($type) {
                case PhoneNumber::FIXED_LINE:
                    array_push($validTypes, PhoneNumberType::FIXED_LINE, PhoneNumberType::FIXED_LINE_OR_MOBILE);
                    break;
                case PhoneNumber::MOBILE:
                    array_push($validTypes, PhoneNumberType::MOBILE, PhoneNumberType::FIXED_LINE_OR_MOBILE);
                    break;
                case PhoneNumber::PAGER:
                    array_push($validTypes, PhoneNumberType::PAGER);
                    break;
                case PhoneNumber::PERSONAL_NUMBER:
                    array_push($validTypes, PhoneNumberType::PERSONAL_NUMBER);
                    break;
                case PhoneNumber::PREMIUM_RATE:
                    array_push($validTypes, PhoneNumberType::PREMIUM_RATE);
                    break;
                case PhoneNumber::SHARED_COST:
                    array_push($validTypes, PhoneNumberType::SHARED_COST);
                    break;
                case PhoneNumber::TOLL_FREE:
                    array_push($validTypes, PhoneNumberType::TOLL_FREE);
                    break;
                case PhoneNumber::UAN:
                    array_push($validTypes, PhoneNumberType::UAN);
                    break;
                case PhoneNumber::VOIP:
                    array_push($validTypes, PhoneNumberType::VOIP);
                    break;
                case PhoneNumber::VOICEMAIL:
                    array_push($validTypes, PhoneNumberType::VOICEMAIL);
                    break;
            }
        }

        $validTypes = array_unique($validTypes);

        if (count($validTypes)) {
            $type = $phoneUtil->getNumberType($phoneNumber);

            if (false === in_array($type, $validTypes, true)) {
                $this->addViolation($value, $constraint);

                return;
            }
        }
    }

    /**
     * Add a violation.
     *
     * @param mixed      $value      The value that should be validated.
     * @param Constraint $constraint The constraint for the validation.
     */
    private function addViolation($value, Constraint $constraint)
    {
        /** @var \Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber $constraint */
        if ($this->context instanceof ExecutionContextInterface) {
            $this->context->buildViolation($constraint->getMessage())
                ->setParameter('{{ type }}', $constraint->getType())
                ->setParameter('{{ value }}', $this->formatValue($value))
                ->setCode(PhoneNumber::INVALID_PHONE_NUMBER_ERROR)
                ->addViolation();
        } else {
            $this->context->addViolation($constraint->getMessage(), array(
                '{{ type }}' => $constraint->getType(),
                '{{ value }}' => $value
            ));
        }
    }
}


<?php

namespace PeskyCMF\Auth;

use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;
use Illuminate\Mail\Message;
use PeskyCMF\Config\CmfConfig;
use PeskyCMF\Db\Admins\CmfAdmin;
use PeskyCMF\Db\CmfDbRecord;
use PeskyCMF\Db\Traits\ResetsPasswordsViaAccessKey;
use PeskyCMF\Event\CmfUserAuthenticated;
use PeskyCMF\Http\CmfJsonResponse;
use PeskyCMF\HttpCode;
use PeskyCMF\Listeners\CmfUserAuthenticatedEventListener;
use PeskyCMF\Traits\DataValidationHelper;
use PeskyORM\Core\DbExpr;
use PeskyORM\ORM\Column;
use PeskyORM\ORM\RecordInterface;
use PeskyORM\ORM\TableInterface;
use Swayok\Utils\Set;

class CmfAuthModule {

    use DataValidationHelper,
        AuthorizesRequests;

    protected $cmfConfig;
    protected $authPolicyName = 'CmfAccessPolicy';
    protected $emailColumnName;
    protected $authGuard;
    protected $originalUserFromLoginAsActionSessionKey = '__original_user';
    // page views
    protected $userLoginPageViewPath = 'cmf::ui.login';
    protected $forgotPasswordPageViewPath = 'cmf::ui.forgot_password';
    protected $replacePasswordPageViewPath = 'cmf::ui.replace_password';
    protected $userProfilePageViewPath = 'cmf::page.profile';
    protected $registrationPageViewPath = 'cmf::ui.registration';
    // emails views
    protected $passwordRevoceryEmailViewPath = 'cmf::emails.password_restore_instructions';
    // html elements
    protected $defaultLoginPageLogo = '<img src="/packages/cmf/raw/img/peskycmf-logo-black.svg" width="340" alt=" " class="mb15">';

    public function __construct(CmfConfig $cmfConfig) {
        $this->cmfConfig = $cmfConfig;
    }

    public function init() {
        $this->configureAuthorizationGatesAndPolicies();
        $authGuardName = $this->getAuthGuardName();
        \Auth::shouldUse($authGuardName);
        $this->authGuard = \Auth::guard($authGuardName);
        $this->listenForUserAuthenticationEvents();
    }

    /**
     * @return CmfConfig
     */
    public function getCmfConfig(): CmfConfig {
        return $this->cmfConfig;
    }

    /**
     * @return string
     */
    protected function getAuthGuardName(): string {
        return $this->getCmfConfig()->config('auth.guard.name', function () {
            $config = $this->getCmfConfig()->config('auth.guard');
            return is_string($config) ? $config : 'admin';
        });
    }

    /**
     * @return \PeskyCMF\Db\Admins\CmfAdmin|\Illuminate\Contracts\Auth\Authenticatable|\PeskyCMF\Db\Traits\ResetsPasswordsViaAccessKey|\App\Db\Admins\Admin|null
     */
    public function getUser(): ?RecordInterface {
        return $this->getAuthGuard()->user();
    }

    /**
     * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard|\Illuminate\Auth\SessionGuard
     */
    public function getAuthGuard(): ?Guard {
        return $this->authGuard ?: \Auth::guard($this->getAuthGuardName());
    }

    /**
     * @return string|RecordInterface
     */
    public function getUserRecordClass(): ?string {
        return $this->getCmfConfig()->config('auth.user_record_class', function () {
            throw new \UnexpectedValueException('You need to provide a DB Record class for users');
        });
    }

    /**
     * @return \PeskyORM\ORM\TableInterface|\PeskyCMF\Db\Admins\CmfAdminsTable|\App\Db\Admins\AdminsTable
     */
    public function getUsersTable(): TableInterface {
        $recordClass = $this->getUserRecordClass(); //< do not merge with next line!!!
        return $recordClass::getTable();
    }

    /**
     * @param string $email
     * @return bool
     */
    public function loginOnceUsingEmail($email): bool {
        return $this->getAuthGuard()->once([$this->getUserEmailColumnName() => $email]);
    }

    /**
     * Logo image for login and restore password pages
     * @return string
     */
    public function getLoginPageLogo(): string {
        return $this->getCmfConfig()->config('auth.login_logo') ?: $this->defaultLoginPageLogo;
    }

    /**
     * @return string
     */
    public function renderUserLoginPageView(): string {
        if ($this->getUser()) {
            return view('cmf::ui.redirect', ['url' => $this->getIntendedUrl()]);
        }
        return view($this->userLoginPageViewPath, ['authModule' => $this])->render();
    }

    /**
     * Enable/disable password restore link in login form
     * @return bool
     */
    public function isRegistrationAllowed(): bool {
        return $this->getCmfConfig()->config('auth.is_registration_allowed', true);
    }

    /**
     * @return \PeskyCMF\Http\CmfJsonResponse|string
     */
    public function renderUserRegistrationPageView() {
        if (!$this->isRegistrationAllowed()) {
            return cmfJsonResponse(HttpCode::NOT_FOUND);
        }
        return view($this->registrationPageViewPath, [
            'authModule' => $this,
        ])->render();
    }

    /**
     * @param Request $request
     * @return \PeskyCMF\Http\CmfJsonResponse|array
     */
    public function processUserRegistrationRequest(Request $request) {
        if (!$this->isRegistrationAllowed()) {
            return cmfJsonResponse(HttpCode::NOT_FOUND);
        }
        $dataOrResponse = $this->validateAndGetDataForRegistration($request);
        if (!is_array($dataOrResponse)) {
            return $dataOrResponse;
        } else {
            $user = $this->getUsersTable()->newRecord();
            $user->fromData($dataOrResponse, false);
            $this->addCustomRegistrationData($user);
            $user->save();
            $this->afterRegistration($user);
            $this->getAuthGuard()->login($user, true);
            return cmfJsonResponse()
                ->setForcedRedirect($this->getCmfConfig()->home_page_url());
        }
    }

    /**
     * @param Request $request
     * @return array|CmfJsonResponse
     */
    protected function validateAndGetDataForRegistration(Request $request) {
        $validationRules = [
            'password' => 'required|string|min:6|confirmed',
        ];
        $tableStricture = $this->getUsersTable()->getTableStructure();
        if ($tableStricture::hasColumn('name')) {
            $validationRules['name'] = 'nullable|max:200';
        }
        $usersTable = $tableStricture::getTableName();
        $userLoginCol = $this->getUserLoginColumnName();
        if ($tableStricture::hasColumn('email')) {
            if ($userLoginCol === 'email') {
                $validationRules['email'] = "required|email|unique:{$usersTable},email";
            } else {
                $validationRules['email'] = 'nullable|email';
            }
        }
        if ($userLoginCol !== 'email') {
            $validationRules[$userLoginCol] = "required|regex:%^[a-zA-Z0-9_@.-]+$%is|min:4|unique:$usersTable,$userLoginCol";
        }
        $columnsToSave = array_keys($validationRules);
        if ($this->isRecaptchaAvailable()) {
            $validationRules['g-recaptcha-response'] = 'recaptcha';
        }
        foreach ($this->getCustomRegistrationFieldsAndValidators() as $columnName => $rules) {
            if (is_int($columnName)) {
                $columnName = $rules;
            } else {
                $validationRules[$columnName] = $rules;
            }
            $columnsToSave[] = $columnName;
        }
        $validator = \Validator::make(
            $request->all(),
            $validationRules,
            Set::flatten((array)cmfTransCustom('.registration_form.errors'))
        );
        if ($validator->fails()) {
            return $this->makeValidationErrorsJsonResponse($validator->getMessageBag()->toArray());
        }
        return $request->only($columnsToSave);
    }

    /**
     * @return array
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function getDataForUserProfileForm(): array {
        /** @var CmfAdmin $admin */
        $admin = $this->getUser();
        $this->authorize('resource.details', ['cmf_profile', $admin]);
        $adminData = $admin->toArray();
        if (!empty($adminData['role'])) {
            $adminData['_role'] = $admin->role;
            $role = $admin->role;
            if ($admin::hasColumn('is_superadmin') && $admin->is_superadmin) {
                $role = 'superadmin';
            }
            $adminData['role'] = cmfTransCustom('.admins.role.' . $role);
        }
        return $adminData;
    }

    /**
     * @return string
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function renderUserProfilePageView(): string {
        $user = $this->getUser();
        $this->authorize('resource.details', ['cmf_profile', $user]);
        return view($this->userProfilePageViewPath, [
            'authModule' => $this,
            'user' => $user,
            'canSubmit' => \Gate::allows('resource.update', ['cmf_profile', $user])
        ])->render();
    }

    /**
     * @param Request $request
     * @return \PeskyCMF\Http\CmfJsonResponse
     * @throws \PeskyORM\Exception\DbException
     * @throws \Illuminate\Auth\Access\AuthorizationException
     * @throws \PeskyORM\Exception\InvalidDataException
     * @throws \PeskyORM\Exception\InvalidTableColumnConfigException
     * @throws \PeskyORM\Exception\OrmException
     */
    public function processUserProfileUpdateRequest(Request $request) {
        /** @var \PeskyCMF\Db\Admins\CmfAdmin $user */
        $user = $this->getUser();
        $this->authorize('resource.update', ['cmf_profile', $user]);
        $updatesOrResponse = $this->validateAndGetUserProfileUpdates($request, $user);
        if (!is_array($updatesOrResponse)) {
            return $updatesOrResponse;
        } else {
            $oldEmail = $user::hasColumn('email') ? $user->email : null;
            $user
                ->begin()
                ->updateValues($updatesOrResponse);
            if (!empty(trim($request->input('new_password')))) {
                $user->setPassword($request->input('new_password'));
            }
            $user->commit();
            if (
                isset($updatesOrResponse['email'])
                && $user::hasColumn('email')
                && strtolower(trim($oldEmail)) !== strtolower(trim($updatesOrResponse['email']))
            ) {
                $this->onUserEmailAddressChange($user, $oldEmail);
            }
            return cmfJsonResponse()
                ->setData(['_reload_user' => true])
                ->setMessage(cmfTransCustom('page.profile.saved'))
                ->reloadPage();
        }
    }

    /**
     * @return string
     */
    public function getAccessPolicyClassName(): string {
        return $this->getCmfConfig()->config('auth.acceess_policy_class') ?: CmfAccessPolicy::class;
    }

    /**
     * @param Request $request
     * @return \PeskyCMF\Http\CmfJsonResponse
     * @throws \Illuminate\Validation\ValidationException
     */
    public function processUserLoginRequest(Request $request): CmfJsonResponse {
        $userLoginColumn = $this->getUserLoginColumnName();
        $data = $this->validate($request, [
            $userLoginColumn => 'required' . ($userLoginColumn === 'email' ? '|email' : ''),
            'password' => 'required',
        ]);
        $credentials = [
            DbExpr::create("LOWER(`{$userLoginColumn}`) = LOWER(``" . trim($data[$userLoginColumn]) . '``)'),
            'password' => $data['password'],
        ];
        if (!$this->getAuthGuard()->attempt($credentials)) {
            return cmfJsonResponse(HttpCode::INVALID)
                ->setMessage(cmfTransCustom('.login_form.login_failed'));
        } else {
            return cmfJsonResponse()->setRedirect($this->getIntendedUrl());
        }
    }

    /**
     * @return \Illuminate\Http\RedirectResponse|\PeskyCMF\Http\CmfJsonResponse
     */
    public function processUserLogoutRequest() {
        $loginPageUrl = $this->getLoginPageUrl(true);
        if (\Session::has($this->originalUserFromLoginAsActionSessionKey)) {
            // logout to original account after 'login_as'
            $userInfo = \Session::pull($this->originalUserFromLoginAsActionSessionKey);
            $user = $this->getAuthGuard()->getProvider()->retrieveByToken(
                array_get($userInfo, 'id', -1),
                array_get($userInfo, 'token', -1)
            );
            if ($user) {
                // Warning: do not use Auth->login($user) - it will fail to login previous user
                $this->getAuthGuard()->loginUsingId($user->getAuthIdentifier(), false);
                $redirectTo = array_get($userInfo, 'url') ?: $loginPageUrl;
                return request()->ajax()
                    ? cmfJsonResponse()->setForcedRedirect($redirectTo)
                    : \Redirect::to($redirectTo);
            }
        }
        $this->logoutCurrentUser();
        return request()->ajax()
            ? cmfJsonResponse()->setForcedRedirect($loginPageUrl)
            : \Redirect::to($loginPageUrl);
    }

    /**
     * Logout current user, invalidate session and reset locale
     */
    public function logoutCurrentUser() {
        $this->getAuthGuard()->logout();
        \Session::remove($this->originalUserFromLoginAsActionSessionKey);
        \Session::invalidate();
        $this->getCmfConfig()->resetLocale();
    }

    /**
     * @param $otherUserId
     * @return \PeskyCMF\Http\CmfJsonResponse
     */
    public function processLoginAsOtherUserRequest($otherUserId) {
        $this->authorize('cmf_page', ['login_as']);
        /** @var Authenticatable|RecordInterface $currentUser */
        $currentUser = $this->getUser();
        $currentUserId = $currentUser->getAuthIdentifier();
        if ($currentUserId === $otherUserId || $currentUserId === (int)$otherUserId) {
            return cmfJsonResponse(HttpCode::CANNOT_PROCESS)
                ->setMessage(cmfTransCustom('admins.login_as.same_user'));
        }
        $token = $currentUser->getRememberToken();
        if (!$token) {
            return cmfJsonResponse(HttpCode::CANNOT_PROCESS)
                ->setMessage(cmfTransCustom('admins.login_as.no_auth_token'));
        }
        /** @var \PeskyCMF\Db\Admins\CmfAdmin|RecordInterface $otherUser */
        $otherUser = $this->getAuthGuard()->loginUsingId($otherUserId);
        if (!is_object($otherUser)) {
            // Warning: do not use Auth->login($currentUser) - it might fail
            $this->getAuthGuard()->loginUsingId($currentUserId, false);
            return cmfJsonResponse(HttpCode::CANNOT_PROCESS)
                ->setMessage(cmfTransCustom('admins.login_as.fail', ['id' => $otherUserId]));
        }
        \Session::put([
            $this->originalUserFromLoginAsActionSessionKey => [
                'id' => $currentUserId,
                'token' => $token,
                'url' => \URL::previous($this->getCmfConfig()->home_page_url(true)),
            ],
            $this->getCmfConfig()->session_message_key() => cmfTransCustom(
                'admins.login_as.success',
                ['user' => $otherUser->getValue($this->getUserLoginColumnName())]
            )
        ]);
        return cmfJsonResponse()
            ->setRedirect($this->getCmfConfig()->home_page_url());
    }

    /**
     * @param Request $request
     * @return \PeskyCMF\Http\CmfJsonResponse
     */
    public function startPasswordRecoveryProcess(Request $request) {
        if (!$this->isPasswordRestoreAllowed()) {
            return cmfJsonResponse(HttpCode::NOT_FOUND);
        }
        $validators = [
            'email' => 'required|email',
        ];
        if ($this->isRecaptchaAvailable()) {
            $validators['g-recaptcha-response'] = 'recaptcha';
        }
        $data = $this->validate($request, $validators);
        $email = strtolower(trim($data['email']));
        if ($this->loginOnceUsingEmail($email)) {
            /** @var CmfDbRecord|ResetsPasswordsViaAccessKey $user */
            $user = $this->getAuthGuard()->getLastAttempted();
            if (!method_exists($user, 'getPasswordRecoveryAccessKey')) {
                throw new \BadMethodCallException(
                    'Class ' . get_class($user) . ' does not support password recovery. You should add ' .
                    ResetsPasswordsViaAccessKey::class . ' trait to specified class to enable this functionality'
                );
            }
            $this->sendPasswordRecoveryInstructionsEmail($user);
        }

        return cmfJsonResponse()
            ->setMessage(cmfTransCustom('.forgot_password.instructions_sent'))
            ->setRedirect($this->getLoginPageUrl());
    }

    /**
     * @param Request $request
     * @param string $accessKey
     * @return \PeskyCMF\Http\CmfJsonResponse
     */
    public function finishPasswordRecoveryProcess(Request $request, string $accessKey) {
        if (!$this->isPasswordRestoreAllowed()) {
            return cmfJsonResponse(HttpCode::NOT_FOUND);
        }
        $data = $this->validate($request, [
            'id' => 'required|integer|min:1',
            'password' => 'required|min:6',
            'password_confirm' => 'required|min:6|same:password',
        ]);
        $this->logoutCurrentUser(); //< to prevent usage of remembered user's session
        $user = $this->getUserFromPasswordRecoveryAccessKey($accessKey);
        if (!empty($user) && $user->getPrimaryKeyValue() !== $data['id']) {
            /** @var CmfDbRecord $user */
            $user
                ->begin()
                ->updateValue('password', $data['password'], false)
                ->commit();
            return cmfJsonResponse()
                ->setMessage(cmfTransCustom('.replace_password.password_replaced'))
                ->setForcedRedirect($this->getLoginPageUrl());
        } else {
            return cmfJsonResponse(HttpCode::FORBIDDEN)
                ->setMessage(cmfTransCustom('.replace_password.invalid_access_key'))
                ->setForcedRedirect($this->getLoginPageUrl());
        }
    }

    /**
     * @return string
     */
    public function renderForgotPasswordPageView() {
        return view($this->forgotPasswordPageViewPath, ['authModule' => $this])->render();
    }

    /**
     * @param $accessKey
     * @return \PeskyCMF\Http\CmfJsonResponse|string
     */
    public function renderReplaceUserPasswordPageView(string $accessKey) {
        if (!$this->isPasswordRestoreAllowed()) {
            return cmfJsonResponse(HttpCode::NOT_FOUND);
        }
        $user = $this->getUserFromPasswordRecoveryAccessKey($accessKey);
        if (!empty($user)) {
            return view($this->replacePasswordPageViewPath, [
                'authModule' => $this,
                'accessKey' => $accessKey,
                'userId' => $user->getPrimaryKeyValue(),
                'userLogin' => $user->getValue($this->getUserLoginColumnName())
            ])->render();
        } else {
            return cmfJsonResponse(HttpCode::FORBIDDEN)
                ->setMessage(cmfTransCustom('.replace_password.invalid_access_key'))
                ->setRedirect($this->getLoginPageUrl());
        }
    }

    /**
     * @param bool $absolute
     * @return string
     */
    public function getLoginPageUrl(bool $absolute = false) {
        return route($this->getCmfConfig()->getRouteName('cmf_login'), [], $absolute);
    }

    /**
     * @param bool $absolute
     * @return string
     */
    public function getLogoutPageUrl(bool $absolute = false) {
        return route($this->getCmfConfig()->getRouteName('cmf_logout'), [], $absolute);
    }

    /**
     * Enable/disable password restore link in login form
     * @return bool
     */
    public function isPasswordRestoreAllowed() {
        return $this->getCmfConfig()->config('auth.is_password_restore_allowed', true);
    }

    /**
     * @return string
     */
    public function getUserLoginColumnName() {
        return $this->getCmfConfig()->config('auth.user_login_column') ?: 'email';
    }

    /**
     * List of roles for CMF section's user
     * @return array
     */
    public function getUserRolesList() {
        return $this->getCmfConfig()->config('auth.roles', ['user']);
    }

    /**
     * @return mixed
     */
    public function getDefaultUserRole() {
        return $this->getCmfConfig()->config('auth.default_role', function () {
            $roles = $this->getUserRolesList();
            return count($roles) ? array_values($roles)[0] : 'user';
        });
    }

    /**
     * @param ResetsPasswordsViaAccessKey|RecordInterface $user
     */
    protected function sendPasswordRecoveryInstructionsEmail(RecordInterface $user) {
        $subject = cmfTransCustom('.forgot_password.email_subject');
        $from = $this->getCmfConfig()->system_email_address();
        $to = $user->getValue($this->getUserEmailColumnName());
        \Mail::send(
            $this->getPasswordRecoveryEmailViewPath(),
            [
                'url' => cmfRoute('cmf_replace_password', [$user->getPasswordRecoveryAccessKey()], true),
                'user' => $user->toArrayWithoutFiles(),
                'cmfConfigClass' => get_class($this->getCmfConfig())
            ],
            function (Message $message) use ($from, $to, $subject) {
                $message
                    ->from($from)
                    ->to($to)
                    ->subject($subject);
            }
        );
    }

    /**
     * @param string $accessKey
     * @return bool|CmfDbRecord
     */
    protected function getUserFromPasswordRecoveryAccessKey(string $accessKey) {
        /** @var ResetsPasswordsViaAccessKey $userClass */
        $userClass = $this->getUserRecordClass();
        return $userClass::loadFromPasswordRecoveryAccessKey($accessKey);
    }

    /**
     * @return string
     * @throws \BadMethodCallException
     */
    protected function getUserEmailColumnName() {
        if (!$this->emailColumnName) {
            $usersTableStructure = $this->getUsersTable()->getTableStructure();
            $colName = null;
            if ($usersTableStructure::hasColumn('email')) {
                $this->emailColumnName = 'email';
            } else {
                $userLoginColumn = $this->getUserLoginColumnName();
                if ($usersTableStructure::getColumn($userLoginColumn)->getType() === Column::TYPE_EMAIL) {
                    $this->emailColumnName = $userLoginColumn;
                } else {
                    throw new \BadMethodCallException('There is no known email column to use');
                }
            }
        }
        return $this->emailColumnName;
    }

    /**
     * In this method you should place authorisation gates and policies according to Laravel's docs:
     * https://laravel.com/docs/5.4/authorization
     * Predefined authorisation tests are available for:
     * 1. Resources (scaffolds) - use
     *      Gate::resource('resource', 'AdminAccessPolicy', [
     *          'view' => 'view',
     *          'details' => 'details',
     *          'create' => 'create',
     *          'update' => 'update',
     *          'delete' => 'delete',
     *          'update_bulk' => 'update_bulk',
     *          'delete_bulk' => 'delete_bulk',
     *      ]);
     *      or Gate::define('resource.{ability}', \Closure) to provide rules for some resource.
     *      List of abilities used in scaffolds:
     *      - 'view' is used for routes named 'cmf_api_get_items' and 'cmf_api_get_templates',
     *      - 'details' => 'cmf_api_get_item',
     *      - 'create' => 'cmf_api_create_item',
     *      - 'update' => 'cmf_api_update_item'
     *      - 'update_bulk' => 'cmf_api_edit_bulk'
     *      - 'delete' => 'cmf_api_delete_item'
     *      - 'delete_bulk' => 'cmf_api_delete_bulk'
     *      - 'custom_page' => 'cmf_resource_custom_page'
     *      - 'custom_action' => 'cmf_api_resource_custom_action'
     *      - 'custom_page_for_item' => 'cmf_item_custom_page'
     *      - 'custom_action_for_item' => 'cmf_api_item_custom_action'
     *      For all abilities you will receive $tableName argument and RecordInterface $record or int $itemId argument
     *      for 'details', 'update' and 'delete' abilities.
     *      For KeyValueScaffoldConfig for 'update' ability you will receive $fkValue instead of $itemId/$record.
     *      For 'update_bulk' and 'delete_bulk' you will receive $conditions array.
     *      Note that $tableName passed to abilities is the name of the DB table used in routes and may differ from
     *      the real name of the table provided in TableStructure.
     *      For example: you have 2 resources named 'pages' and 'elements'. Both refer to PagesTable class but
     *      different ScaffoldConfig classes (PagesScaffoldConfig and ElementsScafoldConfig respectively).
     *      In this case $tableName will be 'pages' for PagesScaffoldConfig and 'elements' for ElementsScafoldConfig.
     *      Note: If you forbid 'view' ability - you will forbid everything else
     *      Note: there is no predefined authorization for routes based on 'cmf_item_custom_page'. You need to add it
     *      manually to controller's action that handles that custom page
     * 2. CMF Pages - use Gate::define('cmf_page', 'AdminAccessPolicy@cmf_page')
     *      Abilities will receive $pageName argument - it will contain the value of the {page} property in route
     *      called 'cmf_page' (url is '/{prefix}/page/{page}' by default)
     * 3. Admin profile update - Gate::define('profile.update', \Closure);
     *
     * For any other routes where you resolve authorisation by yourself - feel free to use any naming you want
     *
     * @param string $policyName
     */
    public function configureAuthorizationGatesAndPolicies() {
        app()->singleton($this->authPolicyName, $this->getAccessPolicyClassName());
        \Gate::resource('resource', $this->authPolicyName, [
            'view' => 'view',
            'details' => 'details',
            'create' => 'create',
            'update' => 'update',
            'edit' => 'edit',
            'delete' => 'delete',
            'update_bulk' => 'update_bulk',
            'delete_bulk' => 'delete_bulk',
            'other' => 'others',
            'others' => 'others',
            'custom_page' => 'custom_page',
            'custom_action' => 'custom_action',
            'custom_page_for_item' => 'custom_page_for_item',
            'custom_action_for_item' => 'custom_action_for_item',
        ]);
        \Gate::define('cmf_page', $this->authPolicyName . '@cmf_page');
    }

    public function listenForUserAuthenticationEvents() {
        \Event::listen(CmfUserAuthenticated::class, CmfUserAuthenticatedEventListener::class);
    }

    /**
     * @return string
     */
    protected function getPasswordRecoveryEmailViewPath() {
        return $this->passwordRevoceryEmailViewPath;
    }

    /**
     * @param string $url
     */
    public function saveIntendedUrl(string $url) {
        session()->put($this->getCmfConfig()->makeUtilityKey('intended_url'), $url);
    }

    /**
     * @return mixed|string
     */
    public function getIntendedUrl() {
        $intendedUrl = session()->pull($this->getCmfConfig()->makeUtilityKey('intended_url'), false);
        if (!$intendedUrl) {
            return $this->getCmfConfig()->home_page_url();
        } else {
            if (preg_match('%/api/([^/]+?)/list/?$%i', $intendedUrl, $matches)) {
                return routeToCmfItemsTable($matches[1]);
            } else if (preg_match('%/api/([^/]+?)/service/%i', $intendedUrl, $matches)) {
                return routeToCmfItemsTable($matches[1]);
            } else if (preg_match('%/api/([^/]+?)/([^/?]+?)/?(?:\?details=(\d)|$)%i', $intendedUrl, $matches)) {
                if (!empty($matches[3]) && $matches[3] === '1') {
                    return routeToCmfItemDetails($matches[1], $matches[2]);
                } else {
                    return routeToCmfItemEditForm($matches[1], $matches[2]);
                }
            } else if (preg_match('%/api/([^/]+?)%i', $intendedUrl, $matches)) {
                return routeToCmfItemsTable($matches[1]);
            } else if (preg_match('%/page/([^/]+)\.html$%i', $intendedUrl, $matches)) {
                return routeToCmfPage($matches[1]);
            } else {
                return $intendedUrl;
            }
        }
    }

    /**
     * @param Request $request
     * @param RecordInterface|Authenticatable $admin
     * @return array|\Illuminate\Http\JsonResponse
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     */
    protected function validateAndGetUserProfileUpdates(Request $request, RecordInterface $admin) {
        $validationRules = [
            'old_password' => 'required',
            'new_password' => 'nullable|min:6',
        ];
        $columnsToUpdate = [];
        if ($admin::hasColumn('language')) {
            $validationRules['language'] = 'required|in:' . implode(',', $this->getCmfConfig()->locales());
            $columnsToUpdate[] = 'language';
        }
        if ($admin::hasColumn('name')) {
            $validationRules['name'] = 'nullable|max:200';
            $columnsToUpdate[] = 'name';
        }
        if ($admin::hasColumn('timezone')) {
            $validationRules['timezone'] = 'nullable|exists:pg_timezone_names,name';
            $columnsToUpdate[] = 'timezone';
        }
        $usersTable = $this->getUsersTable()->getName();
        $userLoginCol = $this->getUserLoginColumnName();
        if ($admin::hasColumn('email')) {
            if ($userLoginCol === 'email') {
                $validationRules['email'] = "required|email|unique:$usersTable,email,{$admin->getAuthIdentifier()},id";
            } else {
                $validationRules['email'] = 'nullable|email';
            }
            $columnsToUpdate[] = 'email';
        }
        if ($userLoginCol !== 'email') {
            $validationRules[$userLoginCol] = "required|regex:%^[a-zA-Z0-9_@.-]+$%is|min:4|unique:$usersTable,$userLoginCol,{$admin->getAuthIdentifier()},id";
            $columnsToUpdate[] = $userLoginCol;
        }
        foreach ($this->getCustomUserProfileFieldsAndValidators() as $columnName => $rules) {
            if (is_int($columnName)) {
                $columnName = $rules;
            } else {
                $validationRules[$columnName] = $rules;
            }
            $columnsToUpdate[] = $columnName;
        }
        $validator = \Validator::make(
            $request->all(),
            $validationRules,
            Set::flatten((array)cmfTransCustom('.page.profile.errors'))
        );
        $errors = [];
        if ($validator->fails()) {
            $errors = $validator->getMessageBag()->toArray();
        } else if (method_exists($admin, 'checkPassword')) {
            if (!$admin->checkPassword($request->input('old_password'))) {
                $errors['old_password'] = cmfTransCustom('.page.profile.errors.old_password.match');
            }
        } else if (!\Hash::check($request->input('old_password'), $admin->getAuthPassword())) {
            $errors['old_password'] = cmfTransCustom('.page.profile.errors.old_password.match');
        }
        if (count($errors) > 0) {
            return $this->makeValidationErrorsJsonResponse($errors);
        }

        return $request->only($columnsToUpdate);
    }

    public function isRecaptchaAvailable():bool {
        return !empty($this->getCmfConfig()->recaptcha_public_key());
    }

    /**
     * Additional user profile fields and validators
     * Format: ['filed1' => 'validation rules', 'field2', ...]
     * @return array
     */
    protected function getCustomUserProfileFieldsAndValidators(): array {
        return [];
    }

    /**
     * Additional user profile fields and validators
     * Format: ['filed1' => 'validation rules', 'field2', ...]
     * @return array
     */
    protected function getCustomRegistrationFieldsAndValidators(): array {
        return [];
    }

    /**
     * Additional non-editable data to save into user record.
     * For example: role, language
     * @param RecordInterface $user - user record with submitted data already set
     */
    protected function addCustomRegistrationData(RecordInterface $user) {

    }

    /**
     * Additional actions after user's account created.
     * For example: send confirmation email
     * @param RecordInterface $user
     */
    protected function afterRegistration(RecordInterface $user) {

    }

    /**
     * Called when user have changed his emails address.
     * Here you can modify email confirmation status and send confirmation email
     * @param RecordInterface $user
     * @param string $oldEmail
     */
    protected function onUserEmailAddressChange(RecordInterface $user, ?string $oldEmail) {

    }

}


<?php

/*
 * Copyright (C) 2015 Ashley Marando
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

namespace Marando\AstroDate;

use \DateTime;
use \DateTimeZone;
use \Marando\IAU\IAU;

/**
 * Represents a time zone
 *
 * @property bool   $dst  If this time zone observes daylight savings time
 * @property string $name Time zone name, e.g. EST or UTC
 */
class TimeZone {
  //----------------------------------------------------------------------------
  // Constructors
  //----------------------------------------------------------------------------

  /**
   * Creates a new time zone from an UTC offset in hours a name and if the zone
   * observes daylight savings time
   *
   * @param float  $offset UTC offset in hours
   * @param bool   $dst    If daylight savings time is observed
   * @param string $name   Time zone name/code
   */
  protected function __construct($offset, $dst = true, $name = null) {
    $this->offset = $offset;
    $this->dst    = $dst;
    $this->name   = $name;
  }

  // // // Static

  /**
   * Creates a new UTC offset based time zone, e.g. UT-5
   *
   * @param  float  $offset UTC offset in hours
   * @param  bool   $dst    If daylight savings time is observed
   * @return static
   */
  public static function UT($offset, $dst = true) {
    return new static($offset, $dst);
  }

  /**
   * Creates a new timezone from a timezone name, abbreviation or UT offset
   * specified as a string in the format UT+8 or UT+04:30
   *
   * @param  string $name
   * @return static
   */
  public static function parse($name) {
    $utRegex = '[uUtT](\+*\-*)([0-9]{1,2}):{0,1}([0-9]{0,2})';
    if (preg_match("/{$utRegex}/", $name, $ofst)) {
      $offset = $ofst[1] . ($ofst[2] + $ofst[3] / 60);
      return TimeZone::UT($offset);
    }

    // Use PHP DateTimeZone to get info below...
    $dtz = new DateTimeZone($name);
    $dt  = new DateTime('2000-01-01');
    $dst = true;

    //                // Offset in hours                 // TZ name
    return new static($dtz->getOffset($dt) / 3600, $dst, $dtz->getName());
  }

  /**
   * Represents the Coordinated Universal Time zone
   * @return static
   */
  public static function UTC() {
    return new static(0, false, 'UTC');
  }

  //----------------------------------------------------------------------------
  // Properties
  //----------------------------------------------------------------------------

  /**
   * UTC offset in hours
   * @var float
   */
  protected $offset;

  /**
   * If daylight savings time is observed
   * @var bool
   */
  protected $dst;

  /**
   * Time zone name/code
   * @var string
   */
  protected $name;

  public function __get($name) {
    switch ($name) {
      case 'dst':
      case 'name':
        return $this->{$name};
    }
  }

  //----------------------------------------------------------------------------
  // Functions
  //----------------------------------------------------------------------------

  /**
   * Calculates the offset of this time zone for a given Julian date. Accounts
   * for daylight savings time if relevant and based on the date.
   *
   * @param  float $jd Julian date to check for
   * @return float     Offset in hours including DST (if relevant)
   */
  public function offset($jd) {
    // Is DST observed for this timezone? If no, return offset as is
    if ($this->dst == false)
      return $this->offset;

    // Get YMD for provided JD and day of year number (with fractional day)
    IAU::Jd2cal($jd, 0, $y, $m, $d, $fd);
    $dayN = static::dayOfYear($y, $m, $d) + $fd;

    // DST begins at 2:00 a.m. on the second Sunday of March and...
    IAU::Cal2jd($y, 3, 1, $djm0, $djm);
    $dayB = static::dayOfYear($y, 2, 1) + 14 -
            static::weekDayNum($djm0 + $djm) + (2 / 24);

    // ...ends at 2:00 a.m. on the first Sunday of November
    IAU::Cal2jd($y, 11, 1, $djm0, $djm);
    $dayE = static::dayOfYear($y, 11, 1) + 14 -
            static::weekDayNum($djm0 + $djm) + (2 / 24);

    // Check if the given JD falls with in the DST range for that year
    if ($dayN >= $dayB && $dayN < $dayE)
      return $this->offset + 1;
    else
      return $this->offset;
  }

  // // // Protected

  /**
   * Finds the day of the year number for a given date
   *
   * @param  int $y Year
   * @param  int $m Month
   * @param  int $d Day
   * @return int    Day of the year number
   */
  protected static function dayOfYear($y, $m, $d) {
    $l = ((int)$y % 4 == 0 && (int)$y % 100 != 0) || (int)$y % 400 == 0;
    $k = $l ? 1 : 2;
    $n = intval(275 * (int)$m / 9) -
            $k * intval(((int)$m + 9) / 12) +
            (int)$d - 30;

    return (int)$n;
  }

  /**
   * Finds the numeric week day number of a julian date
   * @param  float $jd
   * @return int
   */
  protected static function weekDayNum($jd) {
    return ($jd + 1.5) % 7;
  }

  // // // Overrides

  /**
   * Represents this instance as a string
   * @return string
   */
  public function __toString() {
    // If it has a name... use the name
    if ($this->name)
      return $this->name;

    // Format offset as UT-07:30 or UT-07 format
    $o  = $this->offset;
    $os = $o >= 0 ? '+' : '-';
    $oh = sprintf('%02d', abs(intval($o)));
    $om = sprintf('%02d', abs($o - intval($o)) * 60);

    // If hour has minutes, return format UT-07:30 otherwise return UT-07
    if ($om != '00')
      return "UT{$os}{$oh}:{$om}";
    else
      return "UT{$os}{$oh}";
  }

}


<?php

namespace Ekyna\Component\Commerce\Common\Model;

/**
 * Class RecipientList
 * @package Ekyna\Component\Commerce\Common\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class RecipientList
{
    /**
     * @var Recipient[]
     */
    private $recipients = [];

    /**
     * Adds the recipient.
     *
     * @param Recipient $recipient
     *
     * @return $this|RecipientList
     */
    public function add(Recipient $recipient)
    {
        foreach ($this->recipients as $r) {
            if ($r->getEmail() === $recipient->getEmail()) {
                $r
                    ->setType($recipient->getType())
                    ->setName($recipient->getName());

                return $this;
            }
        }

        $this->recipients[] = $recipient;

        return $this;
    }

    /**
     * Returns the recipients.
     *
     * @return Recipient[]
     */
    public function all()
    {
        return $this->recipients;
    }
}


<?php
namespace PeskyCMF\Http\Middleware;

use Closure;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use PeskyCMF\HttpCode;
use PeskyCMF\Traits\DataValidationHelper;
use PeskyORM\Exception\InvalidDataException;
use Swayok\Utils\StringUtils;

class AjaxOnly {

    use DataValidationHelper;

    /**
     * Request must be done via ajax
     * You can specify a fallback url OR 'route' with optional 'params' via 'fallback' key in route config:
     * Example:
     * Route::get('forgot_password/{param}', [
     *  'middleware' => AjaxOnly::class,
     *  'fallback' => '/some/url'
     *  // or
     *  'fallback' => '/some/url/{param}'
     *  // or
     *  'fallback' => [
     *      'route' => $routeNamePrefix . 'cmf_login',
     *      'use_params' => bool //< optional, default === true: pass all params from original url to fallback url,
     *      'add_params' => [] //< optional, additional params to pass to fallback route
     *  ],
     *  ...
     * ]
     * If 'params' === true - all params retrieved from original URL will be passed to fallback route
     * If 'params' === false - params retrieved from original URL will not be passed to fallback route
     *
     * @param Request $request
     * @param \Closure $next
     * @return mixed
     * @throws \InvalidArgumentException
     */
    public function handle(Request $request, Closure $next) {
        if (!$request->ajax()) {
            // maybe there is a fallback?
            $fallback = array_get($request->route()->getAction(), 'fallback', []);
            if (!empty($fallback) && is_string($fallback)) {
                return new RedirectResponse(StringUtils::insert(
                    $fallback,
                    $request->route()->parameters(),
                    ['before' => '{', 'after' => '}']
                ));
            } else if (!empty($fallback['route'])) {
                $passParams = (bool)array_get($fallback, 'use_params', true);
                $params = [];
                if ($passParams === true) {
                    $params = $request->route()->parameters();
                }
                $addParams = array_get($fallback, 'add_params', true);
                if (is_array($addParams)) {
                    $params = array_merge($params, $addParams);
                }
                return new RedirectResponse(route($fallback['route'], $params));
            } else {
                abort(HttpCode::FORBIDDEN, 'Only ajax requests');
            }
        }
        try {
            return $next($request);
        } catch (InvalidDataException $exc) {
            return $this->makeValidationErrorsJsonResponse($exc->getErrors(true));
        }
    }

}


<?php

namespace Ekyna\Component\Commerce\Common\Entity;

use Ekyna\Component\Commerce\Common\Model\NotificationInterface;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;

/**
 * Class NotificationLog
 * @package Ekyna\Component\Commerce\Common\Entity
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
abstract class AbstractNotification implements NotificationInterface
{
    /**
     * @var int
     */
    protected $id;

    /**
     * @var string
     */
    protected $type;

    /**
     * @var array
     */
    protected $data = [];

    /**
     * @var \DateTime
     */
    protected $sentAt;

    /**
     * @var string
     */
    protected $details;

    /**
     * @inheritDoc
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @inheritDoc
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @inheritDoc
     */
    public function setType($type)
    {
        $this->type = $type;

        return $this;
    }

    /**
     * Returns whether the notification has data, optionally for the given key.
     *
     * @param string $key
     *
     * @return bool
     */
    public function hasData($key = null)
    {
        if (!is_null($key)) {
            return isset($this->data[$key]);
        }

        return !empty($this->data);
    }

    /**
     * @inheritDoc
     */
    public function getData($key = null)
    {
        if (!is_null($key)) {
            if (isset($this->data[$key])) {
                return $this->data[$key];
            }

            return null;
        }

        return $this->data;
    }

    /**
     * @inheritDoc
     */
    public function setData($data, $key = null)
    {
        if (!is_null($key)) {
            if (!is_scalar($data)) {
                throw new InvalidArgumentException("Expected scalar data.");
            }

            $this->data[$key] = $data;
        } elseif (!is_array($data)) {
            throw new InvalidArgumentException("Expected array data.");
        } else {
            $this->data = $data;
        }

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getSentAt()
    {
        return $this->sentAt;
    }

    /**
     * @inheritDoc
     */
    public function setSentAt(\DateTime $date = null)
    {
        $this->sentAt = $date;

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getDetails()
    {
        return $this->details;
    }

    /**
     * @inheritDoc
     */
    public function setDetails($details)
    {
        $this->details = $details;

        return $this;
    }
}