<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

namespace Shopware\Plugins\ViisonPickwareCommon\Subscribers\Api;

use Shopware\Plugins\ViisonCommon\Classes\Subscribers\AbstractBaseSubscriber;
use Shopware\Plugins\ViisonPickwareCommon\Classes\BasicAuthResolver;
use Shopware\Plugins\ViisonPickwareCommon\Classes\Exceptions\IncompatibleAppVersionException;
use Shopware\Plugins\ViisonPickwareCommon\Classes\NoCheckAuthAdapter;
use Shopware\Plugins\ViisonPickwareCommon\Classes\Util;
use Shopware_Components_Plugin_Bootstrap;
use Zend_Auth_Adapter_Http;

class Api extends AbstractBaseSubscriber
{
    /**
     * @var boolean $isPickwareApiCall
     */
    private $isPickwareApiCall = false;

    /**
     * @see \Shopware\Plugins\ViisonCommon\Classes\Subscribers\Base::getSubscribedEvents()
     */
    public static function getSubscribedEvents()
    {
        return [
            'Enlight_Controller_Action_PreDispatch_Api' => 'onPreDispatchApi',
            'Enlight_Controller_Front_DispatchLoopStartup' => [
                'onDispatchLoopStartup',
                // Ensure this listener is executed before the request has been routed by ERP because it needs the
                // original controller name.
                -1000,
            ],
            'Enlight_Bootstrap_InitResource_Auth' => [
                'onInitResourceAuth',
                -100,
            ],
            'Enlight_Bootstrap_AfterInitResource_Auth' => 'onAfterInitResourceAuth',
        ];
    }

    /**
     * @param \Enlight_Event_EventArgs $args
     */
    public function onPreDispatchApi(\Enlight_Event_EventArgs $args)
    {
        // We need to early return here in case of exceptions already added to the response so they are processed from
        // the shopware error handler. If we do not do that the shopware error handler is not called and no exceptions
        // are returned in the response.
        $exceptions = $args->getResponse()->getException();
        if (count($exceptions) > 0) {
            return;
        }

        if (!$this->isPickwareApiCall) {
            // Verify that the app's major version matches that of its respective plugin(s) to prevent problems caused
            // by apps that remain logged in after a plugin update. Only do this for requests that happen after the
            // login.
            $this->validateAppVersion();
        }
    }

    /**
     * @throws IncompatibleAppVersionException iff the requesting app's major version and the major version of one of
     *         its respective plugins differ
     */
    private function validateAppVersion()
    {
        $requestingAppName = Util::getRequestingAppName();
        $requestingAppVersion = Util::getRequestingAppVersion();
        $majorVersionOfRequestingApp = intval(explode('.', $requestingAppVersion)[0]);

        $pickwarePluginBootstraps = $this->get('viison_pickware_common.device_licensing')->getAppSupportingPickwarePlugins();
        /** @var Shopware_Components_Plugin_Bootstrap $plugin */
        foreach ($pickwarePluginBootstraps as $plugin) {
            if (!in_array($requestingAppName, $plugin->getNamesOfSupportedPickwareApps(), true)) {
                continue;
            }

            $pluginMajorVersion = intval(explode('.', $plugin->getInfo()['version'])[0]);
            if ($majorVersionOfRequestingApp < $pluginMajorVersion) {
                throw IncompatibleAppVersionException::appVersionTooLow(
                    $requestingAppVersion,
                    $pluginMajorVersion . '.0.0'
                );
            }

            // Allow the plugin to provide a different maximum major version for app compatiblity
            $maxSupportedMajorVersion = (method_exists($plugin, 'getMaxSupportedAppMajorVersion')) ? $plugin->getMaxSupportedAppMajorVersion($requestingAppName) : $pluginMajorVersion;
            if ($majorVersionOfRequestingApp > $maxSupportedMajorVersion) {
                throw IncompatibleAppVersionException::pluginVersionTooLow(
                    $plugin->getInfo()['label'],
                    $plugin->getInfo()['version'],
                    $majorVersionOfRequestingApp . '.0.0'
                );
            }
        }
    }

    /**
     * @param \Enlight_Event_EventArgs $args
     */
    public function onDispatchLoopStartup(\Enlight_Event_EventArgs $args)
    {
        $request = $args->getRequest();
        if ($request->getModuleName() === 'api') {
            $this->isPickwareApiCall = $request->getControllerName() === 'pickware';
        }
    }

    /**
     * Checks whether the current request is a '/api/pickware' request and, if it is,
     * replaces the auth resource's adapters with the custom 'NoCheckAuthAdapter',
     * which allows request without authentication. Otherwise this method does nothing.
     *
     * @param \Enlight_Event_EventArgs $args
     */
    public function onInitResourceAuth(\Enlight_Event_EventArgs $args)
    {
        if (!$this->isPickwareApiCall) {
            return;
        }

        // Create the original auth resource using the RestApi plugin
        $resource = $this->get('plugins')->get('Core')->get('RestApi')->onInitResourceAuth($args);

        // Replace its adapters to allow any requests
        $adapter = new NoCheckAuthAdapter();
        $resource->setBaseAdapter($adapter);
        // We must use reflection to overwrite the '_adapter' field, because Shopware_Components_Auth,
        // the de facto class of $resource, uses the '_adapter' property as an array, while the extended
        // class Enlight_Components_Auth uses it as a single object. Hence the 'setAdapter()' expects a
        // single instance of Zend_Auth_Adapter_Interface. As a result 'setAdapter()' is basically useless,
        // since calling it with an array would be comptible with the internal logic of Shopware_Components_Auth,
        // but not with the method signature of Enlight_Components_Auth and therefore raises a fatal error
        // in PHP 7. On the other hand, calling it with an instance of Zend_Auth_Adapter_Interface is compatible
        // with the method signature of Enlight_Components_Auth, but results in a fatal error as well,
        // because the internal implementation of Shopware_Components_Auth tries to acces the instance as
        // an array. By using reflection, we are able to overwrite '_adapter' with an array containing only
        // our custom adapter. Hence the internal implementation of Shopware_Components_Auth won't break
        // and we still overwrite all existing adapters.
        $reflectionObject = new \ReflectionObject($resource);
        $_adapterProperty = $reflectionObject->getProperty('_adapter');
        $_adapterProperty->setAccessible(true);
        $_adapterProperty->setValue($resource, [
            $adapter,
        ]);

        return $resource;
    }

    /**
     * Adds a resolver for HTTP basic auth to the initialized 'auth' service, if the request is not a special Pickware
     * API request and the resolver does not exist yet (i.e. when running in Shopware < 5.3.2).
     *
     * @param \Enlight_Event_EventArgs $args
     */
    public function onAfterInitResourceAuth(\Enlight_Event_EventArgs $args)
    {
        if ($this->isPickwareApiCall) {
            return;
        }

        $authAdapter = $args->getSubject()->get('auth')->getBaseAdapter();
        if ($authAdapter instanceof Zend_Auth_Adapter_Http && !$authAdapter->getBasicResolver()) {
            // Add a resolver for 'basic' authorization
            $basicResolver = new BasicAuthResolver($this->get('models'));
            $authAdapter->setBasicResolver($basicResolver);

            // Use reflection to activate both 'basic' and 'digest' authorization in the adapter
            $reflectionObject = new \ReflectionObject($authAdapter);
            $_acceptSchemesProperty = $reflectionObject->getProperty('_acceptSchemes');
            $_acceptSchemesProperty->setAccessible(true);
            $_acceptSchemesProperty->setValue($authAdapter, ['basic', 'digest']);
        }
    }
}
